initial commit

This commit is contained in:
Rokas Puzonas 2026-07-13 22:52:29 +03:00
commit 9332649a63
94 changed files with 8640 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
zig-out
zig-pkg
.zig-cache

18
README.md Normal file
View File

@ -0,0 +1,18 @@
# Gaem
## Run
Linux and Windows:
```sh
zig build run
```
Web:
```sh
zig build -Dtarget=wasm32-emscripten run
```
Cross-compile for Windows from Linux:
```sh
zig build -Dtarget=x86_64-windows
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

393
build.zig Normal file
View File

@ -0,0 +1,393 @@
const std = @import("std");
const sokol = @import("sokol");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const assets_dir = "assets";
const asset_files = [_][]const u8{
"tranquil_tunnels_transparent.png"
// "sokoban/sokoban_tilesheet.png",
// "roboto-font/Roboto-Regular.ttf",
// "tiled/map.tmx",
// "tiled/tileset.tsx"
};
const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir });
const is_wasm = target.result.cpu.arch.isWasm();
const is_debug = (optimize == .Debug);
const has_imgui = b.option(bool, "imgui", "ImGui integration") orelse is_debug;
const assets_from_filesystem = b.option(bool, "assets-filesystem", "Allow reading assets from filesystem") orelse is_debug;
var has_tracy = false;
if (!is_wasm) {
has_tracy = b.option(bool, "tracy", "Tracy integration") orelse is_debug;
}
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const build_options = b.addOptions();
build_options.addOption(bool, "has_imgui", has_imgui);
build_options.addOption(?[]const u8, "assets_dir", if (assets_from_filesystem) asset_dir_realpath else null);
exe_mod.addOptions("build_options", build_options);
exe_mod.addImport("embedded_assets", try createEmbeddedAssets(b, assets_dir, &asset_files));
const math_mod = b.createModule(.{
.root_source_file = b.path("src/math.zig")
});
exe_mod.addImport("math", math_mod);
const dep_tiled = b.dependency("tiled", .{});
exe_mod.addImport("tiled", dep_tiled.module("tiled"));
const dep_sokol = b.dependency("sokol", .{
.target = target,
.optimize = optimize,
.with_sokol_imgui = has_imgui
});
const mod_sokol = dep_sokol.module("sokol");
exe_mod.linkLibrary(dep_sokol.artifact("sokol_clib"));
exe_mod.addImport("sokol", mod_sokol);
const dep_shdc = dep_sokol.builder.dependency("shdc", .{});
var slang = sokol.shdc.Slang{};
if (is_wasm) {
slang.wgsl = true;
} else {
slang.glsl410 = true;
slang.hlsl4 = true;
slang.metal_macos = true;
slang.glsl300es = true;
}
const mod_shader = try sokol.shdc.createModule(b, "shader", mod_sokol, .{
.shdc_dep = dep_shdc,
.input = "src/shader.glsl",
.output = "src/shader.zig",
.slang = slang
});
mod_shader.addImport("math", math_mod);
exe_mod.addImport("shader", mod_shader);
if (has_imgui) {
if (b.lazyDependency("cimgui", .{
.target = target,
.optimize = optimize,
})) |dep_cimgui| {
const cimgui = b.lazyImport(@This(), "cimgui").?;
const cimgui_conf = cimgui.getConfig(true);
exe_mod.addImport("cimgui", dep_cimgui.module(cimgui_conf.module_name));
dep_sokol.artifact("sokol_clib").root_module.addIncludePath(dep_cimgui.path(cimgui_conf.include_dir));
}
}
const dep_stb = b.dependency("stb", .{});
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,
.optimize = optimize,
.tracy_enable = has_tracy,
.tracy_only_localhost = true
});
if (has_tracy) {
exe_mod.linkLibrary(dep_tracy.artifact("tracy"));
}
exe_mod.addImport("tracy", dep_tracy.module("tracy"));
var run_cmd_step: *std.Build.Step = undefined;
if (is_wasm) {
if (true) {
@panic("TODO: test in 0.17");
}
const build_step = buildWasm(b, .{
.name = "index",
.root_module = exe_mod,
.dep_sokol = dep_sokol,
});
b.getInstallStep().dependOn(build_step);
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 = "sokol_template_v2",
.root_module = exe_mod
});
b.installArtifact(exe);
{
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| {
run_cmd.addArgs(args);
}
run_cmd_step = &run_cmd.step;
}
{
const exe_tests = b.addTest(.{ .root_module = exe_mod, });
const run_exe_tests = b.addRunArtifact(exe_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_exe_tests.step);
}
}
const run_step = b.step("run", "Run game");
run_step.dependOn(run_cmd_step);
}
fn createEmbeddedAssets(
b: *std.Build,
assets_dir: []const u8,
asset_files: []const []const u8
) !*std.Build.Module {
const write_files = b.addWriteFiles();
var embedded_assets: std.ArrayList(u8) = .empty;
defer embedded_assets.deinit(b.allocator);
try embedded_assets.appendSlice(b.allocator, "pub const files = .{\n");
const mod_embedded_assets = b.createModule(.{});
for (asset_files) |asset_file| {
mod_embedded_assets.addAnonymousImport(asset_file, .{
.root_source_file = b.path(b.pathJoin(&.{ assets_dir, asset_file }))
});
try embedded_assets.appendSlice(b.allocator, " .{ ");
try embedded_assets.appendSlice(b.allocator, ".name = \"");
try embedded_assets.appendSlice(b.allocator, asset_file);
try embedded_assets.appendSlice(b.allocator, "\"");
try embedded_assets.appendSlice(b.allocator, ", ");
try embedded_assets.appendSlice(b.allocator, ".contents = @embedFile(\"");
try embedded_assets.appendSlice(b.allocator, asset_file);
try embedded_assets.appendSlice(b.allocator, "\")");
try embedded_assets.appendSlice(b.allocator, " },\n");
}
try embedded_assets.appendSlice(b.allocator, "};\n");
mod_embedded_assets.root_source_file = write_files.add("embedded_assets.zig", embedded_assets.items);
return mod_embedded_assets;
}
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;
}
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 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.root_module.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 relative_icon_path = try std.fs.path.relative(
b.allocator,
"",
null,
self.resource_file.dirname().getPath(b),
self.icon_file.getPath(b)
);
std.mem.replaceScalar(u8, relative_icon_path, '\\', '/');
{
const cwd = std.Io.Dir.cwd();
const resource_file = try cwd.createFile(b.graph.io, self.resource_file.getPath(b), .{ });
defer resource_file.close(b.graph.io);
var buff: [4*4096]u8 = undefined;
var file_writer = resource_file.writer(b.graph.io, &buff);
const w = &file_writer.interface;
try w.writeAll("IDI_ICON ICON \"");
try w.writeAll(relative_icon_path);
try w.writeAll("\"");
}
}
};
const BuildWasmOptions = struct {
name: []const u8,
root_module: *std.Build.Module,
dep_sokol: *std.Build.Dependency,
};
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", .{});
// TODO:
// const emsdk_install_step = sokol.emSdkInstallStep(b, dep_emsdk, .{});
// main_lib.step.dependOn(emsdk_install_step);
patchWasmIncludeDirs(
opts.root_module,
dep_emsdk.path("upstream/emscripten/cache/sysroot/include"),
&(dep_sokol.artifact("sokol_clib").step)
);
const link_step = sokol.emLinkStep(b, .{
.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/engine/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);
}
}

31
build.zig.zon Normal file
View File

@ -0,0 +1,31 @@
.{
.name = .sokol_template_v2,
.version = "0.0.0",
.fingerprint = 0xf440b4e6e51f5206, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
.sokol = .{
.url = "git+https://github.com/floooh/sokol-zig.git#ba97ac4e82e36a0f6c62f7fad5dc9c2d82beadd8",
.hash = "sokol-0.1.0-pb1HK4prNwDGx2RJFyv7VtYJubDahi19XKioPzPeMCxv",
},
.stb = .{
.path = "./libs/stb",
},
.tiled = .{
.path = "./libs/tiled",
},
.tracy = .{
.path = "./libs/tracy",
},
.cimgui = .{
.url = "git+https://github.com/floooh/dcimgui.git#3c2b41a9333b1195e2c2f8ce6a2d3e50660a73bf",
.hash = "cimgui-0.1.0-44ClkR8dmgDqqnfloyLE1bUAtl4zSKTT1wxaHSEC0ikV",
.lazy = true
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

37
libs/stb/build.zig Normal file
View File

@ -0,0 +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 = b.addModule(name, .{
.target = target,
.optimize = optimize,
.root_source_file = zig_file,
.link_libc = true,
});
mod.addIncludePath(stb_dependency.path("."));
mod.addCSourceFile(.{
.file = impl_file,
.flags = &.{}
});
}

17
libs/stb/build.zig.zon Normal file
View File

@ -0,0 +1,17 @@
.{
.name = .stb_image,
.version = "0.0.0",
.fingerprint = 0xe5d3607840482046, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2",
.dependencies = .{
.stb = .{
.url = "git+https://github.com/nothings/stb.git#f1c79c02822848a9bed4315b12c8c8f3761e1296",
.hash = "N-V-__8AABQ7TgCnPlp8MP4YA8znrjd6E-ZjpF1rvrS8J_2I",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

View File

@ -0,0 +1,31 @@
const std = @import("std");
const c = @cImport({
@cDefine("STBI_NO_STDIO", {});
@cInclude("stb_image.h");
});
const STBImage = @This();
rgba8_pixels: [*c]u8,
width: u32,
height: u32,
pub fn load(png_data: []const u8) !STBImage {
var width: c_int = undefined;
var height: c_int = undefined;
const pixels = c.stbi_load_from_memory(png_data.ptr, @intCast(png_data.len), &width, &height, null, 4);
if (pixels == null) {
return error.InvalidPng;
}
return STBImage{
.rgba8_pixels = pixels,
.width = @intCast(width),
.height = @intCast(height)
};
}
pub fn deinit(self: *const STBImage) void {
c.stbi_image_free(self.rgba8_pixels);
}

View File

@ -0,0 +1,3 @@
#define STB_IMAGE_IMPLEMENTATION
#define STBI_NO_STDIO
#include "stb_image.h"

View File

@ -0,0 +1,36 @@
const std = @import("std");
const c = @cImport({
@cInclude("stb_rect_pack.h");
});
const STBRectPack = @This();
ctx: c.stbrp_context,
pub fn init(self: *STBRectPack, width: u32, height: u32, nodes: []Node) void {
c.stbrp_init_target(&self.ctx, @intCast(width), @intCast(height), nodes.ptr, @intCast(nodes.len));
}
pub fn setupAllowOutOfMem(self: *STBRectPack, enabled: bool) void {
c.stbrp_setup_heuristic(&self.ctx, if (enabled) 1 else 0);
}
pub fn setupHeuristic(self: *STBRectPack, heuristic: Heuristic) void {
c.stbrp_setup_heuristic(&self.ctx, heuristic);
}
pub fn packRects(self: *STBRectPack, rects: []Rect) bool {
return c.stbrp_pack_rects(&self.ctx, rects.ptr, @intCast(rects.len)) == 1;
}
pub const Heuristic = enum(i32) {
Skyline_BL_sortHeight = c.STBRP_HEURISTIC_Skyline_BL_sortHeight,
Skyline_BF_sortHeight = c.STBRP_HEURISTIC_Skyline_BF_sortHeight,
_,
pub const default = Heuristic.Skyline_BL_sortHeight;
};
pub const Node = c.stbrp_node;
pub const Rect = c.stbrp_rect;

View File

@ -0,0 +1,2 @@
#define STB_RECT_PACK_IMPLEMENTATION
#include "stb_rect_pack.h"

View File

@ -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: u32) i32 {
return c.stbtt_GetGlyphKernAdvance(&self.info, @intCast(prev_glyph), @intCast(next_glyph));
}
};

View File

@ -0,0 +1,4 @@
#define STB_TRUETYPE_IMPLEMENTATION
// TODO: #define STBTT_STATIC
#include "stb_truetype.h"

170
libs/stb/src/stb_vorbis.zig Normal file
View File

@ -0,0 +1,170 @@
const std = @import("std");
const assert = std.debug.assert;
const log = std.log.scoped(.stb_vorbis);
const c = @cImport({
@cDefine("STB_VORBIS_NO_INTEGER_CONVERSION", {});
@cDefine("STB_VORBIS_NO_STDIO", {});
@cDefine("STB_VORBIS_HEADER_ONLY", {});
@cInclude("stb_vorbis.c");
});
const STBVorbis = @This();
pub const Error = error {
NeedMoreData,
InvalidApiMixing,
OutOfMemory,
FeatureNotSupported,
TooManyChannels,
FileOpenFailure,
SeekWithoutLength,
UnexpectedEof,
SeekInvalid,
InvalidSetup,
InvalidStream,
MissingCapturePattern,
InvalidStreamStructureVersion,
ContinuedPacketFlagInvalid,
IncorrectStreamSerialNumber,
InvalidFirstPage,
BadPacketType,
CantFindLastPage,
SeekFailed,
OggSkeletonNotSupported,
Unknown
};
fn errorFromInt(err: c_int) ?Error {
return switch (err) {
c.VORBIS__no_error => null,
c.VORBIS_need_more_data => Error.NeedMoreData,
c.VORBIS_invalid_api_mixing => Error.InvalidApiMixing,
c.VORBIS_outofmem => Error.OutOfMemory,
c.VORBIS_feature_not_supported => Error.FeatureNotSupported,
c.VORBIS_too_many_channels => Error.TooManyChannels,
c.VORBIS_file_open_failure => Error.FileOpenFailure,
c.VORBIS_seek_without_length => Error.SeekWithoutLength,
c.VORBIS_unexpected_eof => Error.UnexpectedEof,
c.VORBIS_seek_invalid => Error.SeekInvalid,
c.VORBIS_invalid_setup => Error.InvalidSetup,
c.VORBIS_invalid_stream => Error.InvalidStream,
c.VORBIS_missing_capture_pattern => Error.MissingCapturePattern,
c.VORBIS_invalid_stream_structure_version => Error.InvalidStreamStructureVersion,
c.VORBIS_continued_packet_flag_invalid => Error.ContinuedPacketFlagInvalid,
c.VORBIS_incorrect_stream_serial_number => Error.IncorrectStreamSerialNumber,
c.VORBIS_invalid_first_page => Error.InvalidFirstPage,
c.VORBIS_bad_packet_type => Error.BadPacketType,
c.VORBIS_cant_find_last_page => Error.CantFindLastPage,
c.VORBIS_seek_failed => Error.SeekFailed,
c.VORBIS_ogg_skeleton_not_supported => Error.OggSkeletonNotSupported,
else => Error.Unknown
};
}
handle: *c.stb_vorbis,
pub fn init(data: []const u8, alloc_buffer: []u8) Error!STBVorbis {
const stb_vorbis_alloc: c.stb_vorbis_alloc = .{
.alloc_buffer = alloc_buffer.ptr,
.alloc_buffer_length_in_bytes = @intCast(alloc_buffer.len)
};
var error_code: c_int = -1;
const handle = c.stb_vorbis_open_memory(
data.ptr,
@intCast(data.len),
&error_code,
&stb_vorbis_alloc
);
if (handle == null) {
return errorFromInt(error_code) orelse Error.Unknown;
}
return STBVorbis{
.handle = handle.?
};
}
pub fn getMinimumAllocBufferSize(self: STBVorbis) u32 {
const info = self.getInfo();
return info.setup_memory_required + @max(info.setup_temp_memory_required, info.temp_memory_required);
}
fn getLastError(self: STBVorbis) ?Error {
const error_code = c.stb_vorbis_get_error(self.handle);
return errorFromInt(error_code);
}
pub fn seek(self: STBVorbis, sample_number: u32) !void {
const success = c.stb_vorbis_seek(self.handle, sample_number);
if (success != 1) {
return self.getLastError() orelse Error.Unknown;
}
}
pub fn getStreamLengthInSamples(self: STBVorbis) u32 {
return c.stb_vorbis_stream_length_in_samples(self.handle);
}
pub fn getStreamLengthInSeconds(self: STBVorbis) f32 {
return c.stb_vorbis_stream_length_in_seconds(self.handle);
}
pub fn getSamples(
self: STBVorbis,
channels: []const [*]f32,
max_samples_per_channel: u32
) u32 {
const samples_per_channel = c.stb_vorbis_get_samples_float(
self.handle,
@intCast(channels.len),
@constCast(@ptrCast(channels.ptr)),
@intCast(max_samples_per_channel)
);
return @intCast(samples_per_channel);
}
const Frame = struct {
channels: []const [*c]const f32,
samples_per_channel: u32
};
pub fn getFrame(self: STBVorbis) Frame {
var output: [*c][*c]f32 = null;
var channels: c_int = undefined;
const samples_per_channel = c.stb_vorbis_get_frame_float(
self.handle,
&channels,
&output
);
return Frame{
.channels = output[0..@intCast(channels)],
.samples_per_channel = @intCast(samples_per_channel)
};
}
pub const Info = struct {
sample_rate: u32,
channels: u32,
setup_memory_required: u32,
setup_temp_memory_required: u32,
temp_memory_required: u32,
max_frame_size: u32
};
pub fn getInfo(self: STBVorbis) Info {
const info = c.stb_vorbis_get_info(self.handle);
return Info{
.sample_rate = info.sample_rate,
.channels = @intCast(info.channels),
.setup_memory_required = info.setup_memory_required,
.setup_temp_memory_required = info.setup_temp_memory_required,
.temp_memory_required = info.temp_memory_required,
.max_frame_size = @intCast(info.max_frame_size),
};
}

View File

@ -0,0 +1,3 @@
#define STB_VORBIS_NO_INTEGER_CONVERSION
#define STB_VORBIS_NO_STDIO
#include "stb_vorbis.c"

29
libs/tiled/build.zig Normal file
View File

@ -0,0 +1,29 @@
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("tiled", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/root.zig")
});
const lib = b.addLibrary(.{
.name = "tiled",
.root_module = mod
});
b.installArtifact(lib);
{
const tests = b.addTest(.{
.root_module = mod
});
const run_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_tests.step);
}
}

View File

@ -0,0 +1,18 @@
const std = @import("std");
const Position = @import("./position.zig");
const Buffers = @This();
allocator: std.mem.Allocator,
points: std.ArrayList(Position) = .empty,
pub fn init(gpa: std.mem.Allocator) Buffers {
return Buffers{
.allocator = gpa
};
}
pub fn deinit(self: *Buffers) void {
self.points.deinit(self.allocator);
}

52
libs/tiled/src/color.zig Normal file
View File

@ -0,0 +1,52 @@
const std = @import("std");
const Color = @This();
r: u8,
g: u8,
b: u8,
a: u8,
pub const black = Color{
.r = 0,
.g = 0,
.b = 0,
.a = 255,
};
pub fn parse(str: []const u8, hash_required: bool) !Color {
var color = Color{
.r = undefined,
.g = undefined,
.b = undefined,
.a = 255,
};
if (str.len < 1) {
return error.InvalidColorFormat;
}
const has_hash = str[0] == '#';
if (hash_required and !has_hash) {
return error.InvalidColorFormat;
}
const hex_str = if (has_hash) str[1..] else str;
if (hex_str.len == 6) {
color.r = try std.fmt.parseInt(u8, hex_str[0..2], 16);
color.g = try std.fmt.parseInt(u8, hex_str[2..4], 16);
color.b = try std.fmt.parseInt(u8, hex_str[4..6], 16);
} else if (hex_str.len == 8) {
color.a = try std.fmt.parseInt(u8, hex_str[0..2], 16);
color.r = try std.fmt.parseInt(u8, hex_str[2..4], 16);
color.g = try std.fmt.parseInt(u8, hex_str[4..6], 16);
color.b = try std.fmt.parseInt(u8, hex_str[6..8], 16);
} else {
return error.InvalidColorFormat;
}
return color;
}

View File

@ -0,0 +1,15 @@
pub const Flag = enum(u32) {
flipped_horizontally = 1 << 31, // bit 32
flipped_vertically = 1 << 30, // bit 31
flipped_diagonally = 1 << 29, // bit 30
rotated_hexagonal_120 = 1 << 28, // bit 29
_,
pub const clear: u32 = ~(
@intFromEnum(Flag.flipped_horizontally) |
@intFromEnum(Flag.flipped_vertically) |
@intFromEnum(Flag.flipped_diagonally) |
@intFromEnum(Flag.rotated_hexagonal_120)
);
};

283
libs/tiled/src/layer.zig Normal file
View File

@ -0,0 +1,283 @@
const std = @import("std");
const Property = @import("./property.zig");
const xml = @import("./xml.zig");
const Color = @import("./color.zig");
const Object = @import("./object.zig");
const Position = @import("./position.zig");
const Layer = @This();
pub const TileVariant = struct {
width: u32,
height: u32,
data: []u32,
fn initDataFromXml(
arena: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer,
) ![]u32 {
var iter = xml.TagParser.init(lexer);
const attrs = try iter.begin("data");
const encoding = attrs.get("encoding") orelse "csv";
// TODO: compression
var temp_tiles: std.ArrayList(u32) = .empty;
if (std.mem.eql(u8, encoding, "csv")) {
const text = try lexer.nextExpectText();
var split_iter = std.mem.splitScalar(u8, text, ',');
while (split_iter.next()) |raw_tile_id| {
const tile_id_str = std.mem.trim(u8, raw_tile_id, &std.ascii.whitespace);
const tile_id = try std.fmt.parseInt(u32, tile_id_str, 10);
try temp_tiles.append(scratch.allocator(), tile_id);
}
} else {
return error.UnknownEncodingType;
}
try iter.finish("data");
return try arena.dupe(u32, temp_tiles.items);
}
pub fn get(self: TileVariant, x: usize, y: usize) ?u32 {
if ((0 <= x and x < self.width) and (0 <= y and y < self.height)) {
return self.data[y * self.width + x];
}
return null;
}
};
pub const ImageVariant = struct {
pub const Image = struct {
// TODO: format
source: []const u8,
transparent_color: ?Color,
width: ?u32,
height: ?u32,
fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image {
var iter = xml.TagParser.init(lexer);
const attrs = try iter.begin("image");
// TODO: format
const source = try attrs.getDupe(arena, "source") orelse return error.MissingSource;
const width = try attrs.getNumber(u32, "width") orelse null;
const height = try attrs.getNumber(u32, "height") orelse null;
const transparent_color = try attrs.getColor("trans", false);
try iter.finish("image");
return Image{
.source = source,
.transparent_color = transparent_color,
.width = width,
.height = height
};
}
};
repeat_x: bool,
repeat_y: bool,
image: ?Image
};
pub const ObjectVariant = struct {
pub const DrawOrder = enum {
top_down,
index,
const map: std.StaticStringMap(DrawOrder) = .initComptime(.{
.{ "topdown", .top_down },
.{ "index", .index },
});
};
color: ?Color,
draw_order: DrawOrder,
items: []Object
};
pub const GroupVariant = struct {
layers: []Layer
};
pub const Type = enum {
tile,
object,
image,
group,
const name_map: std.StaticStringMap(Type) = .initComptime(.{
.{ "layer", .tile },
.{ "objectgroup", .object },
.{ "imagelayer", .image },
.{ "group", .group }
});
fn toXmlName(self: Type) []const u8 {
return switch (self) {
.tile => "layer",
.object => "objectgroup",
.image => "imagelayer",
.group => "group",
};
}
};
pub const Variant = union(Type) {
tile: TileVariant,
object: ObjectVariant,
image: ImageVariant,
group: GroupVariant
};
id: u32,
name: []const u8,
class: []const u8,
opacity: f32,
visible: bool,
tint_color: ?Color,
offset_x: f32,
offset_y: f32,
parallax_x: f32,
parallax_y: f32,
properties: Property.List,
variant: Variant,
pub fn initFromXml(
arena: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer,
) !Layer {
const value = try lexer.peek() orelse return error.MissingStartTag;
if (value != .start_tag) return error.MissingStartTag;
var layer_type = Type.name_map.get(value.start_tag.name) orelse return error.UnknownLayerType;
var iter = xml.TagParser.init(lexer);
const attrs = try iter.begin(layer_type.toXmlName());
const id = try attrs.getNumber(u32, "id") orelse return error.MissingId;
const name = try attrs.getDupe(arena, "name") orelse "";
const class = try attrs.getDupe(arena, "class") orelse "";
const opacity = try attrs.getNumber(f32, "opacity") orelse 1;
const visible = try attrs.getBool("visible", "1", "0") orelse true;
const offset_x = try attrs.getNumber(f32, "offsetx") orelse 0;
const offset_y = try attrs.getNumber(f32, "offsety") orelse 0;
const parallax_x = try attrs.getNumber(f32, "parallaxx") orelse 1;
const parallax_y = try attrs.getNumber(f32, "parallaxy") orelse 1;
const tint_color = try attrs.getColor("tintcolor", true);
var variant: Variant = undefined;
switch (layer_type) {
.tile => {
const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth;
const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight;
variant = .{
.tile = TileVariant{
.width = width,
.height = height,
.data = &[0]u32{}
}
};
},
.image => {
const repeat_x = try attrs.getBool("repeatx", "1", "0") orelse false;
const repeat_y = try attrs.getBool("repeaty", "1", "0") orelse false;
variant = .{
.image = ImageVariant{
.repeat_x = repeat_x,
.repeat_y = repeat_y,
.image = null
}
};
},
.object => {
const draw_order = try attrs.getEnum(ObjectVariant.DrawOrder, "draworder", ObjectVariant.DrawOrder.map) orelse .top_down;
const color = try attrs.getColor("color", true);
variant = .{
.object = ObjectVariant{
.color = color,
.draw_order = draw_order,
.items = &.{}
}
};
},
.group => {
variant = .{
.group = .{
.layers = &.{}
}
};
},
}
var properties: Property.List = .empty;
var objects: std.ArrayList(Object) = .empty;
var layers: std.ArrayList(Layer) = .empty;
while (try iter.next()) |node| {
if (node.isTag("properties")) {
properties = try Property.List.initFromXml(arena, scratch, lexer);
continue;
}
if (variant == .tile and node.isTag("data")) {
variant.tile.data = try TileVariant.initDataFromXml(arena, scratch, lexer);
continue;
}
if (variant == .image and node.isTag("image")) {
variant.image.image = try ImageVariant.Image.initFromXml(arena, lexer);
continue;
}
if (variant == .object and node.isTag("object")) {
const object = try Object.initFromXml(arena, scratch, lexer);
try objects.append(scratch.allocator(), object);
continue;
}
if (variant == .group and isLayerNode(node)) {
const layer = try initFromXml(arena, scratch, lexer);
try layers.append(scratch.allocator(), layer);
continue;
}
try iter.skip();
}
try iter.finish(layer_type.toXmlName());
if (variant == .object) {
variant.object.items = try arena.dupe(Object, objects.items);
}
if (variant == .group) {
variant.group.layers = try arena.dupe(Layer, layers.items);
}
return Layer{
.id = id,
.name = name,
.class = class,
.opacity = opacity,
.visible = visible,
.tint_color = tint_color,
.offset_x = offset_x,
.offset_y = offset_y,
.parallax_x = parallax_x,
.parallax_y = parallax_y,
.properties = properties,
.variant = variant,
};
}
pub fn isLayerNode(node: xml.TagParser.Node) bool {
return node.isTag("layer") or node.isTag("objectgroup") or node.isTag("imagelayer") or node.isTag("group");
}

421
libs/tiled/src/object.zig Normal file
View File

@ -0,0 +1,421 @@
const std = @import("std");
const xml = @import("./xml.zig");
const Position = @import("./position.zig");
const Color = @import("./color.zig");
const Object = @This();
pub const Shape = union (Type) {
pub const Type = enum {
rectangle,
point,
ellipse,
polygon,
tile,
// TODO: template
text
};
pub const Rectangle = struct {
x: f32,
y: f32,
width: f32,
height: f32,
};
pub const Tile = struct {
x: f32,
y: f32,
width: f32,
height: f32,
gid: u32
};
pub const Ellipse = struct {
x: f32,
y: f32,
width: f32,
height: f32,
};
pub const Polygon = struct {
x: f32,
y: f32,
points: []const Position
};
pub const Text = struct {
pub const Font = struct {
family: []const u8,
pixel_size: f32,
bold: bool,
italic: bool,
underline: bool,
strikeout: bool,
kerning: bool
};
pub const HorizontalAlign = enum {
left,
center,
right,
justify,
const map: std.StaticStringMap(HorizontalAlign) = .initComptime(.{
.{ "left", .left },
.{ "center", .center },
.{ "right", .right },
.{ "justify", .justify },
});
};
pub const VerticalAlign = enum {
top,
center,
bottom,
const map: std.StaticStringMap(VerticalAlign) = .initComptime(.{
.{ "top", .top },
.{ "center", .center },
.{ "bottom", .bottom },
});
};
x: f32,
y: f32,
width: f32,
height: f32,
word_wrap: bool,
color: Color,
font: Font,
horizontal_align: HorizontalAlign,
vertical_align: VerticalAlign,
content: []const u8
};
pub const Point = struct {
x: f32,
y: f32,
};
rectangle: Rectangle,
point: Point,
ellipse: Ellipse,
polygon: Polygon,
tile: Tile,
text: Text
};
id: u32,
name: []const u8,
class: []const u8,
rotation: f32, // TODO: maybe this field should be moved to Shape struct
visible: bool,
shape: Shape,
pub fn initFromXml(
arena: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer,
) !Object {
var iter = xml.TagParser.init(lexer);
const attrs = try iter.begin("object");
const id = try attrs.getNumber(u32, "id") orelse return error.MissingId;
const name = try attrs.getDupe(arena, "name") orelse "";
const class = try attrs.getDupe(arena, "type") orelse "";
const x = try attrs.getNumber(f32, "x") orelse 0;
const y = try attrs.getNumber(f32, "y") orelse 0;
const width = try attrs.getNumber(f32, "width") orelse 0;
const height = try attrs.getNumber(f32, "height") orelse 0;
const rotation = try attrs.getNumber(f32, "rotation") orelse 0;
const visible = try attrs.getBool("visible", "1", "0") orelse true;
var shape: ?Shape = null;
while (try iter.next()) |node| {
if (shape == null) {
if (node.isTag("point")) {
shape = .{
.point = Shape.Point{
.x = x,
.y = y,
}
};
} else if (node.isTag("ellipse")) {
shape = .{
.ellipse = Shape.Ellipse{
.x = x,
.y = y,
.width = width,
.height = height
}
};
} else if (node.isTag("text")) {
const HorizontalShape = Shape.Text.HorizontalAlign;
const VerticalAlign = Shape.Text.VerticalAlign;
const text_attrs = node.tag.attributes;
const word_wrap = try text_attrs.getBool("wrap", "1", "0") orelse false;
const color = try text_attrs.getColor("color", true) orelse Color.black;
const horizontal_align = try text_attrs.getEnum(HorizontalShape, "halign", HorizontalShape.map) orelse .left;
const vertical_align = try text_attrs.getEnum(VerticalAlign, "valign", VerticalAlign.map) orelse .top;
const bold = try text_attrs.getBool("bold", "1", "0") orelse false;
const italic = try text_attrs.getBool("italic", "1", "0") orelse false;
const strikeout = try text_attrs.getBool("strikeout", "1", "0") orelse false;
const underline = try text_attrs.getBool("underline", "1", "0") orelse false;
const kerning = try text_attrs.getBool("kerning", "1", "0") orelse true;
const pixel_size = try text_attrs.getNumber(f32, "pixelsize") orelse 16;
const font_family = try text_attrs.getDupe(arena, "fontfamily") orelse "sans-serif";
_ = try lexer.nextExpectStartTag("text");
var content: []const u8 = "";
const content_value = try lexer.peek();
if (content_value != null and content_value.? == .text) {
content = try arena.dupe(u8, content_value.?.text);
}
try lexer.skipUntilMatchingEndTag("text");
shape = .{
.text = Shape.Text{
.x = x,
.y = y,
.width = width,
.height = height,
.word_wrap = word_wrap,
.color = color,
.horizontal_align = horizontal_align,
.vertical_align = vertical_align,
.content = try arena.dupe(u8, content),
.font = .{
.bold = bold,
.italic = italic,
.strikeout = strikeout,
.underline = underline,
.pixel_size = pixel_size,
.kerning = kerning,
.family = font_family,
}
}
};
continue;
} else if (node.isTag("polygon")) {
const points_str = node.tag.attributes.get("points") orelse "";
var points: std.ArrayList(Position) = .empty;
var point_iter = std.mem.splitScalar(u8, points_str, ' ');
while (point_iter.next()) |point_str| {
const point = try Position.parseCommaDelimited(point_str);
try points.append(scratch.allocator(), point);
}
shape = .{
.polygon = Shape.Polygon{
.x = x,
.y = y,
.points = try arena.dupe(Position, points.items)
}
};
}
}
try iter.skip();
}
if (shape == null) {
if (try attrs.getNumber(u32, "gid")) |gid| {
shape = .{
.tile = Shape.Tile{
.x = x,
.y = y,
.width = width,
.height = height,
.gid = gid
}
};
} else {
shape = .{
.rectangle = Shape.Rectangle{
.x = x,
.y = y,
.width = width,
.height = height
}
};
}
}
try iter.finish("object");
return Object{
.id = id,
.name = name,
.class = class,
.rotation = rotation,
.visible = visible,
.shape = shape orelse return error.UnknownShapeType
};
}
fn expectParsedEquals(expected: Object, body: []const u8) !void {
const allocator = std.testing.allocator;
var ctx: xml.Lexer.TestingContext = undefined;
ctx.init(allocator, body);
defer ctx.deinit();
var scratch = std.heap.ArenaAllocator.init(allocator);
defer scratch.deinit();
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const parsed = try initFromXml(arena.allocator(), &scratch, &ctx.lexer);
try std.testing.expectEqualDeep(expected, parsed);
}
test Object {
try expectParsedEquals(
Object{
.id = 10,
.name = "rectangle",
.class = "object class",
.rotation = 0,
.visible = true,
.shape = .{
.rectangle = .{
.x = 12.34,
.y = 56.78,
.width = 31.5,
.height = 20.25,
}
}
},
\\ <object id="10" name="rectangle" type="object class" x="12.34" y="56.78" width="31.5" height="20.25"/>
);
try expectParsedEquals(
Object{
.id = 3,
.name = "point",
.class = "foo",
.rotation = 0,
.visible = true,
.shape = .{
.point = .{
.x = 77.125,
.y = 99.875
}
}
},
\\ <object id="3" name="point" type="foo" x="77.125" y="99.875">
\\ <point/>
\\ </object>
);
try expectParsedEquals(
Object{
.id = 4,
.name = "ellipse",
.class = "",
.rotation = 0,
.visible = true,
.shape = .{
.ellipse = .{
.x = 64.25,
.y = 108.25,
.width = 22.375,
.height = 15.375
}
}
},
\\ <object id="4" name="ellipse" x="64.25" y="108.25" width="22.375" height="15.375">
\\ <ellipse/>
\\ </object>
);
try expectParsedEquals(
Object{
.id = 5,
.name = "",
.class = "",
.rotation = 0,
.visible = true,
.shape = .{
.polygon = .{
.x = 40.125,
.y = 96.25,
.points = &[_]Position{
.{ .x = 0, .y = 0 },
.{ .x = 13.25, .y = -4.25 },
.{ .x = 10.125, .y = 18.625 },
.{ .x = 2.25, .y = 17.375 },
.{ .x = -0.125, .y = 25.75 },
.{ .x = -3.875, .y = 20.75 },
}
}
}
},
\\ <object id="5" x="40.125" y="96.25">
\\ <polygon points="0,0 13.25,-4.25 10.125,18.625 2.25,17.375 -0.125,25.75 -3.875,20.75"/>
\\ </object>
);
try expectParsedEquals(
Object{
.id = 2,
.name = "tile",
.class = "",
.rotation = 0,
.visible = true,
.shape = .{
.tile = .{
.x = 60.125,
.y = 103.5,
.width = 8,
.height = 8,
.gid = 35
}
}
},
\\ <object id="2" name="tile" gid="35" x="60.125" y="103.5" width="8" height="8"/>
);
try expectParsedEquals(
Object{
.id = 6,
.name = "text",
.class = "",
.rotation = 0,
.visible = true,
.shape = .{
.text = .{
.x = 64.3906,
.y = 92.8594,
.width = 87.7188,
.height = 21.7813,
.content = "Hello World",
.word_wrap = true,
.color = .black,
.horizontal_align = .center,
.vertical_align = .top,
.font = .{
.family = "sans-serif",
.pixel_size = 16,
.bold = false,
.italic = false,
.underline = false,
.strikeout = false,
.kerning = true
},
}
}
},
\\ <object id="6" name="text" x="64.3906" y="92.8594" width="87.7188" height="21.7813">
\\ <text wrap="1" halign="center"> Hello World </text>
\\ </object>
);
}

View File

@ -0,0 +1,20 @@
const std = @import("std");
const Position = @This();
x: f32,
y: f32,
pub fn parseCommaDelimited(str: []const u8) !Position {
const comma_index = std.mem.indexOfScalar(u8, str, ',') orelse return error.MissingComma;
const x_str = str[0..comma_index];
const y_str = str[(comma_index+1)..];
const x = try std.fmt.parseFloat(f32, x_str);
const y = try std.fmt.parseFloat(f32, y_str);
return Position{
.x = x,
.y = y
};
}

153
libs/tiled/src/property.zig Normal file
View File

@ -0,0 +1,153 @@
const std = @import("std");
const xml = @import("./xml.zig");
const Property = @This();
pub const Type = enum {
string,
int,
bool,
const map: std.StaticStringMap(Type) = .initComptime(.{
.{ "string", .string },
.{ "int", .int },
.{ "bool", .bool },
});
};
pub const Value = union(Type) {
string: []const u8,
int: i32,
bool: bool
};
name: []const u8,
value: Value,
pub const List = struct {
items: []Property,
pub const empty = List{
.items = &[0]Property{}
};
pub fn initFromXml(
arena: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer
) !Property.List {
var iter = xml.TagParser.init(lexer);
_ = try iter.begin("properties");
var temp_properties: std.ArrayList(Property) = .empty;
while (try iter.next()) |node| {
if (node.isTag("property")) {
const property = try Property.initFromXml(arena, lexer);
try temp_properties.append(scratch.allocator(), property);
continue;
}
try iter.skip();
}
try iter.finish("properties");
const properties = try arena.dupe(Property, temp_properties.items);
return List{
.items = properties
};
}
pub fn get(self: List, name: []const u8) ?Value {
for (self.items) |item| {
if (std.mem.eql(u8, item.name, name)) {
return item.value;
}
}
return null;
}
pub fn getString(self: List, name: []const u8) ?[]const u8 {
if (self.get(name)) |value| {
return value.string;
}
return null;
}
};
pub fn init(name: []const u8, value: Value) Property {
return Property{
.name = name,
.value = value
};
}
pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Property {
var iter = xml.TagParser.init(lexer);
const attrs = try iter.begin("property");
const name = try attrs.getDupe(arena, "name") orelse return error.MissingName;
const prop_type_str = attrs.get("type") orelse "string";
const value_str = attrs.get("value") orelse "";
const prop_type = Type.map.get(prop_type_str) orelse return error.UnknownPropertyType;
const value = switch(prop_type) {
.string => Value{
.string = try arena.dupe(u8, value_str)
},
.int => Value{
.int = try std.fmt.parseInt(i32, value_str, 10)
},
.bool => Value{
.bool = std.mem.eql(u8, value_str, "true")
}
};
try iter.finish("property");
return Property{
.name = name,
.value = value
};
}
fn expectParsedEquals(expected: Property, body: []const u8) !void {
const allocator = std.testing.allocator;
var ctx: xml.Lexer.TestingContext = undefined;
ctx.init(allocator, body);
defer ctx.deinit();
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const parsed = try initFromXml(arena.allocator(), &ctx.lexer);
try std.testing.expectEqualDeep(expected, parsed);
}
test Property {
try expectParsedEquals(
Property.init("solid", .{ .string = "hello" }),
\\ <property name="solid" value="hello"/>
);
try expectParsedEquals(
Property.init("solid", .{ .string = "hello" }),
\\ <property name="solid" type="string" value="hello"/>
);
try expectParsedEquals(
Property.init("integer", .{ .int = 123 }),
\\ <property name="integer" type="int" value="123"/>
);
try expectParsedEquals(
Property.init("boolean", .{ .bool = true }),
\\ <property name="boolean" type="bool" value="true"/>
);
try expectParsedEquals(
Property.init("boolean", .{ .bool = false }),
\\ <property name="boolean" type="bool" value="false"/>
);
}

16
libs/tiled/src/root.zig Normal file
View File

@ -0,0 +1,16 @@
const std = @import("std");
// Warning:
// This library is not complete, it does not cover all features that the specification provides.
// But there are enough features implemented so that I could use this for my games.
// Map format specification:
// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#
pub const xml = @import("./xml.zig");
pub const Tileset = @import("./tileset.zig");
pub const Tilemap = @import("./tilemap.zig");
test {
_ = std.testing.refAllDeclsRecursive(@This());
}

235
libs/tiled/src/tilemap.zig Normal file
View File

@ -0,0 +1,235 @@
const std = @import("std");
const xml = @import("./xml.zig");
const Io = std.Io;
const assert = std.debug.assert;
const Property = @import("./property.zig");
const Layer = @import("./layer.zig");
const Object = @import("./object.zig");
const Position = @import("./position.zig");
const Tileset = @import("./tileset.zig");
const GlobalTileId = @import("./global_tile_id.zig");
const Tilemap = @This();
pub const Orientation = enum {
orthogonal,
staggered,
hexagonal,
isometric,
const map: std.StaticStringMap(Orientation) = .initComptime(.{
.{ "orthogonal", .orthogonal },
.{ "staggered", .staggered },
.{ "hexagonal", .hexagonal },
.{ "isometric", .isometric }
});
};
pub const RenderOrder = enum {
right_down,
right_up,
left_down,
left_up,
const map: std.StaticStringMap(RenderOrder) = .initComptime(.{
.{ "right-down", .right_down },
.{ "right-up", .right_up },
.{ "left-down", .left_down },
.{ "left-up", .left_up },
});
};
pub const StaggerAxis = enum {
x,
y,
const map: std.StaticStringMap(StaggerAxis) = .initComptime(.{
.{ "x", .x },
.{ "y", .y }
});
};
pub const TilesetReference = struct {
source: []const u8,
first_gid: u32,
pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !TilesetReference {
var iter = xml.TagParser.init(lexer);
const attrs = try iter.begin("tileset");
const source = try attrs.getDupe(arena, "source") orelse return error.MissingFirstGid;
const first_gid = try attrs.getNumber(u32, "firstgid") orelse return error.MissingFirstGid;
try iter.finish("tileset");
return TilesetReference{
.source = source,
.first_gid = first_gid
};
}
};
pub const Tile = struct {
tileset: *const Tileset,
id: u32,
pub fn getProperties(self: Tile) Property.List {
return self.tileset.getTileProperties(self.id) orelse .empty;
}
};
arena: std.heap.ArenaAllocator,
version: []const u8,
tiled_version: ?[]const u8,
orientation: Orientation,
render_order: RenderOrder,
width: u32,
height: u32,
tile_width: u32,
tile_height: u32,
infinite: bool,
stagger_axis: ?StaggerAxis,
next_layer_id: u32,
next_object_id: u32,
tilesets: []TilesetReference,
layers: []Layer,
pub fn initFromBuffer(
gpa: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
xml_buffers: *xml.Lexer.Buffers,
buffer: []const u8
) !Tilemap {
var reader = Io.Reader.fixed(buffer);
return initFromReader(gpa, scratch, xml_buffers, &reader);
}
pub fn initFromReader(
gpa: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
xml_buffers: *xml.Lexer.Buffers,
reader: *Io.Reader,
) !Tilemap {
var lexer = xml.Lexer.init(reader, xml_buffers);
return initFromXml(gpa, scratch, &lexer);
}
// Map specification:
// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#map
pub fn initFromXml(
gpa: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer,
) !Tilemap {
var arena_allocator = std.heap.ArenaAllocator.init(gpa);
errdefer arena_allocator.deinit();
const arena = arena_allocator.allocator();
var iter = xml.TagParser.init(lexer);
const map_attrs = try iter.begin("map");
const version = try map_attrs.getDupe(arena, "version") orelse return error.MissingVersion;
const tiled_version = try map_attrs.getDupe(arena, "tiledversion");
const orientation = try map_attrs.getEnum(Orientation, "orientation", Orientation.map) orelse return error.MissingOrientation;
const render_order = try map_attrs.getEnum(RenderOrder, "renderorder", RenderOrder.map) orelse return error.MissingRenderOrder;
// TODO: compressionlevel
const width = try map_attrs.getNumber(u32, "width") orelse return error.MissingWidth;
const height = try map_attrs.getNumber(u32, "height") orelse return error.MissingHeight;
const tile_width = try map_attrs.getNumber(u32, "tilewidth") orelse return error.MissingTileWidth;
const tile_height = try map_attrs.getNumber(u32, "tileheight") orelse return error.MissingTileHeight;
// TODO: hexidelength
const infinite_int = try map_attrs.getNumber(u32, "infinite") orelse 0;
const infinite = infinite_int != 0;
const next_layer_id = try map_attrs.getNumber(u32, "nextlayerid") orelse return error.MissingLayerId;
const next_object_id = try map_attrs.getNumber(u32, "nextobjectid") orelse return error.MissingObjectId;
// TODO: parallaxoriginx
// TODO: parallaxoriginy
// TODO: backgroundcolor
var stagger_axis: ?StaggerAxis = null;
if (orientation == .hexagonal or orientation == .staggered) {
stagger_axis = try map_attrs.getEnum(StaggerAxis, "staggeraxis", StaggerAxis.map) orelse return error.MissingRenderOrder;
// TODO: staggerindex
}
var tileset_list: std.ArrayList(TilesetReference) = .empty;
var layer_list: std.ArrayList(Layer) = .empty;
while (try iter.next()) |node| {
if (node.isTag("tileset")) {
try tileset_list.append(scratch.allocator(), try TilesetReference.initFromXml(arena, lexer));
continue;
} else if (Layer.isLayerNode(node)) {
const layer = try Layer.initFromXml(
arena,
scratch,
lexer,
);
try layer_list.append(scratch.allocator(), layer);
continue;
}
try iter.skip();
}
try iter.finish("map");
const tilesets = try arena.dupe(TilesetReference, tileset_list.items);
const layers = try arena.dupe(Layer, layer_list.items);
return Tilemap{
.arena = arena_allocator,
.version = version,
.tiled_version = tiled_version,
.orientation = orientation,
.render_order = render_order,
.width = width,
.height = height,
.tile_width = tile_width,
.tile_height = tile_height,
.infinite = infinite,
.stagger_axis = stagger_axis,
.next_object_id = next_object_id,
.next_layer_id = next_layer_id,
.tilesets = tilesets,
.layers = layers,
};
}
fn getTilesetByGid(self: *const Tilemap, gid: u32) ?TilesetReference {
var result: ?TilesetReference = null;
for (self.tilesets) |tileset| {
if (gid < tileset.first_gid) {
continue;
}
if (result != null and result.?.first_gid < tileset.first_gid) {
continue;
}
result = tileset;
}
return result;
}
pub fn getTile(self: *const Tilemap, layer: *const Layer, tilesets: Tileset.List, x: usize, y: usize) ?Tile {
assert(layer.variant == .tile);
const tile_variant = layer.variant.tile;
const gid = tile_variant.get(x, y) orelse return null;
const tileset_ref = self.getTilesetByGid(gid & GlobalTileId.Flag.clear) orelse return null;
const tileset = tilesets.get(tileset_ref.source) orelse return null;
const id = gid - tileset_ref.first_gid;
return Tile{
.tileset = tileset,
.id = id
};
}
pub fn deinit(self: *const Tilemap) void {
self.arena.deinit();
}

226
libs/tiled/src/tileset.zig Normal file
View File

@ -0,0 +1,226 @@
const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const xml = @import("./xml.zig");
const Property = @import("./property.zig");
const Position = @import("./position.zig");
const Tileset = @This();
pub const Image = struct {
source: []const u8,
width: u32,
height: u32,
pub fn intFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image {
var iter = xml.TagParser.init(lexer);
const attrs = try iter.begin("image");
const source = try attrs.getDupe(arena, "width") orelse return error.MissingSource;
const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth;
const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight;
try iter.finish("image");
return Image{
.source = source,
.width = width,
.height = height
};
}
};
pub const Tile = struct {
id: u32,
properties: Property.List,
pub fn initFromXml(
arena: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer,
) !Tile {
var iter = xml.TagParser.init(lexer);
const tile_attrs = try iter.begin("tile");
const id = try tile_attrs.getNumber(u32, "id") orelse return error.MissingId;
var properties: Property.List = .empty;
while (try iter.next()) |node| {
if (node.isTag("properties")) {
properties = try Property.List.initFromXml(arena, scratch, lexer);
continue;
}
try iter.skip();
}
try iter.finish("tile");
return Tile{
.id = id,
.properties = properties
};
}
};
pub const List = struct {
const Entry = struct {
name: []const u8,
tileset: Tileset
};
list: std.ArrayList(Entry),
pub const empty = List{
.list = .empty
};
pub fn add(self: *List, gpa: Allocator, name: []const u8, tileset: Tileset) !void {
if (self.get(name) != null) {
return error.DuplicateName;
}
const name_dupe = try gpa.dupe(u8, name);
errdefer gpa.free(name_dupe);
try self.list.append(gpa, .{
.name = name_dupe,
.tileset = tileset
});
}
pub fn get(self: *const List, name: []const u8) ?*const Tileset {
for (self.list.items) |*entry| {
if (std.mem.eql(u8, entry.name, name)) {
return &entry.tileset;
}
}
return null;
}
pub fn deinit(self: *List, gpa: Allocator) void {
for (self.list.items) |entry| {
gpa.free(entry.name);
entry.tileset.deinit();
}
self.list.deinit(gpa);
}
};
arena: std.heap.ArenaAllocator,
version: []const u8,
tiled_version: []const u8,
name: []const u8,
tile_width: u32,
tile_height: u32,
tile_count: u32,
columns: u32,
image: Image,
tiles: []Tile,
pub fn initFromBuffer(
gpa: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
xml_buffers: *xml.Lexer.Buffers,
buffer: []const u8
) !Tileset {
var reader = Io.Reader.fixed(buffer);
return initFromReader(gpa, scratch, xml_buffers, &reader);
}
pub fn initFromReader(
gpa: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
xml_buffers: *xml.Lexer.Buffers,
reader: *Io.Reader,
) !Tileset {
var lexer = xml.Lexer.init(reader, xml_buffers);
return initFromXml(gpa, scratch, &lexer);
}
pub fn initFromXml(
gpa: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer
) !Tileset {
var arena_state = std.heap.ArenaAllocator.init(gpa);
const arena = arena_state.allocator();
var iter = xml.TagParser.init(lexer);
const tileset_attrs = try iter.begin("tileset");
const version = try tileset_attrs.getDupe(arena, "version") orelse return error.MissingTilesetTag;
const tiled_version = try tileset_attrs.getDupe(arena, "tiledversion") orelse return error.MissingTilesetTag;
const name = try tileset_attrs.getDupe(arena, "name") orelse return error.MissingTilesetTag;
const tile_width = try tileset_attrs.getNumber(u32, "tilewidth") orelse return error.MissingTilesetTag;
const tile_height = try tileset_attrs.getNumber(u32, "tileheight") orelse return error.MissingTilesetTag;
const tile_count = try tileset_attrs.getNumber(u32, "tilecount") orelse return error.MissingTilesetTag;
const columns = try tileset_attrs.getNumber(u32, "columns") orelse return error.MissingTilesetTag;
var image: ?Image = null;
var tiles_list: std.ArrayList(Tile) = .empty;
while (try iter.next()) |node| {
if (node.isTag("image")) {
image = try Image.intFromXml(arena, lexer);
continue;
} else if (node.isTag("tile")) {
const tile = try Tile.initFromXml(arena, scratch, lexer);
try tiles_list.append(scratch.allocator(), tile);
continue;
}
try iter.skip();
}
try iter.finish("tileset");
const tiles = try arena.dupe(Tile, tiles_list.items);
return Tileset{
.arena = arena_state,
.version = version,
.tiled_version = tiled_version,
.name = name,
.tile_width = tile_width,
.tile_height = tile_height,
.tile_count = tile_count,
.columns = columns,
.image = image orelse return error.MissingImageTag,
.tiles = tiles
};
}
pub fn getTileProperties(self: *const Tileset, id: u32) ?Property.List {
for (self.tiles) |tile| {
if (tile.id == id) {
return tile.properties;
}
}
return null;
}
pub fn getTilePositionInImage(self: *const Tileset, id: u32) ?Position {
if (id >= self.tile_count) {
return null;
}
const tileset_width = @divExact(self.image.width, self.tile_width);
const tile_x = @mod(id, tileset_width);
const tile_y = @divFloor(id, tileset_width);
return Position{
.x = @floatFromInt(tile_x * self.tile_width),
.y = @floatFromInt(tile_y * self.tile_height),
};
}
pub fn deinit(self: *const Tileset) void {
self.arena.deinit();
}

687
libs/tiled/src/xml.zig Normal file
View File

@ -0,0 +1,687 @@
const std = @import("std");
const Io = std.Io;
const assert = std.debug.assert;
const Color = @import("./color.zig");
pub const Attribute = struct {
name: []const u8,
value: []const u8,
pub const List = struct {
items: []const Attribute,
pub fn get(self: List, name: []const u8) ?[]const u8 {
for (self.items) |attr| {
if (std.mem.eql(u8, attr.name, name)) {
return attr.value;
}
}
return null;
}
pub fn getDupe(self: List, gpa: std.mem.Allocator, name: []const u8) !?[]u8 {
if (self.get(name)) |value| {
return try gpa.dupe(u8, value);
}
return null;
}
pub fn getNumber(self: List, T: type, name: []const u8) !?T {
if (self.get(name)) |value| {
if (@typeInfo(T) == .int) {
return try std.fmt.parseInt(T, value, 10);
} else if (@typeInfo(T) == .float) {
return try std.fmt.parseFloat(T, value);
}
}
return null;
}
pub fn getBool(self: List, name: []const u8, true_value: []const u8, false_value: []const u8) !?bool {
if (self.get(name)) |value| {
if (std.mem.eql(u8, value, true_value)) {
return true;
} else if (std.mem.eql(u8, value, false_value)) {
return false;
} else {
return error.InvalidBoolean;
}
}
return null;
}
pub fn getEnum(self: List, T: type, name: []const u8, map: std.StaticStringMap(T)) !?T {
if (self.get(name)) |value| {
return map.get(value) orelse return error.InvalidEnumValue;
}
return null;
}
pub fn getColor(self: List, name: []const u8, hash_required: bool) !?Color {
if (self.get(name)) |value| {
return try Color.parse(value, hash_required);
}
return null;
}
pub fn format(self: List, writer: *Io.Writer) Io.Writer.Error!void {
if (self.items.len > 0) {
try writer.writeAll("{ ");
for (self.items, 0..) |attribute, i| {
if (i > 0) {
try writer.writeAll(", ");
}
try writer.print("{f}", .{attribute});
}
try writer.writeAll(" }");
} else {
try writer.writeAll("{ }");
}
}
};
pub fn format(self: *const Attribute, writer: *Io.Writer) Io.Writer.Error!void {
try writer.print("{s}{{ .name='{s}', .value='{s}' }}", .{ @typeName(Attribute), self.name, self.value });
}
pub fn formatSlice(data: []const Attribute, writer: *Io.Writer) Io.Writer.Error!void {
if (data.len > 0) {
try writer.writeAll("{ ");
for (data, 0..) |attribute, i| {
if (i > 0) {
try writer.writeAll(", ");
}
try writer.print("{f}", .{attribute});
}
try writer.writeAll(" }");
} else {
try writer.writeAll("{ }");
}
}
fn altSlice(data: []const Attribute) std.fmt.Alt(Attribute.List, Attribute.List.format) {
return .{ .data = data };
}
};
pub const Tag = struct {
name: []const u8,
attributes: Attribute.List
};
pub const Lexer = struct {
pub const Buffers = struct {
scratch: std.heap.ArenaAllocator,
text: std.ArrayList(u8),
pub fn init(allocator: std.mem.Allocator) Buffers {
return Buffers{
.scratch = std.heap.ArenaAllocator.init(allocator),
.text = .empty
};
}
pub fn clear(self: *Buffers) void {
self.text.clearRetainingCapacity();
_ = self.scratch.reset(.retain_capacity);
}
pub fn deinit(self: *Buffers) void {
const allocator = self.scratch.child_allocator;
self.scratch.deinit();
self.text.deinit(allocator);
}
};
pub const Token = union(enum) {
start_tag: Tag,
end_tag: []const u8,
text: []const u8,
pub fn isStartTag(self: Token, name: []const u8) bool {
if (self == .start_tag) {
return std.mem.eql(u8, self.start_tag.name, name);
}
return false;
}
pub fn isEndTag(self: Token, name: []const u8) bool {
if (self == .end_tag) {
return std.mem.eql(u8, self.end_tag, name);
}
return false;
}
};
pub const StepResult = struct {
token: ?Token,
self_closing: bool = false,
};
pub const TestingContext = struct {
io_reader: Io.Reader,
buffers: Buffers,
lexer: Lexer,
pub fn init(self: *TestingContext, allocator: std.mem.Allocator, body: []const u8) void {
self.* = TestingContext{
.lexer = undefined,
.io_reader = Io.Reader.fixed(body),
.buffers = Buffers.init(allocator)
};
self.lexer = Lexer.init(&self.io_reader, &self.buffers);
}
pub fn deinit(self: *TestingContext) void {
self.buffers.deinit();
}
};
io_reader: *Io.Reader,
buffers: *Buffers,
peeked_value: ?Token,
cursor: usize,
queued_end_tag: ?[]const u8,
pub fn init(reader: *Io.Reader, buffers: *Buffers) Lexer {
buffers.clear();
return Lexer{
.io_reader = reader,
.buffers = buffers,
.cursor = 0,
.queued_end_tag = null,
.peeked_value = null
};
}
fn step(self: *Lexer) !StepResult {
_ = self.buffers.scratch.reset(.retain_capacity);
if (try self.peekByte() == '<') {
self.tossByte();
if (try self.peekByte() == '/') {
// End tag
self.tossByte();
const name = try self.parseName();
try self.skipWhiteSpace();
if (!std.mem.eql(u8, try self.takeBytes(1), ">")) {
return error.InvalidEndTag;
}
const token = Token{ .end_tag = name };
return .{ .token = token };
} else if (try self.peekByte() == '?') {
// Prolog tag
self.tossByte();
if (!std.mem.eql(u8, try self.takeBytes(4), "xml ")) {
return error.InvalidPrologTag;
}
const attributes = try self.parseAttributes();
try self.skipWhiteSpace();
if (!std.mem.eql(u8, try self.takeBytes(2), "?>")) {
return error.MissingPrologEnd;
}
const version = attributes.get("version") orelse return error.InvalidProlog;
if (!std.mem.eql(u8, version, "1.0")) {
return error.InvalidPrologVersion;
}
const encoding = attributes.get("encoding") orelse return error.InvalidProlog;
if (!std.mem.eql(u8, encoding, "UTF-8")) {
return error.InvalidPrologEncoding;
}
return .{ .token = null };
} else {
// Start tag
const name = try self.parseName();
const attributes = try self.parseAttributes();
try self.skipWhiteSpace();
const token = Token{
.start_tag = .{
.name = name,
.attributes = attributes
}
};
var self_closing = false;
if (std.mem.eql(u8, try self.peekBytes(1), ">")) {
self.tossBytes(1);
} else if (std.mem.eql(u8, try self.peekBytes(2), "/>")) {
self.tossBytes(2);
self_closing = true;
} else {
return error.UnfinishedStartTag;
}
return .{
.token = token,
.self_closing = self_closing
};
}
} else {
try self.skipWhiteSpace();
const text_start = self.cursor;
while (try self.peekByte() != '<') {
self.tossByte();
}
var text: []const u8 = self.buffers.text.items[text_start..self.cursor];
text = std.mem.trimEnd(u8, text, &std.ascii.whitespace);
var token: ?Token = null;
if (text.len > 0) {
token = Token{ .text = text };
}
return .{ .token = token };
}
}
pub fn next(self: *Lexer) !?Token {
if (self.peeked_value) |value| {
self.peeked_value = null;
return value;
}
if (self.queued_end_tag) |name| {
self.queued_end_tag = null;
return Token{
.end_tag = name
};
}
while (true) {
if (self.buffers.text.items.len == 0) {
self.readIntoTextBuffer() catch |e| switch (e) {
error.EndOfStream => break,
else => return e
};
}
const saved_cursor = self.cursor;
const result = self.step() catch |e| switch(e) {
error.EndOfTextBuffer => {
self.cursor = saved_cursor;
const unused_capacity = self.buffers.text.capacity - self.buffers.text.items.len;
if (unused_capacity == 0 and self.cursor > 0) {
self.rebaseBuffer();
} else {
self.readIntoTextBuffer() catch |read_err| switch (read_err) {
error.EndOfStream => break,
else => return read_err
};
}
continue;
},
else => return e
};
if (result.token) |token| {
if (token == .start_tag and result.self_closing) {
self.queued_end_tag = token.start_tag.name;
}
return token;
}
}
return null;
}
pub fn nextExpectEndTag(self: *Lexer, name: []const u8) !void {
const value = try self.next() orelse return error.MissingEndTag;
if (!value.isEndTag(name)) return error.MissingEndTag;
}
pub fn nextExpectStartTag(self: *Lexer, name: []const u8) !Attribute.List {
const value = try self.next() orelse return error.MissingStartTag;
if (!value.isStartTag(name)) return error.MissingStartTag;
return value.start_tag.attributes;
}
pub fn nextExpectText(self: *Lexer) ![]const u8 {
const value = try self.next() orelse return error.MissingTextTag;
if (value != .text) return error.MissingTextTag;
return value.text;
}
pub fn skipUntilMatchingEndTag(self: *Lexer, name: ?[]const u8) !void {
var depth: usize = 0;
while (true) {
const value = try self.next() orelse return error.MissingEndTag;
if (depth == 0 and value == .end_tag) {
if (name != null and !std.mem.eql(u8, value.end_tag, name.?)) {
return error.MismatchedEndTag;
}
break;
}
if (value == .start_tag) {
depth += 1;
} else if (value == .end_tag) {
depth -= 1;
}
}
}
pub fn peek(self: *Lexer) !?Token {
if (try self.next()) |value| {
self.peeked_value = value;
return value;
}
return null;
}
fn readIntoTextBuffer(self: *Lexer) !void {
const gpa = self.buffers.scratch.child_allocator;
const text = &self.buffers.text;
try text.ensureUnusedCapacity(gpa, 1);
var writer = Io.Writer.fixed(text.allocatedSlice());
writer.end = text.items.len;
_ = self.io_reader.stream(&writer, .limited(text.capacity - text.items.len)) catch |e| switch (e) {
error.WriteFailed => unreachable,
else => |ee| return ee
};
text.items.len = writer.end;
}
fn rebaseBuffer(self: *Lexer) void {
if (self.cursor == 0) {
return;
}
const text = &self.buffers.text;
@memmove(
text.items[0..(text.items.len - self.cursor)],
text.items[self.cursor..]
);
text.items.len -= self.cursor;
self.cursor = 0;
}
fn isNameStartChar(c: u8) bool {
return c == ':' or c == '_' or std.ascii.isAlphabetic(c);
}
fn isNameChar(c: u8) bool {
return isNameStartChar(c) or c == '-' or c == '.' or ('0' <= c and c <= '9');
}
fn hasBytes(self: *Lexer, n: usize) bool {
const text = self.buffers.text.items;
return self.cursor + n <= text.len;
}
fn peekBytes(self: *Lexer, n: usize) ![]const u8 {
if (self.hasBytes(n)) {
const text = self.buffers.text.items;
return text[self.cursor..][0..n];
}
return error.EndOfTextBuffer;
}
fn tossBytes(self: *Lexer, n: usize) void {
assert(self.hasBytes(n));
self.cursor += n;
}
fn takeBytes(self: *Lexer, n: usize) ![]const u8 {
const result = try self.peekBytes(n);
self.tossBytes(n);
return result;
}
fn peekByte(self: *Lexer) !u8 {
return (try self.peekBytes(1))[0];
}
fn tossByte(self: *Lexer) void {
self.tossBytes(1);
}
fn takeByte(self: *Lexer) !u8 {
return (try self.takeBytes(1))[0];
}
fn parseName(self: *Lexer) ![]const u8 {
const name_start = self.cursor;
if (isNameStartChar(try self.peekByte())) {
self.tossByte();
while (isNameChar(try self.peekByte())) {
self.tossByte();
}
}
return self.buffers.text.items[name_start..self.cursor];
}
fn skipWhiteSpace(self: *Lexer) !void {
while (std.ascii.isWhitespace(try self.peekByte())) {
self.tossByte();
}
}
fn parseAttributeValue(self: *Lexer) ![]const u8 {
const quote = try self.takeByte();
if (quote != '"' and quote != '\'') {
return error.InvalidAttributeValue;
}
const value_start: usize = self.cursor;
var value_len: usize = 0;
while (true) {
const c = try self.takeByte();
if (c == '<' or c == '&') {
return error.InvalidAttributeValue;
}
if (c == quote) {
break;
}
value_len += 1;
}
return self.buffers.text.items[value_start..][0..value_len];
}
fn parseAttributes(self: *Lexer) !Attribute.List {
const arena = self.buffers.scratch.allocator();
var attributes: std.ArrayList(Attribute) = .empty;
while (true) {
try self.skipWhiteSpace();
const name = try self.parseName();
if (name.len == 0) {
break;
}
try self.skipWhiteSpace();
if (try self.takeByte() != '=') {
return error.MissingAttributeEquals;
}
try self.skipWhiteSpace();
const value = try self.parseAttributeValue();
const list = Attribute.List{ .items = attributes.items };
if (list.get(name) != null) {
return error.DuplicateAttribute;
}
try attributes.append(arena, Attribute{
.name = name,
.value = value
});
}
return Attribute.List{
.items = attributes.items
};
}
test "self closing tag" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello />
);
defer ctx.deinit();
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try std.testing.expect((try ctx.lexer.next()) == null);
}
test "tag" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello></hello>
);
defer ctx.deinit();
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try std.testing.expect((try ctx.lexer.next()) == null);
}
test "tag with prolog" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <?xml version="1.0" encoding="UTF-8"?>
\\ <hello></hello>
);
defer ctx.deinit();
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try std.testing.expect((try ctx.lexer.next()) == null);
}
test "text content" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello> Hello World </hello>
);
defer ctx.deinit();
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try std.testing.expectEqualStrings("Hello World", (try ctx.lexer.next()).?.text);
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try std.testing.expect((try ctx.lexer.next()) == null);
}
test "attributes" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello a='1' b='2'/>
);
defer ctx.deinit();
const token = try ctx.lexer.next();
const attrs = token.?.start_tag.attributes;
try std.testing.expectEqualStrings("1", attrs.get("a").?);
try std.testing.expectEqualStrings("2", attrs.get("b").?);
}
};
// TODO: The API for this is easy to misuse.
// Design a better API for using Reader
// As a compromise `assert` was used to guard against some of the ways this can be misused
pub const TagParser = struct {
lexer: *Lexer,
begin_called: bool = false,
finish_called: bool = false,
pub const Node = union(enum) {
tag: Tag,
text: []const u8,
pub fn isTag(self: Node, name: []const u8) bool {
if (self == .tag) {
return std.mem.eql(u8, self.tag.name, name);
}
return false;
}
};
pub fn init(lexer: *Lexer) TagParser {
return TagParser{
.lexer = lexer,
};
}
pub fn begin(self: *TagParser, name: []const u8) !Attribute.List {
assert(!self.begin_called);
self.begin_called = true;
return try self.lexer.nextExpectStartTag(name);
}
pub fn finish(self: *TagParser, name: []const u8) !void {
assert(self.begin_called);
assert(!self.finish_called);
self.finish_called = true;
try self.lexer.skipUntilMatchingEndTag(name);
}
pub fn next(self: *TagParser) !?Node {
assert(self.begin_called);
assert(!self.finish_called);
const value = try self.lexer.peek() orelse return error.MissingEndTag;
if (value == .end_tag) {
return null;
}
return switch (value) {
.text => |text| Node{ .text = text },
.start_tag => |start_tag| Node{ .tag = start_tag },
.end_tag => unreachable,
};
}
pub fn skip(self: *TagParser) !void {
assert(self.begin_called);
assert(!self.finish_called);
const value = try self.lexer.next() orelse return error.MissingNode;
if (value == .end_tag) {
return error.UnexpectedEndTag;
} else if (value == .start_tag) {
// TODO: Make this configurable
var name_buffer: [64]u8 = undefined;
var name: std.ArrayList(u8) = .initBuffer(&name_buffer);
try name.appendSliceBounded(value.start_tag.name);
try self.lexer.skipUntilMatchingEndTag(name.items);
}
}
};

138
libs/tracy/build.zig Normal file
View File

@ -0,0 +1,138 @@
const std = @import("std");
const digits2 = std.fmt.digits2;
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const tracy_enable = b.option(bool, "tracy_enable", "Enable profiling") orelse true;
const tracy_on_demand = b.option(bool, "tracy_on_demand", "On-demand profiling") orelse false;
const tracy_callstack: ?u8 = b.option(u8, "tracy_callstack", "Enforce callstack collection for tracy regions");
const tracy_no_callstack = b.option(bool, "tracy_no_callstack", "Disable all callstack related functionality") orelse false;
const tracy_no_callstack_inlines = b.option(bool, "tracy_no_callstack_inlines", "Disables the inline functions in callstacks") orelse false;
const tracy_only_localhost = b.option(bool, "tracy_only_localhost", "Only listen on the localhost interface") orelse false;
const tracy_no_broadcast = b.option(bool, "tracy_no_broadcast", "Disable client discovery by broadcast to local network") orelse false;
const tracy_only_ipv4 = b.option(bool, "tracy_only_ipv4", "Tracy will only accept connections on IPv4 addresses (disable IPv6)") orelse false;
const tracy_no_code_transfer = b.option(bool, "tracy_no_code_transfer", "Disable collection of source code") orelse false;
const tracy_no_context_switch = b.option(bool, "tracy_no_context_switch", "Disable capture of context switches") orelse false;
const tracy_no_exit = b.option(bool, "tracy_no_exit", "Client executable does not exit until all profile data is sent to server") orelse false;
const tracy_no_sampling = b.option(bool, "tracy_no_sampling", "Disable call stack sampling") orelse false;
const tracy_no_verify = b.option(bool, "tracy_no_verify", "Disable zone validation for C API") orelse false;
const tracy_no_vsync_capture = b.option(bool, "tracy_no_vsync_capture", "Disable capture of hardware Vsync events") orelse false;
const tracy_no_frame_image = b.option(bool, "tracy_no_frame_image", "Disable the frame image support and its thread") orelse false;
// NOTE For some reason system tracing on zig projects crashes tracy, will need to investigate
const tracy_no_system_tracing = b.option(bool, "tracy_no_system_tracing", "Disable systrace sampling") orelse true;
const tracy_delayed_init = b.option(bool, "tracy_delayed_init", "Enable delayed initialization of the library (init on first call)") orelse false;
const tracy_manual_lifetime = b.option(bool, "tracy_manual_lifetime", "Enable the manual lifetime management of the profile") orelse false;
const tracy_fibers = b.option(bool, "tracy_fibers", "Enable fibers support") orelse false;
const tracy_no_crash_handler = b.option(bool, "tracy_no_crash_handler", "Disable crash handling") orelse false;
const tracy_timer_fallback = b.option(bool, "tracy_timer_fallback", "Use lower resolution timers") orelse false;
const shared = b.option(bool, "shared", "Build the tracy client as a shared libary") orelse false;
const options = b.addOptions();
options.addOption(bool, "tracy_enable", tracy_enable);
options.addOption(bool, "tracy_on_demand", tracy_on_demand);
options.addOption(?u8, "tracy_callstack", tracy_callstack);
options.addOption(bool, "tracy_no_callstack", tracy_no_callstack);
options.addOption(bool, "tracy_no_callstack_inlines", tracy_no_callstack_inlines);
options.addOption(bool, "tracy_only_localhost", tracy_only_localhost);
options.addOption(bool, "tracy_no_broadcast", tracy_no_broadcast);
options.addOption(bool, "tracy_only_ipv4", tracy_only_ipv4);
options.addOption(bool, "tracy_no_code_transfer", tracy_no_code_transfer);
options.addOption(bool, "tracy_no_context_switch", tracy_no_context_switch);
options.addOption(bool, "tracy_no_exit", tracy_no_exit);
options.addOption(bool, "tracy_no_sampling", tracy_no_sampling);
options.addOption(bool, "tracy_no_verify", tracy_no_verify);
options.addOption(bool, "tracy_no_vsync_capture", tracy_no_vsync_capture);
options.addOption(bool, "tracy_no_frame_image", tracy_no_frame_image);
options.addOption(bool, "tracy_no_system_tracing", tracy_no_system_tracing);
options.addOption(bool, "tracy_delayed_init", tracy_delayed_init);
options.addOption(bool, "tracy_manual_lifetime", tracy_manual_lifetime);
options.addOption(bool, "tracy_fibers", tracy_fibers);
options.addOption(bool, "tracy_no_crash_handler", tracy_no_crash_handler);
options.addOption(bool, "tracy_timer_fallback", tracy_timer_fallback);
options.addOption(bool, "shared", shared);
const dep_src = b.dependency("src", .{});
const tracy_module = b.addModule("tracy", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
tracy_module.addImport("tracy-options", options.createModule());
tracy_module.addIncludePath(dep_src.path("./public"));
const tracy_client = b.addLibrary(.{
.linkage = if (shared) .dynamic else .static,
.name = "tracy",
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
}),
});
if (target.result.os.tag == .windows) {
tracy_client.root_module.linkSystemLibrary("dbghelp", .{});
tracy_client.root_module.linkSystemLibrary("ws2_32", .{});
}
if (target.result.abi != .msvc) {
tracy_client.root_module.link_libcpp = true;
} else {
tracy_client.root_module.link_libc = true;
}
tracy_client.root_module.addCSourceFile(.{
.file = dep_src.path("./public/TracyClient.cpp"),
.flags = if (target.result.os.tag == .windows) &.{"-fms-extensions"} else &.{},
});
tracy_client.installHeadersDirectory(dep_src.path("public"), "", .{
.include_extensions = &.{".h", ".hpp"},
});
if (tracy_enable)
tracy_client.root_module.addCMacro("TRACY_ENABLE", "1");
if (tracy_on_demand)
tracy_client.root_module.addCMacro("TRACY_ON_DEMAND", "1");
if (tracy_callstack) |depth| {
tracy_client.root_module.addCMacro("TRACY_CALLSTACK", "\"" ++ digits2(depth) ++ "\"");
}
if (tracy_no_callstack)
tracy_client.root_module.addCMacro("TRACY_NO_CALLSTACK", "1");
if (tracy_no_callstack_inlines)
tracy_client.root_module.addCMacro("TRACY_NO_CALLSTACK_INLINES", "1");
if (tracy_only_localhost)
tracy_client.root_module.addCMacro("TRACY_ONLY_LOCALHOST", "1");
if (tracy_no_broadcast)
tracy_client.root_module.addCMacro("TRACY_NO_BROADCAST", "1");
if (tracy_only_ipv4)
tracy_client.root_module.addCMacro("TRACY_ONLY_IPV4", "1");
if (tracy_no_code_transfer)
tracy_client.root_module.addCMacro("TRACY_NO_CODE_TRANSFER", "1");
if (tracy_no_context_switch)
tracy_client.root_module.addCMacro("TRACY_NO_CONTEXT_SWITCH", "1");
if (tracy_no_exit)
tracy_client.root_module.addCMacro("TRACY_NO_EXIT", "1");
if (tracy_no_sampling)
tracy_client.root_module.addCMacro("TRACY_NO_SAMPLING", "1");
if (tracy_no_verify)
tracy_client.root_module.addCMacro("TRACY_NO_VERIFY", "1");
if (tracy_no_vsync_capture)
tracy_client.root_module.addCMacro("TRACY_NO_VSYNC_CAPTURE", "1");
if (tracy_no_frame_image)
tracy_client.root_module.addCMacro("TRACY_NO_FRAME_IMAGE", "1");
if (tracy_no_system_tracing)
tracy_client.root_module.addCMacro("TRACY_NO_SYSTEM_TRACING", "1");
if (tracy_delayed_init)
tracy_client.root_module.addCMacro("TRACY_DELAYED_INIT", "1");
if (tracy_manual_lifetime)
tracy_client.root_module.addCMacro("TRACY_MANUAL_LIFETIME", "1");
if (tracy_fibers)
tracy_client.root_module.addCMacro("TRACY_FIBERS", "1");
if (tracy_no_crash_handler)
tracy_client.root_module.addCMacro("TRACY_NO_CRASH_HANDLER", "1");
if (tracy_timer_fallback)
tracy_client.root_module.addCMacro("TRACY_TIMER_FALLBACK", "1");
if (shared and target.result.os.tag == .windows)
tracy_client.root_module.addCMacro("TRACY_EXPORTS", "1");
b.installArtifact(tracy_client);
}

13
libs/tracy/build.zig.zon Normal file
View File

@ -0,0 +1,13 @@
.{
.name = .tracy,
.version = "0.0.1",
.dependencies = .{
.src = .{
.url = "https://github.com/wolfpld/tracy/archive/refs/tags/v0.13.1.tar.gz",
.hash = "N-V-__8AAOncKwEm1F9c5LrT7HMNmRMYX8-fAoqpc6YyTu9X",
},
},
.minimum_zig_version = "0.16.0",
.paths = .{""},
.fingerprint = 0x255a89eeaaacb778,
}

376
libs/tracy/src/root.zig Normal file
View File

@ -0,0 +1,376 @@
const std = @import("std");
const digits2 = std.fmt.digits2;
const builtin = @import("builtin");
const options = @import("tracy-options");
const c = @cImport({
if (options.tracy_enable) @cDefine("TRACY_ENABLE", {});
if (options.tracy_on_demand) @cDefine("TRACY_ON_DEMAND", {});
if (options.tracy_callstack) |depth| @cDefine("TRACY_CALLSTACK", "\"" ++ digits2(depth) ++ "\"");
if (options.tracy_no_callstack) @cDefine("TRACY_NO_CALLSTACK", {});
if (options.tracy_no_callstack_inlines) @cDefine("TRACY_NO_CALLSTACK_INLINES", {});
if (options.tracy_only_localhost) @cDefine("TRACY_ONLY_LOCALHOST", {});
if (options.tracy_no_broadcast) @cDefine("TRACY_NO_BROADCAST", {});
if (options.tracy_only_ipv4) @cDefine("TRACY_ONLY_IPV4", {});
if (options.tracy_no_code_transfer) @cDefine("TRACY_NO_CODE_TRANSFER", {});
if (options.tracy_no_context_switch) @cDefine("TRACY_NO_CONTEXT_SWITCH", {});
if (options.tracy_no_exit) @cDefine("TRACY_NO_EXIT", {});
if (options.tracy_no_sampling) @cDefine("TRACY_NO_SAMPLING", {});
if (options.tracy_no_verify) @cDefine("TRACY_NO_VERIFY", {});
if (options.tracy_no_vsync_capture) @cDefine("TRACY_NO_VSYNC_CAPTURE", {});
if (options.tracy_no_frame_image) @cDefine("TRACY_NO_FRAME_IMAGE", {});
if (options.tracy_no_system_tracing) @cDefine("TRACY_NO_SYSTEM_TRACING", {});
if (options.tracy_delayed_init) @cDefine("TRACY_DELAYED_INIT", {});
if (options.tracy_manual_lifetime) @cDefine("TRACY_MANUAL_LIFETIME", {});
if (options.tracy_fibers) @cDefine("TRACY_FIBERS", {});
if (options.tracy_no_crash_handler) @cDefine("TRACY_NO_CRASH_HANDLER", {});
if (options.tracy_timer_fallback) @cDefine("TRACY_TIMER_FALLBACK", {});
if (options.shared and builtin.os.tag == .windows) @cDefine("TRACY_IMPORTS", {});
@cInclude("tracy/TracyC.h");
});
pub inline fn setThreadName(comptime name: [:0]const u8) void {
if (!options.tracy_enable) return;
c.___tracy_set_thread_name(name);
}
pub inline fn startupProfiler() void {
if (!options.tracy_enable) return;
if (!options.tracy_manual_lifetime) return;
c.___tracy_startup_profiler();
}
pub inline fn shutdownProfiler() void {
if (!options.tracy_enable) return;
if (!options.tracy_manual_lifetime) return;
c.___tracy_shutdown_profiler();
}
pub inline fn profilerStarted() bool {
if (!options.tracy_enable) return false;
if (!options.tracy_manual_lifetime) return true;
return c.___tracy_profiler_started() != 0;
}
pub inline fn isConnected() bool {
if (!options.tracy_enable) return false;
return c.___tracy_connected() > 0;
}
pub inline fn frameMark() void {
if (!options.tracy_enable) return;
c.___tracy_emit_frame_mark(null);
}
pub inline fn frameMarkNamed(comptime name: [:0]const u8) void {
if (!options.tracy_enable) return;
c.___tracy_emit_frame_mark(name);
}
const DiscontinuousFrame = struct {
name: [:0]const u8,
pub inline fn deinit(frame: *const DiscontinuousFrame) void {
if (!options.tracy_enable) return;
c.___tracy_emit_frame_mark_end(frame.name);
}
};
pub inline fn initDiscontinuousFrame(comptime name: [:0]const u8) DiscontinuousFrame {
if (!options.tracy_enable) return .{ .name = name };
c.___tracy_emit_frame_mark_start(name);
return .{ .name = name };
}
pub inline fn frameImage(image: *anyopaque, width: u16, height: u16, offset: u8, flip: bool) void {
if (!options.tracy_enable) return;
c.___tracy_emit_frame_mark_image(image, width, height, offset, @as(c_int, @intFromBool(flip)));
}
pub const ZoneOptions = struct {
active: bool = true,
name: ?[]const u8 = null,
color: ?u32 = null,
};
const ZoneContext = if (options.tracy_enable) extern struct {
ctx: c.___tracy_c_zone_context,
pub inline fn deinit(zone: *const ZoneContext) void {
if (!options.tracy_enable) return;
c.___tracy_emit_zone_end(zone.ctx);
}
pub inline fn name(zone: *const ZoneContext, zone_name: []const u8) void {
if (!options.tracy_enable) return;
c.___tracy_emit_zone_name(zone.ctx, zone_name.ptr, zone_name.len);
}
pub inline fn text(zone: *const ZoneContext, zone_text: []const u8) void {
if (!options.tracy_enable) return;
c.___tracy_emit_zone_text(zone.ctx, zone_text.ptr, zone_text.len);
}
pub inline fn color(zone: *const ZoneContext, zone_color: u32) void {
if (!options.tracy_enable) return;
c.___tracy_emit_zone_color(zone.ctx, zone_color);
}
pub inline fn value(zone: *const ZoneContext, zone_value: u64) void {
if (!options.tracy_enable) return;
c.___tracy_emit_zone_value(zone.ctx, zone_value);
}
} else struct {
pub inline fn deinit(_: *const ZoneContext) void {}
pub inline fn name(_: *const ZoneContext, _: []const u8) void {}
pub inline fn text(_: *const ZoneContext, _: []const u8) void {}
pub inline fn color(_: *const ZoneContext, _: u32) void {}
pub inline fn value(_: *const ZoneContext, _: u64) void {}
};
pub inline fn initZone(comptime src: std.builtin.SourceLocation, comptime opts: ZoneOptions) ZoneContext {
if (!options.tracy_enable) return .{};
const active: c_int = @intFromBool(opts.active);
const static = struct {
var src_loc = c.___tracy_source_location_data{
.name = if (opts.name) |name| name.ptr else null,
.function = src.fn_name.ptr,
.file = src.file,
.line = 0,
.color = opts.color orelse 0,
};
};
// src.line magically is not comptime https://github.com/ziglang/zig/pull/12016#issuecomment-1178092847
static.src_loc.line = src.line;
if (!options.tracy_no_callstack) {
if (options.tracy_callstack) |depth| {
return .{
.ctx = c.___tracy_emit_zone_begin_callstack(&static.src_loc, depth, active),
};
}
}
return .{
.ctx = c.___tracy_emit_zone_begin(&static.src_loc, active),
};
}
pub inline fn plot(comptime T: type, comptime name: [:0]const u8, value: T) void {
if (!options.tracy_enable) return;
const type_info = @typeInfo(T);
switch (type_info) {
.int => |int_type| {
if (int_type.bits > 64) @compileError("Too large int to plot");
if (int_type.signedness == .unsigned and int_type.bits > 63) @compileError("Too large unsigned int to plot");
c.___tracy_emit_plot_int(name, value);
},
.float => |float_type| {
if (float_type.bits <= 32) {
c.___tracy_emit_plot_float(name, value);
} else if (float_type.bits <= 64) {
c.___tracy_emit_plot(name, value);
} else {
@compileError("Too large float to plot");
}
},
else => @compileError("Unsupported plot value type"),
}
}
pub const PlotType = enum(c_int) {
Number,
Memory,
Percentage,
Watt,
};
pub const PlotConfig = struct {
plot_type: PlotType,
step: c_int,
fill: c_int,
color: u32,
};
pub inline fn plotConfig(comptime name: [:0]const u8, comptime config: PlotConfig) void {
if (!options.tracy_enable) return;
c.___tracy_emit_plot_config(
name,
@intFromEnum(config.plot_type),
config.step,
config.fill,
config.color,
);
}
pub inline fn message(comptime msg: [:0]const u8) void {
if (!options.tracy_enable) return;
const depth = options.tracy_callstack orelse 0;
c.___tracy_emit_messageL(msg, depth);
}
pub inline fn messageColor(comptime msg: [:0]const u8, color: u32) void {
if (!options.tracy_enable) return;
const depth = options.tracy_callstack orelse 0;
c.___tracy_emit_messageLC(msg, color, depth);
}
const tracy_message_buffer_size = if (options.tracy_enable) 4096 else 0;
threadlocal var tracy_message_buffer: [tracy_message_buffer_size]u8 = undefined;
pub inline fn print(comptime fmt: []const u8, args: anytype) void {
if (!options.tracy_enable) return;
const depth = options.tracy_callstack orelse 0;
var stream = std.io.fixedBufferStream(&tracy_message_buffer);
stream.writer().print(fmt, args) catch {};
const written = stream.getWritten();
c.___tracy_emit_message(written.ptr, written.len, depth);
}
pub inline fn printColor(comptime fmt: []const u8, args: anytype, color: u32) void {
if (!options.tracy_enable) return;
const depth = options.tracy_callstack orelse 0;
var stream = std.io.fixedBufferStream(&tracy_message_buffer);
stream.writer().print(fmt, args) catch {};
const written = stream.getWritten();
c.___tracy_emit_messageC(written.ptr, written.len, color, depth);
}
pub inline fn printAppInfo(comptime fmt: []const u8, args: anytype) void {
if (!options.tracy_enable) return;
var stream = std.io.fixedBufferStream(&tracy_message_buffer);
stream.reset();
stream.writer().print(fmt, args) catch {};
const written = stream.getWritten();
c.___tracy_emit_message_appinfo(written.ptr, written.len);
}
pub const TracingAllocator = struct {
parent_allocator: std.mem.Allocator,
pool_name: ?[:0]const u8,
const Self = @This();
pub fn init(parent_allocator: std.mem.Allocator) Self {
return .{
.parent_allocator = parent_allocator,
.pool_name = null,
};
}
pub fn initNamed(comptime pool_name: [:0]const u8, parent_allocator: std.mem.Allocator) Self {
return .{
.parent_allocator = parent_allocator,
.pool_name = pool_name,
};
}
pub fn allocator(self: *Self) std.mem.Allocator {
return .{
.ptr = self,
.vtable = &.{
.alloc = alloc,
.resize = resize,
.remap = remap,
.free = free,
},
};
}
fn alloc(
ctx: *anyopaque,
len: usize,
ptr_align: std.mem.Alignment,
ret_addr: usize,
) ?[*]u8 {
const self: *Self = @ptrCast(@alignCast(ctx));
const result = self.parent_allocator.rawAlloc(len, ptr_align, ret_addr);
if (!options.tracy_enable) return result;
if (self.pool_name) |name| {
c.___tracy_emit_memory_alloc_named(result, len, 0, name.ptr);
} else {
c.___tracy_emit_memory_alloc(result, len, 0);
}
return result;
}
fn resize(
ctx: *anyopaque,
buf: []u8,
buf_align: std.mem.Alignment,
new_len: usize,
ret_addr: usize,
) bool {
const self: *Self = @ptrCast(@alignCast(ctx));
const result = self.parent_allocator.rawResize(buf, buf_align, new_len, ret_addr);
if (!result) return false;
if (!options.tracy_enable) return true;
if (self.pool_name) |name| {
c.___tracy_emit_memory_free_named(buf.ptr, 0, name.ptr);
c.___tracy_emit_memory_alloc_named(buf.ptr, new_len, 0, name.ptr);
} else {
c.___tracy_emit_memory_free(buf.ptr, 0);
c.___tracy_emit_memory_alloc(buf.ptr, new_len, 0);
}
return true;
}
fn remap(
ctx: *anyopaque,
buf: []u8,
buf_align: std.mem.Alignment,
new_len: usize,
ret_addr: usize,
) ?[*]u8 {
const self: *Self = @ptrCast(@alignCast(ctx));
const result = self.parent_allocator.rawRemap(buf, buf_align, new_len, ret_addr);
const new_buf = result orelse return null;
if (!options.tracy_enable) return new_buf;
if (self.pool_name) |name| {
c.___tracy_emit_memory_free_named(buf.ptr, 0, name.ptr);
c.___tracy_emit_memory_alloc_named(new_buf, new_len, 0, name.ptr);
} else {
c.___tracy_emit_memory_free(buf.ptr, 0);
c.___tracy_emit_memory_alloc(new_buf, new_len, 0);
}
return new_buf;
}
fn free(
ctx: *anyopaque,
buf: []u8,
buf_align: std.mem.Alignment,
ret_addr: usize,
) void {
const self: *Self = @ptrCast(@alignCast(ctx));
if (options.tracy_enable) {
if (self.pool_name) |name| {
c.___tracy_emit_memory_free_named(buf.ptr, 0, name.ptr);
} else {
c.___tracy_emit_memory_free(buf.ptr, 0);
}
}
self.parent_allocator.rawFree(buf, buf_align, ret_addr);
}
};

57
scripts/png-to-icon.zig Normal file
View File

@ -0,0 +1,57 @@
const std = @import("std");
const STBImage = @import("stb_image");
// https://en.wikipedia.org/wiki/ICO_(file_format)#Icon_file_structure
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer _ = gpa.deinit();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len != 3) {
std.debug.print("Usage: ./png-to-icon <png-file> <output-ico>", .{});
std.process.exit(1);
}
const cwd = std.fs.cwd();
const input_png_path = args[1];
const output_ico_path = args[2];
const input_png_data = try cwd.readFileAlloc(allocator, input_png_path, 1024 * 1024 * 5);
defer allocator.free(input_png_data);
const png_image = try STBImage.load(input_png_data);
defer png_image.deinit();
std.debug.assert(png_image.width > 0 and png_image.width <= 256);
std.debug.assert(png_image.height > 0 and png_image.height <= 256);
const output_ico_file = try std.fs.cwd().createFile(output_ico_path, .{ });
defer output_ico_file.close();
var buffer: [4096 * 4]u8 = undefined;
var writer = output_ico_file.writer(&buffer);
// ICONDIR structure
try writer.interface.writeInt(u16, 0, .little); // Must always be zero
try writer.interface.writeInt(u16, 1, .little); // Image type. 1 for .ICO
try writer.interface.writeInt(u16, 1, .little); // Number of images
// ICONDIRENTRY structure
try writer.interface.writeInt(u8, @truncate(png_image.width), .little); // Image width
try writer.interface.writeInt(u8, @truncate(png_image.height), .little); // Image height
try writer.interface.writeInt(u8, 0, .little); // Number of colors in color pallete. 0 means that color pallete is not used
try writer.interface.writeInt(u8, 0, .little); // Must always be zero
try writer.interface.writeInt(u16, 0, .little); // Color plane
try writer.interface.writeInt(u16, 32, .little); // Bits per pixel
try writer.interface.writeInt(u32, @intCast(input_png_data.len), .little); // Image size in bytes
try writer.interface.writeInt(u32, 22, .little); // Offset to image data from the start
// PNG image data
try writer.interface.writeAll(input_png_data);
try writer.interface.flush();
}

115
src/app.zig Normal file
View File

@ -0,0 +1,115 @@
const std = @import("std");
const CLI = @import("./cli.zig");
const log = std.log.scoped(.app);
const Allocator = std.mem.Allocator;
const Math = @import("math");
const Vec2 = Math.Vec2;
const STBImage = @import("stb_image");
const STBTrueType = @import("stb_truetype");
const Tiled = @import("tiled");
const Platform = @import("./platform.zig");
const Gfx = Platform.Gfx;
const ImGUI = Platform.ImGUI;
const Rect = Platform.Rect;
const App = @This();
tilesheet: Tilesheet,
const Tilesheet = struct {
image: Gfx.ImageData,
sprites: []Gfx.Sprite.Id,
tile_size: Vec2,
width: u32,
height: u32,
pub fn init(gpa: Allocator, image: Gfx.ImageData, width: u32, height: u32, tile_size: Vec2) !Tilesheet {
const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height);
errdefer gpa.free(sprites);
@memset(sprites, Gfx.getNilSprite());
return Tilesheet{
.sprites = sprites,
.image = image,
.width = width,
.height = height,
.tile_size = tile_size
};
}
pub fn deinit(self: Tilesheet) void {
for (self.sprites) |sprite| {
Gfx.deinitSprite(sprite);
}
}
pub fn get(self: Tilesheet, x: u32, y: u32) Gfx.Sprite.Id {
if (x >= self.width or y >= self.height) {
log.warn("Attempt to get tile which is out of bounds", .{});
return Gfx.getNilSprite();
}
const sprite_index = y * self.width + x;
if (self.sprites[sprite_index] == Gfx.getNilSprite()) {
const sprite = Gfx.initSprite(.{ .padding = 1 });
Gfx.setSprite(sprite, .{
.image = self.image,
.rect = Rect.init(@floatFromInt(x), @floatFromInt(y), 1, 1).multiply(self.tile_size)
});
self.sprites[sprite_index] = sprite;
}
return self.sprites[sprite_index];
}
};
pub fn init(self: *App, plt: Platform.Init) !?u8 {
// const robot_font = Gfx.initFont();
// Gfx.setFont(robot_font, plt.assets.readFile("roboto-font/Roboto-Regular.ttf"));
const tilesheet_png = try STBImage.load(plt.assets.readFile("tranquil_tunnels_transparent.png"));
const tilesheet = try Tilesheet.init(
plt.arena,
Gfx.ImageData{
.width = tilesheet_png.width,
.height = tilesheet_png.height,
.pixels = .{ .rgba8 = tilesheet_png.rgba8_pixels }
},
13,
8,
.init(64, 64)
);
self.* = App{
.tilesheet = tilesheet
};
return null;
}
pub fn frame(self: *App, plt: Platform.Frame) !void {
_ = self; // autofix
const input = plt.input;
_ = input; // autofix
const dt = plt.deltaTime();
_ = dt; // autofix
Gfx.setClearColor(.rgb(40, 40, 40));
Gfx.drawLine(.init(0, 200), .init(300, 200), .white, 5);
// Gfx.drawSprite(frames[self.player_frame_index], self.player_pos, .init(64, 64), .white);
// Gfx.drawSprite(self.tilesheet.get(11, 6), self.player_pos.add(.init(0, 200)), .init(64, 64), .white);
// Gfx.drawText(self.roboto_font, .{
// .pos = .init(300, 100),
// .text = "Hello, World!",
// .height = 64,
// });
}
pub fn deinit(self: *App, plt: Platform.Deinit) void {
_ = plt; // autofix
_ = self; // autofix
}

302
src/cli.zig Normal file
View File

@ -0,0 +1,302 @@
const std = @import("std");
const Io = std.Io;
const log = std.log.scoped(.engine);
const CLI = @This();
gpa: std.mem.Allocator,
arena: std.heap.ArenaAllocator,
options: std.ArrayList(Option),
commands: std.ArrayList(Command),
stdout: Channel,
stderr: Channel,
pub const Command = struct {
name: []const u8,
description: []const u8,
const Id = enum (u32) { _ };
};
pub const Option = struct {
long_name: []const u8,
description: []const u8,
kind: Kind,
pub const Kind = enum {
toggle,
single_argument
};
pub const Id = enum (u32) { _ };
const Value = struct {
argument: ?[]const u8 = null
};
};
const Channel = struct {
file: Io.File,
buffer: [4 * 4096]u8,
writer: Io.File.Writer,
pub fn init(self: *Channel, io: Io, file: Io.File) void {
self.* = Channel{
.file = file,
.buffer = undefined,
.writer = file.writer(io, &self.buffer)
};
}
pub fn write(self: *Channel, comptime fmt: []const u8, args: anytype) void {
self.writer.interface.print(fmt, args) catch |e| {
log.err("Failed to write to channel: {}", .{e});
};
}
pub fn flush(self: *Channel) void {
self.writer.interface.flush() catch |e| {
log.err("Failed to flush channel: {}", .{e});
};
}
};
pub fn init(self: *CLI, io: Io, gpa: std.mem.Allocator) void {
self.* = CLI{
.gpa = gpa,
.arena = .init(gpa),
.options = .empty,
.commands = .empty,
.stdout = undefined,
.stderr = undefined
};
self.stdout.init(io, Io.File.stdout());
self.stderr.init(io, Io.File.stderr());
}
pub fn deinit(self: *CLI) void {
self.stderr.flush();
self.stdout.flush();
self.arena.deinit();
self.options.deinit(self.gpa);
self.commands.deinit(self.gpa);
}
pub const AddOptionOptions = struct {
long_name: []const u8,
description: ?[]const u8 = null,
kind: Option.Kind = .toggle,
};
pub fn addOption(self: *CLI, opts: AddOptionOptions) !Option.Id {
const arena = self.arena.allocator();
var owned_description: []const u8 = "";
if (opts.description) |description| {
owned_description = try arena.dupe(u8, description);
}
const index = self.options.items.len;
try self.options.append(self.gpa, .{
.long_name = try arena.dupe(u8, opts.long_name),
.description = owned_description,
.kind = opts.kind
});
return @enumFromInt(index);
}
fn getOptionByLongName(self: *CLI, long_name: []const u8) ?Option.Id {
for (0.., self.options.items) |i, option| {
if (std.mem.eql(u8, long_name, option.long_name)) {
return @enumFromInt(i);
}
}
return null;
}
pub const AddCommandOptions = struct {
name: []const u8,
description: ?[]const u8 = null
};
pub fn addCommand(self: *CLI, opts: AddCommandOptions) !Command.Id {
const arena = self.arena.allocator();
var owned_description: []const u8 = "";
if (opts.description) |description| {
owned_description = try arena.dupe(u8, description);
}
const index = self.commands.items.len;
try self.commands.append(self.gpa, Command{
.name = try arena.dupe(u8, opts.name),
.description = owned_description,
});
return @enumFromInt(index);
}
pub fn showUsage(self: *CLI, program_name: []const u8) void {
self.stderr.write("Usage: {s}", .{program_name});
if (self.options.items.len > 0) {
self.stderr.write(" [options]", .{});
}
if (self.commands.items.len > 0) {
self.stderr.write(" <command>", .{});
}
self.stderr.write("\n", .{});
if (self.commands.items.len > 0) {
self.stderr.write("\n", .{});
self.stderr.write("Commands:\n", .{});
for (self.commands.items) |command| {
self.stderr.write(" {s} {s}\n", .{command.name, command.description});
}
}
if (self.options.items.len > 0) {
self.stderr.write("\n", .{});
self.stderr.write("Options:\n", .{});
for (0.., self.options.items) |i, option| {
if (i > 0) {
self.stderr.write("\n", .{});
}
self.stderr.write(" --{s}", .{option.long_name});
if (option.kind == .single_argument) {
// TODO: Make this hint renameable
self.stderr.write(" <value>", .{});
}
self.stderr.write("\n", .{});
if (option.description.len > 0) {
self.stderr.write(" {s}\n", .{option.description});
}
}
}
self.stderr.flush();
}
pub const ParseResult = struct {
arena: std.heap.ArenaAllocator,
command: ?Command.Id,
options: []?Option.Value,
pub fn init(gpa: std.mem.Allocator, options_len: usize) !ParseResult {
var arena = std.heap.ArenaAllocator.init(gpa);
errdefer arena.deinit();
const options = try arena.allocator().alloc(?Option.Value, options_len);
@memset(options, null);
return ParseResult{
.arena = arena,
.command = null,
.options = options
};
}
pub fn deinit(self: *ParseResult) void {
self.arena.deinit();
}
pub fn getOption(self: ParseResult, id: Option.Id) ?Option.Value {
return self.options[@intFromEnum(id)];
}
pub fn isSet(self: ParseResult, id: Option.Id) bool {
return self.getOption(id) != null;
}
// TODO: I don't like this function name
pub fn getOptionArgument(self: ParseResult, id: Option.Id) ?[]const u8 {
if (self.getOption(id)) |option| {
return option.argument;
}
return null;
}
};
const Parser = struct {
args: []const []const u8,
cursor: u32,
pub fn peek(self: *Parser) ?[]const u8 {
if (self.cursor < self.args.len) {
return self.args[self.cursor];
}
return null;
}
pub fn take(self: *Parser) ?[]const u8 {
if (self.peek()) |value| {
self.cursor += 1;
return value;
}
return null;
}
};
fn printError(self: *CLI, comptime fmt: []const u8, args: anytype) void {
self.stderr.write("ERROR: " ++ fmt ++ "\n", args);
self.stderr.flush();
}
pub fn parse(self: *CLI, gpa: std.mem.Allocator, args: []const []const u8) !ParseResult {
var result = try ParseResult.init(gpa, self.options.items.len);
errdefer result.deinit();
var parser = Parser{
.args = args,
.cursor = 1
};
next_arg: while (parser.take()) |arg| {
if (std.mem.startsWith(u8, arg, "--")) {
const long_option_name = arg[2..];
if (self.getOptionByLongName(long_option_name)) |option_id| {
const option = self.options.items[@intFromEnum(option_id)];
var result_value = Option.Value{};
if (option.kind == .single_argument) {
const argument = parser.take() orelse {
self.printError("Missing required argument for '{s}'", .{arg});
return error.MissingArgument;
};
result_value.argument = argument;
}
result.options[@intFromEnum(option_id)] = result_value;
continue :next_arg;
}
self.printError("Unknown option '{s}'", .{arg});
return error.UnknownOption;
}
if (result.command == null) {
for (0.., self.commands.items) |i, command| {
if (std.mem.eql(u8, command.name, arg)) {
result.command = @enumFromInt(i);
continue :next_arg;
}
}
self.printError("Unknown command '{s}'", .{arg});
return error.UnknownCommand;
} else {
self.printError("Unknown argument '{s}'", .{arg});
return error.UnknownArgument;
}
}
return result;
}

40
src/color.zig Normal file
View File

@ -0,0 +1,40 @@
const std = @import("std");
const assert = std.debug.assert;
const Color = @This();
r: f32,
g: f32,
b: f32,
a: f32,
pub const black = rgb(0, 0, 0);
pub const white = rgb(255, 255, 255);
pub const purple = rgb(255, 0, 255);
pub fn rgba(r: u8, g: u8, b: u8, a: f32) Color {
assert(0 <= a and a <= 1);
return .{
.r = @as(f32, @floatFromInt(r)) / 255,
.g = @as(f32, @floatFromInt(g)) / 255,
.b = @as(f32, @floatFromInt(b)) / 255,
.a = a,
};
}
pub fn rgb(r: u8, g: u8, b: u8) Color {
return rgba(r, g, b, 1);
}
pub fn rgb_hex(text: []const u8) ?Color {
if (text.len != 7) {
return null;
}
if (text[0] != '#') {
return null;
}
const r = std.fmt.parseInt(u8, text[1..3], 16) catch return null;
const g = std.fmt.parseInt(u8, text[3..5], 16) catch return null;
const b = std.fmt.parseInt(u8, text[5..7], 16) catch return null;
return rgb(r, g, b);
}

320
src/file_watcher.zig Normal file
View File

@ -0,0 +1,320 @@
const std = @import("std");
const INotify = @import("inotify.zig");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const FileWatcher = @This();
root_dir: []u8,
inotify: INotify,
files: std.ArrayList(File),
watches: std.ArrayList(Watch),
const File = struct {
path: []u8,
debounce: Io.Duration,
last_event_at: ?Io.Timestamp,
pub fn deinit(self: File, gpa: Allocator) void {
gpa.free(self.path);
}
};
const Watch = struct {
wd: ?u32,
path: []u8,
pub fn deinit(self: Watch, gpa: Allocator) void {
gpa.free(self.path);
}
};
pub fn init(gpa: Allocator, root_dir: []const u8) !FileWatcher {
if (!std.fs.path.isAbsolute(root_dir)) {
return error.PathIsNotAbsolute;
}
const inotify_buf = try gpa.alloc(u8, INotify.max_event_size * 10);
errdefer gpa.free(inotify_buf);
const root_dir_owned = try gpa.dupe(u8, root_dir);
errdefer gpa.free(root_dir_owned);
return FileWatcher{
.root_dir = root_dir_owned,
.inotify = try .init(inotify_buf),
.files = .empty,
.watches = .empty
};
}
pub fn deinit(self: *FileWatcher, gpa: Allocator) void {
gpa.free(self.root_dir);
gpa.free(self.inotify.buf);
self.inotify.deinit();
for (self.files.items) |file| {
file.deinit(gpa);
}
self.files.deinit(gpa);
for (self.watches.items) |watch| {
watch.deinit(gpa);
}
self.watches.deinit(gpa);
}
fn findFileIndex(self: *FileWatcher, path: []const u8) ?usize {
for (0.., self.files.items) |i, file| {
if (std.mem.eql(u8, file.path, path)) {
return i;
}
}
return null;
}
fn findFile(self: *FileWatcher, path: []const u8) ?*File {
if (self.findFileIndex(path)) |file_index| {
return &self.files.items[file_index];
} else {
return null;
}
}
fn findWatchByPath(self: *FileWatcher, path: []const u8) ?usize {
for (0.., self.watches.items) |i, watch| {
if (std.mem.eql(u8, watch.path, path)) {
return i;
}
}
return null;
}
fn findWatchByDescriptor(self: *FileWatcher, wd: u32) ?usize {
for (0.., self.watches.items) |i, watch| {
if (watch.wd == wd) {
return i;
}
}
return null;
}
fn bufJoinPaths(path_buff: []u8, paths: []const []const u8) ![:0]u8 {
return try std.fmt.bufPrintZ(path_buff, "{f}", .{
std.fs.path.fmtJoin(paths)
});
}
fn attemptRegisteringWatch(self: *FileWatcher, watch: *Watch) !void {
if (watch.wd != null) {
return;
}
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
const path = try bufJoinPaths(&path_buff, &.{ self.root_dir, watch.path });
watch.wd = self.inotify.add(path, INotify.Mask{
.create = true,
.modify = true,
.delete_self = true,
.move_self = true,
.moved_to = true,
.moved_from = true,
}) catch |err| switch (err) {
error.FileNotFound => null,
else => return err
};
}
pub fn toRelativePath(outer_path: []const u8, inner_path: []const u8) ?[]const u8 {
if (inner_path.len == outer_path.len) {
if (std.mem.eql(u8, inner_path, outer_path)) {
return "";
}
} else if (inner_path.len > outer_path.len) {
if (std.mem.startsWith(u8, inner_path, outer_path) and inner_path[outer_path.len] == std.fs.path.sep) {
return inner_path[(outer_path.len+1)..];
}
}
return null;
}
fn isInside(outer_path: []const u8, inner_path: []const u8) bool {
return toRelativePath(outer_path, inner_path) != null;
}
fn unregisterWatchesRecursively(self: *FileWatcher, dir: []const u8) !void {
for (self.watches.items) |*watch| {
const wd = watch.wd orelse continue;
if (isInside(dir, watch.path)) {
try self.inotify.remove(wd);
watch.wd = null;
}
}
}
fn registerWatchesRecursively(self: *FileWatcher, dir: []const u8) !void {
for (self.watches.items) |*watch| {
if (watch.wd == null and isInside(dir, watch.path)) {
try self.attemptRegisteringWatch(watch);
}
}
}
fn ensureWatchExists(self: *FileWatcher, gpa: Allocator, path: []const u8) !usize {
var watch_index: usize = undefined;
if (self.findWatchByPath(path)) |found_watch_index| {
watch_index = found_watch_index;
} else {
try self.watches.ensureUnusedCapacity(gpa, 1);
const path_owned = try gpa.dupe(u8, path);
errdefer gpa.free(path_owned);
watch_index = self.watches.items.len;
self.watches.appendAssumeCapacity(Watch{
.wd = null,
.path = path_owned
});
}
try self.attemptRegisteringWatch(&self.watches.items[watch_index]);
return watch_index;
}
fn removeWatch(self: *FileWatcher, gpa: Allocator, index: usize) void {
const watch = self.watches.swapRemove(index);
watch.deinit(gpa);
}
pub fn add(self: *FileWatcher, gpa: Allocator, path: []const u8, debounce: Io.Duration) !void {
if (std.fs.path.isAbsolute(path)) {
return error.PathIsAbsolute;
}
if (self.findFileIndex(path) != null) {
return;
}
var dir_iter: ?[]const u8 = path;
while (dir_iter) |current| {
_ = try self.ensureWatchExists(gpa, current);
dir_iter = std.fs.path.dirname(current);
}
_ = try self.ensureWatchExists(gpa, "");
try self.files.ensureUnusedCapacity(gpa, 1);
const path_owned = try gpa.dupe(u8, path);
errdefer gpa.free(path_owned);
self.files.appendAssumeCapacity(File{
.path = path_owned,
.debounce = debounce,
.last_event_at = null
});
}
pub fn remove(self: *FileWatcher, gpa: Allocator, path: []const u8) void {
const index = self.findFileIndex(path);
if (index == null) {
return;
}
const file = self.files.items[index];
self.files.swapRemove(index);
file.deinit(gpa);
}
fn nextEvents(self: *FileWatcher) !?*File {
while (try self.inotify.next()) |e| {
const watch_index = self.findWatchByDescriptor(e.wd) orelse continue;
var watch = &self.watches.items[watch_index];
if (e.mask.ignored) {
watch.wd = null;
} else if (e.mask.q_overflow) {
return error.QueueOverflow;
} else if (e.mask.delete_self or e.mask.modify) {
if (self.findFile(watch.path)) |file| {
return file;
}
} else if (e.mask.create) {
assert(e.name != null);
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) });
if (self.findWatchByPath(path)) |child_watch_index| {
try self.attemptRegisteringWatch(&self.watches.items[child_watch_index]);
}
if (self.findFile(path)) |file| {
return file;
}
} else if (e.mask.moved_from) {
assert(e.name != null);
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) });
try self.unregisterWatchesRecursively(path);
if (self.findFile(path)) |file| {
return file;
}
} else if (e.mask.moved_to) {
assert(e.name != null);
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) });
try self.registerWatchesRecursively(path);
if (self.findFile(path)) |file| {
return file;
}
}
}
return null;
}
pub fn next(self: *FileWatcher, io: Io) !?[]const u8 {
const now = Io.Clock.real.now(io);
var queue_overflow = false;
while (true) {
const file = (self.nextEvents() catch |err| switch (err) {
error.QueueOverflow => {
queue_overflow = true;
break;
},
else => return err
}) orelse break;
file.last_event_at = now;
}
if (queue_overflow) {
for (self.files.items) |*file| {
file.last_event_at = now;
}
}
for (self.files.items) |*file| {
const last_event_at = file.last_event_at orelse continue;
if (now.nanoseconds > last_event_at.addDuration(file.debounce).nanoseconds) {
file.last_event_at = null;
return file.path;
}
}
return null;
}

1996
src/graphics.zig Normal file

File diff suppressed because it is too large Load Diff

339
src/imgui.zig Normal file
View File

@ -0,0 +1,339 @@
const std = @import("std");
const log = std.log.scoped(.imgui);
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const build_options = @import("build_options");
const Color = @import("./color.zig");
const Math = @import("math");
const Vec2 = Math.Vec2;
const sokol = @import("sokol");
const sapp = sokol.app;
const simgui = sokol.imgui;
const ig = @import("cimgui");
const ImGUI = @This();
const State = struct {
enabled: bool,
frame: ?Allocator
};
var g_state: State = undefined;
pub fn init(logger: simgui.Logger) void {
if (!build_options.has_imgui) {
return;
}
const self = &g_state;
simgui.setup(.{
.logger = logger
});
if (@hasDecl(ig, "ImGuiConfigFlags_DockingEnable")) {
ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_DockingEnable;
ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_ViewportsEnable;
}
self.* = State{
.enabled = true,
.frame = null
};
}
pub fn deinit() void {
if (!build_options.has_imgui) {
return;
}
simgui.shutdown();
}
pub fn event(e: sapp.Event) bool {
if (!build_options.has_imgui) {
return false;
}
const self = &g_state;
if (!self.enabled) {
return false;
}
return simgui.handleEvent(e);
}
pub fn beginFrame(enabled: bool, frame: Allocator) void {
if (!build_options.has_imgui) {
return;
}
const self = &g_state;
self.frame = frame;
self.enabled = enabled;
if (!self.enabled) {
return;
}
simgui.newFrame(.{
.width = sapp.width(),
.height = sapp.height(),
.delta_time = sapp.frameDuration(),
.dpi_scale = sapp.dpiScale(),
});
const viewport = ig.igGetMainViewport();
ig.igSetNextWindowPos(viewport[0].WorkPos, ig.ImGuiCond_Always);
ig.igSetNextWindowSize(viewport[0].WorkSize, ig.ImGuiCond_Always);
ig.igSetNextWindowViewport(viewport[0].ID);
const window_flags =
ig.ImGuiWindowFlags_NoDocking |
ig.ImGuiWindowFlags_NoTitleBar |
ig.ImGuiWindowFlags_NoCollapse |
ig.ImGuiWindowFlags_NoResize |
ig.ImGuiWindowFlags_NoMove |
ig.ImGuiWindowFlags_NoBackground |
ig.ImGuiWindowFlags_NoBringToFrontOnFocus |
ig.ImGuiWindowFlags_NoNavFocus;
ig.igPushStyleVar(ig.ImGuiStyleVar_WindowRounding, 0.0);
ig.igPushStyleVar(ig.ImGuiStyleVar_WindowBorderSize, 0.0);
ig.igPushStyleVarImVec2(ig.ImGuiStyleVar_WindowPadding, .{ .x = 0, .y = 0 });
_ = ig.igBegin("DockSpace", null, window_flags);
ig.igPopStyleVarEx(3);
const dockspace_id = ig.igGetID("MyDockSpace");
_ = ig.igDockSpaceEx(dockspace_id, ig.ImVec2{ .x = 0, .y = 0 }, ig.ImGuiDockNodeFlags_PassthruCentralNode, null);
}
pub fn endFrame() void {
if (!build_options.has_imgui) {
return;
}
const self = &g_state;
if (!self.enabled) {
return;
}
// TODO:
// if (ig.igBegin("Game", null, ig.ImGuiWindowFlags_None)) {
// // std.debug.print("{}\n", .{ig.igGetContentRegionAvail()});
// }
// ig.igEnd();
ig.igEnd();
// if (ig.igBeginMainMenuBar()) {
// defer ig.igEndMainMenuBar();
//
// if (ig.igBeginMenu("foo")) {
// defer ig.igEndMenu();
//
// if (ig.igMenuItem("bar")) {
// }
// }
// }
simgui.render();
}
pub const WindowOptions = struct {
name: []const u8,
pos: ?Vec2 = null,
size: ?Vec2 = null,
collapsed: ?bool = null,
open: ?*bool = null,
};
pub fn beginWindow(opts: WindowOptions) bool {
if (!build_options.has_imgui) {
return false;
}
const self = &g_state;
if (!self.enabled) {
return false;
}
if (opts.open != null and opts.open.?.* == false) {
return false;
}
if (opts.pos) |pos| {
ig.igSetNextWindowPos(toImVec2(pos), ig.ImGuiCond_Once);
}
if (opts.size) |size| {
ig.igSetNextWindowSize(toImVec2(size), ig.ImGuiCond_Once);
}
if (opts.collapsed) |collapsed| {
ig.igSetNextWindowCollapsed(collapsed, ig.ImGuiCond_Once);
}
ig.igSetNextWindowBgAlpha(1);
const frame = g_state.frame.?;
const namez = frame.dupeSentinel(u8, opts.name, 0) catch blk: {
log.err("dupeSentinel() failed",.{});
break :blk "<unknown>";
};
var open = ig.igBegin(namez, opts.open, ig.ImGuiWindowFlags_None);
if (opts.open) |opts_open| {
if (opts_open.* == false) {
open = false;
}
}
if (!open) {
endWindow();
}
return open;
}
pub fn endWindow() void {
if (!build_options.has_imgui) {
return;
}
const self = &g_state;
if (!self.enabled) {
return;
}
ig.igEnd();
}
fn formatString(comptime fmt: []const u8, args: anytype) [:0]const u8 {
const frame = g_state.frame.?;
return std.fmt.allocPrintSentinel(frame, fmt, args, 0) catch blk: {
log.err("allocPrintSentinel() failed", .{});
break :blk "<unknown>";
};
}
pub fn text(comptime fmt: []const u8, args: anytype) void {
if (!build_options.has_imgui) {
return;
}
const self = &g_state;
if (!self.enabled) {
return;
}
ig.igTextUnformatted(formatString(fmt, args));
}
pub fn button(label: []const u8) bool {
if (!build_options.has_imgui) {
return false;
}
const self = &g_state;
if (!self.enabled) {
return false;
}
return ig.igButton(formatString("{s}", .{label}));
}
pub const SliderOptions = struct {
label: []const u8,
value: *f32,
min: f32,
max: f32,
};
pub fn slider(opts: SliderOptions) bool {
if (!build_options.has_imgui) {
return false;
}
const self = &g_state;
if (!self.enabled) {
return false;
}
return ig.igSliderFloat(
formatString("{s}", .{opts.label}),
opts.value,
opts.min,
opts.max
);
}
pub fn checkbox(label: []const u8, value: *bool) bool {
if (!build_options.has_imgui) {
return false;
}
const self = &g_state;
if (!self.enabled) {
return false;
}
return ig.igCheckbox(
formatString("{s}", .{label}),
value,
);
}
pub fn separator() void {
if (!build_options.has_imgui) {
return;
}
const self = &g_state;
if (!self.enabled) {
return;
}
ig.igSeparator();
}
const ColorEditOptions = struct {
label: []const u8,
value: *Color,
has_alpha: bool = false
};
pub fn colorEdit(opts: ColorEditOptions) bool {
if (!build_options.has_imgui) {
return false;
}
const self = &g_state;
if (!self.enabled) {
return false;
}
const label = formatString("{s}", .{opts.label});
var color: [4]f32 = .{
opts.value.r,
opts.value.g,
opts.value.b,
opts.value.a,
};
var changed: bool = undefined;
if (opts.has_alpha) {
changed = ig.igColorEdit4(label, &color, ig.ImGuiColorEditFlags_None);
} else {
changed = ig.igColorEdit3(label, &color, ig.ImGuiColorEditFlags_None);
}
if (changed) {
opts.value.r = color[0];
opts.value.g = color[1];
opts.value.b = color[2];
opts.value.a = color[3];
}
return changed;
}
fn toImVec2(vec2: Vec2) ig.ImVec2 {
return ig.ImVec2{
.x = vec2.x,
.y = vec2.y,
};
}

186
src/inotify.zig Normal file
View File

@ -0,0 +1,186 @@
const std = @import("std");
const Io = std.Io;
const assert = std.debug.assert;
const linux = std.os.linux;
const INotify = @This();
fd: i32,
buf: []u8,
buf_offset: usize,
buf_used: usize,
pub const Mask = packed struct(u32) {
access: bool = false,
modify: bool = false,
attrib: bool = false,
close_write: bool = false,
close_nowrite: bool = false,
open: bool = false,
moved_from: bool = false,
moved_to: bool = false,
create: bool = false,
delete: bool = false,
delete_self: bool = false,
move_self: bool = false,
_unused1: u1 = 0,
unmount: bool = false,
q_overflow: bool = false,
ignored: bool = false,
_unused2: u8 = 0,
onlydir: bool = false,
dont_follow: bool = false,
excl_unlink: bool = false,
_unused3: u1 = 0,
mask_create: bool = false,
mask_add: bool = false,
isdir: bool = false,
oneshot: bool = false,
pub fn format(self: Mask, writer: *Io.Writer) Io.Writer.Error!void {
try writer.writeAll(".{");
var first = true;
inline for (@typeInfo(Mask).@"struct".fields) |field| {
if (@typeInfo(field.type) == .bool and @field(self, field.name)) {
if (!first) {
try writer.writeAll(",");
}
try writer.print(" .{s} = true", .{field.name});
first = false;
}
}
try writer.writeAll(" }");
}
fn testMask(expected: u32, actual: Mask) !void {
try std.testing.expectEqual(expected, @as(u32, @bitCast(actual)));
}
test {
try testMask(linux.IN.ACCESS, Mask{ .access = true });
try testMask(linux.IN.MODIFY, Mask{ .modify = true });
try testMask(linux.IN.ATTRIB, Mask{ .attrib = true });
try testMask(linux.IN.CLOSE_WRITE, Mask{ .close_write = true });
try testMask(linux.IN.CLOSE_NOWRITE, Mask{ .close_nowrite = true });
try testMask(linux.IN.OPEN, Mask{ .open = true });
try testMask(linux.IN.MOVED_FROM, Mask{ .moved_from = true });
try testMask(linux.IN.MOVED_TO, Mask{ .moved_to = true });
try testMask(linux.IN.CREATE, Mask{ .create = true });
try testMask(linux.IN.DELETE, Mask{ .delete = true });
try testMask(linux.IN.DELETE_SELF, Mask{ .delete_self = true });
try testMask(linux.IN.MOVE_SELF, Mask{ .move_self = true });
try testMask(linux.IN.UNMOUNT, Mask{ .unmount = true });
try testMask(linux.IN.Q_OVERFLOW, Mask{ .q_overflow = true });
try testMask(linux.IN.IGNORED, Mask{ .ignored = true });
try testMask(linux.IN.ONLYDIR, Mask{ .onlydir = true });
try testMask(linux.IN.DONT_FOLLOW, Mask{ .dont_follow = true });
try testMask(linux.IN.EXCL_UNLINK, Mask{ .excl_unlink = true });
try testMask(linux.IN.MASK_CREATE, Mask{ .mask_create = true });
try testMask(linux.IN.MASK_ADD, Mask{ .mask_add = true });
try testMask(linux.IN.ISDIR, Mask{ .isdir = true });
try testMask(linux.IN.ONESHOT, Mask{ .oneshot = true });
}
};
pub const Event = struct {
wd: u32,
mask: Mask,
cookie: u32,
name: ?[*:0]const u8,
};
pub const max_event_size = @sizeOf(linux.inotify_event) + linux.NAME_MAX + 1;
pub fn init(buf: []u8) !INotify {
const fd = linux.inotify_init1(linux.IN.NONBLOCK);
if (linux.errno(fd) != .SUCCESS) {
return error.InotifyInit;
}
return INotify{
.fd = @intCast(fd),
.buf = buf,
.buf_offset = 0,
.buf_used = 0,
};
}
pub fn add(self: INotify, path: [*:0]const u8, mask: Mask) !u32 {
const wd = linux.inotify_add_watch(
self.fd,
path,
@bitCast(mask)
);
switch (linux.errno(wd)) {
.SUCCESS => return @intCast(wd),
.ACCES => return error.AccessDenied,
.BADF => unreachable, // The given file descriptor is not valid.
.EXIST => return error.MarkAlreadyExists,
.FAULT => unreachable, // path points outside of the process's accessible address space.
.INVAL => unreachable, // The given event mask contains no valid events;
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOSPC => return error.UserMarkQuotaExceeded,
.NOTDIR => return error.NotDir,
else => |err| return std.posix.unexpectedErrno(err),
}
}
pub fn remove(self: INotify, wd: u32) !void {
const rc = linux.inotify_rm_watch(self.fd, @bitCast(wd));
if (linux.errno(rc) != .SUCCESS) {
return error.InotifyRemoveWatch;
}
}
pub fn next(self: *INotify) !?Event {
if (self.buf_used <= self.buf.len) {
const n = std.posix.read(self.fd, self.buf[self.buf_used..]) catch |err| switch(err) {
error.WouldBlock => 0,
else => return err
};
if (n > 0) {
self.buf_used += n;
}
}
if (self.buf_offset < self.buf_used) {
const event = std.mem.bytesAsValue(
linux.inotify_event,
self.buf[self.buf_offset..][0..@sizeOf(linux.inotify_event)]
);
self.buf_offset += @sizeOf(linux.inotify_event) + event.len;
assert(self.buf_offset <= self.buf_used);
if (self.buf_used == self.buf_offset) {
self.buf_offset = 0;
self.buf_used = 0;
}
var name: ?[*:0]const u8 = null;
if (event.len > 0) {
name = std.mem.span(@as([*:0]const u8, @ptrCast(event)) + @sizeOf(linux.inotify_event));
}
return Event{
.wd = @bitCast(event.wd),
.mask = @bitCast(event.mask),
.cookie = event.cookie,
.name = name
};
}
return null;
}
pub fn deinit(self: INotify) void {
_ = linux.close(self.fd);
}

238
src/input.zig Normal file
View File

@ -0,0 +1,238 @@
const std = @import("std");
const assert = std.debug.assert;
const sokol = @import("sokol");
const Math = @import("math");
const Vec2 = Math.Vec2;
const Input = @This();
key_code_mapping: std.EnumMap(KeyCode, u21),
window_size: Vec2,
focused: bool,
keyboard: KeyStateType(KeyCode),
mouse_buttons: KeyStateType(MouseButton),
mouse_position: ?Vec2,
mouse_scroll: Vec2,
pub fn init(window_size: Vec2) Input {
return Input{
.key_code_mapping = .{},
.focused = false,
.keyboard = .empty,
.mouse_buttons = .empty,
.mouse_position = null,
.mouse_scroll = .init(0, 0),
.window_size = window_size
};
}
pub fn isKeyPressed(self: Input, key: KeyCode) bool {
return self.keyboard.pressed.contains(key);
}
pub fn isKeyReleased(self: Input, key: KeyCode) bool {
return self.keyboard.released.contains(key);
}
pub fn isKeyDown(self: Input, key: KeyCode) bool {
return self.keyboard.down.contains(key);
}
pub fn isMousePressed(self: Input, button: MouseButton) bool {
return self.mouse_buttons.pressed.contains(button);
}
pub fn iMouseReleased(self: Input, button: MouseButton) bool {
return self.mouse_buttons.released.contains(button);
}
pub fn isMouseDown(self: Input, button: MouseButton) bool {
return self.mouse_buttons.down.contains(button);
}
pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) {
SPACE = 32,
APOSTROPHE = 39,
COMMA = 44,
MINUS = 45,
PERIOD = 46,
SLASH = 47,
_0 = 48,
_1 = 49,
_2 = 50,
_3 = 51,
_4 = 52,
_5 = 53,
_6 = 54,
_7 = 55,
_8 = 56,
_9 = 57,
SEMICOLON = 59,
EQUAL = 61,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
LEFT_BRACKET = 91,
BACKSLASH = 92,
RIGHT_BRACKET = 93,
GRAVE_ACCENT = 96,
WORLD_1 = 161,
WORLD_2 = 162,
ESCAPE = 256,
ENTER = 257,
TAB = 258,
BACKSPACE = 259,
INSERT = 260,
DELETE = 261,
RIGHT = 262,
LEFT = 263,
DOWN = 264,
UP = 265,
PAGE_UP = 266,
PAGE_DOWN = 267,
HOME = 268,
END = 269,
CAPS_LOCK = 280,
SCROLL_LOCK = 281,
NUM_LOCK = 282,
PRINT_SCREEN = 283,
PAUSE = 284,
F1 = 290,
F2 = 291,
F3 = 292,
F4 = 293,
F5 = 294,
F6 = 295,
F7 = 296,
F8 = 297,
F9 = 298,
F10 = 299,
F11 = 300,
F12 = 301,
F13 = 302,
F14 = 303,
F15 = 304,
F16 = 305,
F17 = 306,
F18 = 307,
F19 = 308,
F20 = 309,
F21 = 310,
F22 = 311,
F23 = 312,
F24 = 313,
F25 = 314,
KP_0 = 320,
KP_1 = 321,
KP_2 = 322,
KP_3 = 323,
KP_4 = 324,
KP_5 = 325,
KP_6 = 326,
KP_7 = 327,
KP_8 = 328,
KP_9 = 329,
KP_DECIMAL = 330,
KP_DIVIDE = 331,
KP_MULTIPLY = 332,
KP_SUBTRACT = 333,
KP_ADD = 334,
KP_ENTER = 335,
KP_EQUAL = 336,
LEFT_SHIFT = 340,
LEFT_CONTROL = 341,
LEFT_ALT = 342,
LEFT_SUPER = 343,
RIGHT_SHIFT = 344,
RIGHT_CONTROL = 345,
RIGHT_ALT = 346,
RIGHT_SUPER = 347,
MENU = 348,
};
pub const MouseButton = enum {
left,
right,
middle,
pub fn fromSokol(mouse_button: sokol.app.Mousebutton) ?MouseButton {
return switch(mouse_button) {
.LEFT => .left,
.RIGHT => .right,
.MIDDLE => .middle,
else => null
};
}
};
pub const Event = union(enum) {
mouse_pressed: MouseButton,
mouse_released: MouseButton,
mouse_move: Vec2,
mouse_enter: Vec2,
mouse_leave,
mouse_scroll: Vec2,
key_pressed: KeyCode,
key_released: KeyCode,
char: u21,
window_resize: Vec2,
focused,
unfocused,
};
fn KeyStateType(T: type) type {
return struct {
pressed: std.EnumSet(T),
released: std.EnumSet(T),
down: std.EnumSet(T),
pub const empty = @This(){
.pressed = .empty,
.released = .empty,
.down = .empty,
};
pub fn press(self: *@This(), key: T) void {
self.pressed.insert(key);
self.down.insert(key);
}
pub fn release(self: *@This(), key: T) void {
self.released.insert(key);
self.down.remove(key);
}
pub fn releaseAll(self: *@This()) void {
var iter = self.down.iterator();
while (iter.next()) |key| {
self.release(key);
}
}
};
}

25
src/main.zig Normal file
View File

@ -0,0 +1,25 @@
const std = @import("std");
const Platform = @import("platform.zig");
const App = @import("app.zig");
pub const std_options: std.Options = .{
.log_scope_levels = &.{
.{ .scope = .graphics, .level = .info }
}
};
pub fn main(init: std.process.Init) void {
Platform.run(App, .{
.init = init,
.window_title = "Gaem",
.width = 800,
.height = 600,
});
}
test {
std.testing.refAllDecls(@This());
_ = @import("./slot_map.zig");
}

447
src/math.zig Normal file
View File

@ -0,0 +1,447 @@
const std = @import("std");
const assert = std.debug.assert;
pub const bytes_per_kib = 1024;
pub const bytes_per_mib = bytes_per_kib * 1024;
pub const bytes_per_gib = bytes_per_mib * 1024;
pub const bytes_per_kb = 1000;
pub const bytes_per_mb = bytes_per_kb * 1000;
pub const bytes_per_gb = bytes_per_mb * 1000;
pub const Range = struct {
from: f32,
to: f32,
pub const zero = init(0, 0);
pub fn init(from: f32, to: f32) Range {
return Range{
.from = from,
.to = to
};
}
pub fn getSize(self: Range) f32 {
return @abs(self.from - self.to);
}
pub fn random(self: Range, rng: std.Random) f32 {
return self.from + rng.float(f32) * (self.to - self.from);
}
};
pub const Vec2 = extern struct {
x: f32,
y: f32,
pub const zero = init(0, 0);
pub fn init(x: f32, y: f32) Vec2 {
return Vec2{
.x = x,
.y = y,
};
}
pub fn initFromInt(T: type, x: T, y: T) Vec2 {
return .init(@floatFromInt(x), @floatFromInt(y));
}
pub fn initAngle(angle: f32) Vec2 {
return Vec2{
.x = @cos(angle),
.y = @sin(angle),
};
}
pub fn rotateLeft90(self: Vec2) Vec2 {
return Vec2.init(self.y, -self.x);
}
pub fn rotateRight90(self: Vec2) Vec2 {
return Vec2.init(-self.y, self.x);
}
pub fn rotate(self: Vec2, angle: f32) Vec2 {
return init(
@cos(angle) * self.x - @sin(angle) * self.y,
@sin(angle) * self.x + @cos(angle) * self.y,
);
}
pub fn rotateAround(self: Vec2, angle: f32, origin: Vec2) Vec2 {
return self.sub(origin).rotate(angle).add(origin);
}
pub fn getAngle(self: Vec2) f32 {
return std.math.atan2(self.y, self.x);
}
pub fn flip(self: Vec2) Vec2 {
return Vec2.init(-self.x, -self.y);
}
pub fn add(self: Vec2, other: Vec2) Vec2 {
return Vec2.init(
self.x + other.x,
self.y + other.y,
);
}
pub fn sub(self: Vec2, other: Vec2) Vec2 {
return Vec2.init(
self.x - other.x,
self.y - other.y,
);
}
pub fn multiplyScalar(self: Vec2, value: f32) Vec2 {
return Vec2.init(
self.x * value,
self.y * value,
);
}
pub fn multiply(self: Vec2, other: Vec2) Vec2 {
return Vec2.init(
self.x * other.x,
self.y * other.y,
);
}
pub fn divide(self: Vec2, other: Vec2) Vec2 {
return Vec2.init(
self.x / other.x,
self.y / other.y,
);
}
pub fn divideScalar(self: Vec2, value: f32) Vec2 {
return Vec2.init(
self.x / value,
self.y / value,
);
}
pub fn lengthSqr(self: Vec2) f32 {
return self.x*self.x + self.y*self.y;
}
pub fn length(self: Vec2) f32 {
return @sqrt(self.lengthSqr());
}
pub fn distance(self: Vec2, other: Vec2) f32 {
return self.sub(other).length();
}
pub fn distanceSqr(self: Vec2, other: Vec2) f32 {
return self.sub(other).lengthSqr();
}
pub fn limitLength(self: Vec2, max_length: f32) Vec2 {
const self_length = self.length();
if (self_length > max_length) {
if (self_length == 0) {
return Vec2.init(0, 0);
}
return self.divideScalar(self_length / max_length);
} else {
return self;
}
}
pub fn normalized(self: Vec2) Vec2 {
const self_length = self.length();
if (self_length == 0) {
return Vec2.init(0, 0);
}
return self.divideScalar(self_length);
}
pub fn initScalar(value: f32) Vec2 {
return Vec2.init(value, value);
}
pub fn eql(self: Vec2, other: Vec2) bool {
return self.x == other.x and self.y == other.y;
}
pub fn format(self: Vec2, writer: *std.io.Writer) std.io.Writer.Error!void {
try writer.print("Vec2{{ {d}, {d} }}", .{ self.x, self.y });
}
};
pub const Vec3 = extern struct {
x: f32, y: f32, z: f32,
pub const zero = init(0, 0, 0);
pub fn init(x: f32, y: f32, z: f32) Vec3 {
return Vec3{
.x = x,
.y = y,
.z = z,
};
}
pub fn initScalar(value: f32) Vec3 {
return Vec3.init(value, value, value);
}
pub fn asArray(self: *Vec3) []f32 {
const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(self))));
return ptr[0..3];
}
pub fn lerp(a: Vec3, b: Vec3, t: f32) Vec3 {
return Vec3.init(
std.math.lerp(a.x, b.x, t),
std.math.lerp(a.y, b.y, t),
std.math.lerp(a.z, b.z, t),
);
}
pub fn clamp(self: Vec3, min_value: f32, max_value: f32) Vec3 {
return Vec3.init(
std.math.clamp(self.x, min_value, max_value),
std.math.clamp(self.y, min_value, max_value),
std.math.clamp(self.z, min_value, max_value),
);
}
};
pub const Vec4 = extern struct {
x: f32, y: f32, z: f32, w: f32,
pub const zero = init(0, 0, 0, 0);
pub fn init(x: f32, y: f32, z: f32, w: f32) Vec4 {
return Vec4{
.x = x,
.y = y,
.z = z,
.w = w
};
}
pub fn initVec3XYZ(vec3: Vec3, w: f32) Vec4 {
return init(vec3.x, vec3.y, vec3.z, w);
}
pub fn initScalar(value: f32) Vec4 {
return Vec4.init(value, value, value, value);
}
pub fn multiplyMat4(left: Vec4, right: Mat4) Vec4 {
var result: Vec4 = undefined;
// TODO: SIMD
result.x = left.x * right.columns[0][0];
result.y = left.x * right.columns[0][1];
result.z = left.x * right.columns[0][2];
result.w = left.x * right.columns[0][3];
result.x += left.y * right.columns[1][0];
result.y += left.y * right.columns[1][1];
result.z += left.y * right.columns[1][2];
result.w += left.y * right.columns[1][3];
result.x += left.z * right.columns[2][0];
result.y += left.z * right.columns[2][1];
result.z += left.z * right.columns[2][2];
result.w += left.z * right.columns[2][3];
result.x += left.w * right.columns[3][0];
result.y += left.w * right.columns[3][1];
result.z += left.w * right.columns[3][2];
result.w += left.w * right.columns[3][3];
return result;
}
pub fn multiply(left: Vec4, right: Vec4) Vec4 {
return init(
left.x * right.x,
left.y * right.y,
left.z * right.z,
left.w * right.w
);
}
pub fn asArray(self: *Vec4) []f32 {
const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(self))));
return ptr[0..4];
}
pub fn initArray(array: []const f32) Vec4 {
return Vec4.init(array[0], array[1], array[2], array[3]);
}
pub fn lerp(a: Vec4, b: Vec4, t: f32) Vec4 {
return Vec4.init(
std.math.lerp(a.x, b.x, t),
std.math.lerp(a.y, b.y, t),
std.math.lerp(a.z, b.z, t),
std.math.lerp(a.w, b.w, t),
);
}
pub fn clamp(self: Vec4, min_value: f32, max_value: f32) Vec4 {
return Vec4.init(
std.math.clamp(self.x, min_value, max_value),
std.math.clamp(self.y, min_value, max_value),
std.math.clamp(self.z, min_value, max_value),
std.math.clamp(self.w, min_value, max_value),
);
}
pub fn toVec3XYZ(self: Vec4) Vec3 {
return Vec3.init(self.x, self.y, self.z);
}
};
pub const Mat4 = extern struct {
columns: [4][4]f32,
pub fn initZero() Mat4 {
var self: Mat4 = undefined;
@memset(self.asArray(), 0);
return self;
}
pub fn initIdentity() Mat4 {
return Mat4.initDiagonal(1);
}
pub fn initDiagonal(value: f32) Mat4 {
var self = Mat4.initZero();
self.columns[0][0] = value;
self.columns[1][1] = value;
self.columns[2][2] = value;
self.columns[3][3] = value;
return self;
}
pub fn multiply(left: Mat4, right: Mat4) Mat4 {
var self: Mat4 = undefined;
inline for (.{ 0, 1, 2, 3 }) |i| {
var column = Vec4.initArray(&right.columns[i]).multiplyMat4(left);
@memcpy(&self.columns[i], column.asArray());
}
return self;
}
pub fn initScale(scale: Vec3) Mat4 {
var self = Mat4.initIdentity();
self.columns[0][0] = scale.x;
self.columns[1][1] = scale.y;
self.columns[2][2] = scale.z;
return self;
}
pub fn initTranslate(offset: Vec3) Mat4 {
var self = Mat4.initIdentity();
self.columns[3][0] = offset.x;
self.columns[3][1] = offset.y;
self.columns[3][2] = offset.z;
return self;
}
pub fn asArray(self: *Mat4) []f32 {
const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(&self.columns))));
return ptr[0..16];
}
};
pub const Rect = struct {
pos: Vec2,
size: Vec2,
pub const zero = Rect{
.pos = Vec2.zero,
.size = Vec2.zero
};
pub const unit = Rect{
.pos = Vec2.zero,
.size = Vec2.init(1, 1)
};
pub fn init(x: f32, y: f32, width: f32, height: f32) Rect {
return Rect{
.pos = Vec2.init(x, y),
.size = Vec2.init(width, height)
};
}
pub fn clip(self: Rect, other: Rect) Rect {
const left_edge = @max(self.left(), other.left());
const right_edge = @min(self.right(), other.right());
const top_edge = @max(self.top(), other.top());
const bottom_edge = @min(self.bottom(), other.bottom());
return Rect.init(
left_edge,
top_edge,
right_edge - left_edge,
bottom_edge - top_edge
);
}
pub fn left(self: Rect) f32 {
return self.pos.x;
}
pub fn right(self: Rect) f32 {
return self.pos.x + self.size.x;
}
pub fn top(self: Rect) f32 {
return self.pos.y;
}
pub fn bottom(self: Rect) f32 {
return self.pos.y + self.size.y;
}
pub fn center(self: Rect) Vec2 {
return self.pos.add(self.size.multiplyScalar(0.5));
}
pub fn multiply(self: Rect, xy: Vec2) Rect {
return Rect{
.pos = self.pos.multiply(xy),
.size = self.size.multiply(xy),
};
}
pub fn divide(self: Rect, xy: Vec2) Rect {
return Rect{
.pos = self.pos.divide(xy),
.size = self.size.divide(xy),
};
}
pub fn isInside(self: Rect, pos: Vec2) bool {
const x_overlap = self.pos.x <= pos.x and pos.x < self.pos.x + self.size.x;
const y_overlap = self.pos.y <= pos.y and pos.y < self.pos.y + self.size.y;
return x_overlap and y_overlap;
}
};
pub const Line = struct {
p0: Vec2,
p1: Vec2
};
pub fn isInsideRect(rect_pos: Vec2, rect_size: Vec2, pos: Vec2) bool {
const rect = Rect{
.pos = rect_pos,
.size = rect_size
};
return rect.isInside(pos);
}

674
src/platform.zig Normal file
View File

@ -0,0 +1,674 @@
const builtin = @import("builtin");
const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.platform);
const assert = std.debug.assert;
const build_options = @import("build_options");
const sokol = @import("sokol");
const sapp = sokol.app;
const sg = sokol.gfx;
const sglue = sokol.glue;
const sgl = sokol.gl;
pub const Math = @import("math");
pub const Vec2 = Math.Vec2;
pub const Vec4 = Math.Vec4;
pub const Mat4 = Math.Mat4;
pub const Rect = Math.Rect;
const tracy = @import("tracy");
pub const Color = @import("./color.zig");
const shd = @import("shader");
pub const ImGUI = @import("./imgui.zig");
pub const Gfx = @import("./graphics.zig");
pub const Input = @import("./input.zig");
const EmbeddedAssets = @import("embedded_assets");
fn PlatformType(App: type) type {
return struct {
const Self = @This();
gpa: Allocator,
arena: *std.heap.ArenaAllocator,
io: Io,
cli_args: []const [:0]const u8,
app: App,
started_at: Io.Timestamp,
last_frame_at: Io.Timestamp,
assets: Assets,
input: Input,
events_buffer: [256]Input.Event,
events_overflow: bool,
events: std.ArrayList(Input.Event),
last_key_press_is_repeat: bool,
last_key_pressed: ?Input.KeyCode,
frame_arena: std.heap.ArenaAllocator,
show_imgui: bool,
fn init(self: *Self) !void {
const window_size = Vec2.init(sapp.widthf(), sapp.heightf());
self.started_at = Io.Timestamp.now(self.io, .awake);
self.last_frame_at = self.started_at;
self.input = Input.init(window_size);
try Gfx.init(self.gpa, .{
.func = sokolLogCallback
});
ImGUI.init(.{
.func = sokolLogCallback
});
if (@hasDecl(App, "init")) {
const plt = Init{
.gpa = self.gpa,
.arena = self.arena.allocator(),
.io = self.io,
.assets = &self.assets
};
if (try self.app.init(plt)) |exit_code| {
std.process.exit(exit_code);
}
}
}
fn sokolInit(user_data: ?*anyopaque) callconv(.c) void {
const self: *Self = @ptrCast(@alignCast(user_data));
self.init() catch |err| {
log.err("init() error: {}", .{err});
if (@errorReturnTrace()) |trace| {
std.debug.dumpErrorReturnTrace(trace);
}
sapp.quit();
};
}
fn frame(self: *Self) !void {
tracy.frameMark();
_ = self.frame_arena.reset(.free_all); // TODO: make arena reset mode configurable
const now = Io.Timestamp.now(self.io, .awake);
const t = self.started_at.durationTo(now);
const dt = self.last_frame_at.durationTo(now);
self.last_frame_at = now;
if (self.input.keyboard.pressed.contains(.F4)) {
self.show_imgui = !self.show_imgui;
}
{
Gfx.beginFrame();
ImGUI.beginFrame(self.show_imgui, self.frame_arena.allocator());
if (@hasDecl(App, "frame")) {
const plt = Frame{
.t = t.nanoseconds,
.dt = dt.nanoseconds,
.gpa = self.gpa,
.arena = self.arena.allocator(),
.io = self.io,
.input = &self.input,
.input_events = self.events.items,
.frame = self.frame_arena.allocator(),
.assets = &self.assets
};
try self.app.frame(plt);
}
Gfx.flush(.{});
Gfx.showDebug();
ImGUI.endFrame();
Gfx.endFrame();
}
self.events_overflow = false;
self.events.clearRetainingCapacity();
self.input.keyboard.pressed = .empty;
self.input.keyboard.released = .empty;
self.input.mouse_buttons.pressed = .empty;
self.input.mouse_buttons.released = .empty;
}
fn sokolFrame(user_data: ?*anyopaque) callconv(.c) void {
const self: *Self = @ptrCast(@alignCast(user_data));
self.frame() catch |err| {
log.err("frame() error: {}", .{err});
if (@errorReturnTrace()) |trace| {
std.debug.dumpErrorReturnTrace(trace);
}
sapp.quit();
};
}
// TODO: I don't like the names of these 'event' related functions.
// All of them "append an event", but they do different things.
fn applyEventToState(self: *Self, e: Input.Event) void {
const input = &self.input;
switch (e) {
.key_pressed => |key| {
assert(!input.isKeyDown(key));
input.keyboard.press(key);
self.last_key_pressed = key;
},
.key_released => |key| {
assert(input.isKeyDown(key));
input.keyboard.release(key);
},
.mouse_pressed => |button| {
assert(input.mouse_position != null);
assert(!input.isMouseDown(button));
input.mouse_buttons.press(button);
},
.mouse_released => |button| {
assert(input.mouse_position != null);
assert(input.isMouseDown(button));
input.mouse_buttons.release(button);
},
.mouse_enter => |pos| {
assert(input.mouse_position == null);
input.mouse_position = pos;
},
.mouse_move => |pos| {
assert(input.mouse_position != null);
input.mouse_position = pos;
},
.mouse_leave => {
assert(input.mouse_position != null);
input.mouse_position = null;
},
.char => |char| {
// A 'key_pressed' event always occurs before a 'char' event.
// This allows us to differentiate between keycodes and scan codes.
// And how to map key codes to character codes.
assert(self.last_key_pressed != null);
input.key_code_mapping.put(self.last_key_pressed.?, char);
self.last_key_pressed = null;
},
.mouse_scroll => |offset| {
assert(input.mouse_position != null);
input.mouse_scroll = input.mouse_scroll.add(offset);
},
.window_resize => |window_size| {
self.input.window_size = window_size;
},
.focused => {
assert(!input.focused);
input.focused = true;
},
.unfocused => {
assert(input.focused);
assert(input.mouse_position == null);
assert(input.keyboard.down.eql(.empty));
assert(input.mouse_buttons.down.eql(.empty));
input.focused = false;
}
}
}
fn appendEvent(self: *Self, e: Input.Event) void {
if (self.events.capacity == self.events.items.len) {
if (!self.events_overflow) {
log.warn("too many events, limit: {}", .{self.events.capacity});
}
self.events_overflow = true;
return;
}
self.events.appendAssumeCapacity(e);
self.applyEventToState(e);
}
fn pushEvent(self: *Self, e: Input.Event) void {
const input = &self.input;
if (input.focused) {
if (e == .focused) {
return;
}
if (e == .key_pressed and input.isKeyDown(e.key_pressed)) {
return;
}
if (e == .key_released and !input.isKeyDown(e.key_released)) {
return;
}
if (e == .mouse_move and input.mouse_position.?.eql(e.mouse_move)) {
return;
}
if (e == .unfocused) {
var key_iter = input.keyboard.down.iterator();
while (key_iter.next()) |key| {
self.appendEvent(.{ .key_released = key });
}
var mouse_buttons_iter = input.mouse_buttons.down.iterator();
while (mouse_buttons_iter.next()) |button| {
self.appendEvent(.{ .mouse_released = button });
}
if (input.mouse_position != null) {
self.appendEvent(.{ .mouse_leave = {} });
}
}
} else {
if (e == .unfocused) {
return;
}
if (e != .focused) {
self.appendEvent(.focused);
if (e == .mouse_move) {
self.appendEvent(.{ .mouse_enter = e.mouse_move });
}
}
}
self.appendEvent(e);
}
fn event(self: *Self, e: sapp.Event) !bool {
if (self.show_imgui) {
if (ImGUI.event(e)) {
self.pushEvent(.{
.unfocused = {}
});
return true;
}
}
const input = &self.input;
switch (e.type) {
.MOUSE_DOWN => {
if (input.mouse_position == null) {
self.pushEvent(.{
.mouse_enter = .init(e.mouse_x, e.mouse_y)
});
}
self.pushEvent(.{
.mouse_pressed = Input.MouseButton.fromSokol(e.mouse_button) orelse return false,
});
},
.MOUSE_UP => {
if (input.mouse_position == null) {
self.pushEvent(.{
.mouse_enter = .init(e.mouse_x, e.mouse_y)
});
}
self.pushEvent(.{
.mouse_released = Input.MouseButton.fromSokol(e.mouse_button) orelse return false,
});
},
.MOUSE_MOVE => {
if (input.mouse_position == null) {
self.pushEvent(.{
.mouse_enter = .init(e.mouse_x, e.mouse_y)
});
}
self.pushEvent(.{
.mouse_move = .init(e.mouse_x, e.mouse_y)
});
},
.MOUSE_ENTER => {
self.pushEvent(.{
.mouse_enter = .init(e.mouse_x, e.mouse_y)
});
},
.MOUSE_LEAVE => {
self.pushEvent(.{
.mouse_leave = {}
});
},
.MOUSE_SCROLL => {
self.pushEvent(.{
.mouse_scroll = .init(e.scroll_x, e.scroll_y)
});
},
.KEY_DOWN => {
self.last_key_press_is_repeat = e.key_repeat;
if (!e.key_repeat) {
self.pushEvent(.{
.key_pressed = @enumFromInt(@intFromEnum(e.key_code))
});
}
},
.KEY_UP => {
self.pushEvent(.{
.key_released = @enumFromInt(@intFromEnum(e.key_code)),
});
},
.CHAR => {
if (!self.last_key_press_is_repeat) {
self.pushEvent(.{
.char = @intCast(e.char_code)
});
}
},
.FOCUSED => {
self.pushEvent(.{
.focused = {}
});
},
.UNFOCUSED => {
self.pushEvent(.{
.unfocused = {}
});
},
else => {}
}
return true;
}
fn sokolEvent(ev: [*c]const sapp.Event, user_data: ?*anyopaque) callconv(.c) void {
const self: *Self = @ptrCast(@alignCast(user_data));
const consumed_event = self.event(ev.*) catch |err| blk: {
log.err("event() error: {}", .{err});
if (@errorReturnTrace()) |trace| {
std.debug.dumpErrorReturnTrace(trace);
}
sapp.quit();
break :blk false;
};
if (consumed_event) {
sapp.consumeEvent();
}
}
fn sokolCleanup(user_data: ?*anyopaque) callconv(.c) void {
const self: *Self = @ptrCast(@alignCast(user_data));
if (@hasDecl(App, "deinit")) {
const plt = Deinit{
.gpa = self.gpa,
.io = self.io
};
self.app.deinit(plt);
}
self.frame_arena.deinit();
self.assets.deinit();
ImGUI.deinit();
Gfx.deinit();
}
};
}
pub const RunOptions = struct {
init: std.process.Init,
window_title: [*c]const u8 = null,
width: i32 = 800,
height: i32 = 600,
};
pub const Assets = struct {
arena: std.heap.ArenaAllocator,
gpa: Allocator,
io: Io,
dir: ?Io.Dir,
files: std.array_hash_map.String(File),
fn init(gpa: Allocator, io: Io, dir: ?Io.Dir) Assets {
return Assets{
.gpa = gpa,
.io = io,
.arena = .init(gpa),
.dir = dir,
.files = .empty
};
}
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.readFileAllocOptions(
self.io,
filename,
arena,
.unlimited,
.of(u8),
0
);
const filename_owned = try arena.dupe(u8, filename);
try self.files.put(self.gpa, filename_owned, File{
.contents = contents
});
return contents;
}
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 which isn't defined in assets: '{s}'", .{filename});
return contents;
} else {
log.err("Attempt to read file '{s}', which doesn't exist", .{filename});
return "";
}
} else |err| {
log.err("Failed to read file from filesystem: {}", .{err});
return "";
}
}
}
fn insertStaticFile(self: *Assets, filename: []const u8, contents: [:0]const u8) !void {
try self.files.put(self.gpa, filename, File{
.contents = contents
});
}
fn deinit(self: *Assets) void {
self.arena.deinit();
self.files.deinit(self.gpa);
}
const File = struct {
contents: [:0]const u8
};
};
pub const Init = struct {
gpa: Allocator,
arena: Allocator,
io: Io,
assets: *Assets
};
pub const Deinit = struct {
gpa: Allocator,
io: Io,
};
pub const Nanoseconds = i96;
pub const Frame = struct {
t: Nanoseconds,
dt: Nanoseconds,
gpa: Allocator,
arena: Allocator,
frame: Allocator,
io: Io,
assets: *Assets,
input: *Input,
input_events: []Input.Event,
fn nanosecondsToSeconds(nanoseconds: i96) f32 {
return @floatCast(@as(f64, @floatFromInt(nanoseconds)) / std.time.ns_per_s);
}
pub fn deltaTime(self: Frame) f32 {
return nanosecondsToSeconds(self.dt);
}
pub fn time(self: Frame) f32 {
return nanosecondsToSeconds(self.t);
}
};
pub fn run(App: type, opts: RunOptions) void {
const arena = opts.init.arena.allocator();
const gpa = opts.init.gpa;
const io = opts.init.io;
var assets_dir: ?Io.Dir = null;
if (build_options.assets_dir) |assets_dir_path| {
const cwd = Io.Dir.cwd();
assets_dir = cwd.openDir(io, assets_dir_path, .{}) catch |e| blk: {
log.err("Failed to open assets directory: {}", .{e});
break :blk null;
};
}
const PlatformApp = PlatformType(App);
const plt = arena.create(PlatformApp) catch @panic("OOM");
plt.* = PlatformApp{
.gpa = gpa,
.arena = opts.init.arena,
.io = io,
.cli_args = opts.init.minimal.args.toSlice(arena) catch @panic("OOM"),
.app = undefined,
.input = undefined,
.events_buffer = undefined,
.events_overflow = false,
.events = .initBuffer(&plt.events_buffer),
.started_at = undefined,
.last_frame_at = undefined,
.last_key_press_is_repeat = false,
.last_key_pressed = null,
.frame_arena = .init(gpa),
.show_imgui = builtin.mode == .Debug,
.assets = .init(gpa, io, assets_dir)
};
inline for (EmbeddedAssets.files) |file| {
plt.assets.insertStaticFile(file.name, file.contents) catch @panic("OOM");
}
var icon: sapp.IconDesc = .{};
icon.sokol_default = true;
// 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();
// 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
// }
// };
tracy.setThreadName("Main");
const is_wasm = builtin.cpu.arch.isWasm();
if (builtin.os.tag == .linux and !is_wasm) {
var sa: std.posix.Sigaction = .{
.handler = .{ .handler = posixSignalHandler },
.mask = std.posix.sigemptyset(),
.flags = std.posix.SA.RESTART,
};
std.posix.sigaction(std.posix.SIG.INT, &sa, null);
}
sapp.run(.{
.init_userdata_cb = PlatformApp.sokolInit,
.frame_userdata_cb = PlatformApp.sokolFrame,
.cleanup_userdata_cb = PlatformApp.sokolCleanup,
.event_userdata_cb = PlatformApp.sokolEvent,
.user_data = plt,
.window_title = opts.window_title,
.width = opts.width,
.height = opts.height,
.icon = icon,
.logger = .{ .func = sokolLogCallback },
});
}
fn posixSignalHandler(sig: std.posix.SIG) callconv(.c) void {
_ = sig;
sapp.requestQuit();
}
fn toSokolColor(color: Color) sokol.gfx.Color {
// TODO: Assert and do cast
return sokol.gfx.Color{
.r = color.r,
.g = color.g,
.b = color.b,
.a = color.a,
};
}
fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void {
if (log_level == 0) {
log.err(format, args);
} else if (log_level == 1) {
log.err(format, args);
} else if (log_level == 2) {
log.warn(format, args);
} else {
log.info(format, args);
}
}
fn cStrToZig(c_str: [*c]const u8) [:0]const u8 {
return std.mem.span(c_str);
}
fn sokolLogCallback(tag: [*c]const u8, log_level: u32, log_item: u32, message: [*c]const u8, line_nr: u32, filename: [*c]const u8, user_data: ?*anyopaque) callconv(.c) void {
_ = user_data;
if (filename != null) {
sokolLogFmt(
log_level,
"[{s}][id:{}] {s}:{}: {s}",
.{
cStrToZig(tag orelse "-"),
log_item,
std.fs.path.basename(cStrToZig(filename orelse "-")),
line_nr,
cStrToZig(message orelse "")
}
);
} else {
sokolLogFmt(
log_level,
"[{s}][id:{}] {s}",
.{
cStrToZig(tag orelse "-"),
log_item,
cStrToZig(message orelse "")
}
);
}
}

50
src/shader.glsl Normal file
View File

@ -0,0 +1,50 @@
@header const m = @import("math")
@ctype mat4 m.Mat4
/* main vertex shader */
@vs vs
in vec2 position;
in vec2 texcoord0;
in vec4 color0;
out vec4 color;
out vec4 uv;
layout(binding = 0) uniform vs_params {
mat4 view;
mat4 projection;
};
void main() {
gl_Position = projection * view * vec4(position, 0, 1);
color = color0;
uv = vec4(texcoord0, 0.0, 1.0);
}
@end
/* main fragment shader */
@fs fs
layout(binding=0) uniform texture2D tex;
layout(binding=0) uniform sampler smp;
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() {
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
/* main shader program */
@program main vs fs

305
src/slot_map.zig Normal file
View File

@ -0,0 +1,305 @@
const std = @import("std");
const tracy = @import("tracy");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
/// This array provides:
/// - O(1) insertion
/// - O(1) removal
/// - O(1) lookup
/// - Pointers and IDs stay stable when inserting and removing.
/// - Unique IDs for every inserted item (assuming that generation doesn't overflow)
///
/// For more details about this data structure checkout this podcast episode from Wookash:
/// * https://www.youtube.com/watch?v=ShSGHb65f3M
pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
assert(@typeInfo(Generation) == .int);
assert(@typeInfo(Index) == .int);
return struct {
const Self = @This();
// TODO: This struct will probably have paddings bytes inserted.
// It's not ideal for a data structure like this which will be commonly used.
//
// The `used` field could be packed into a bitset.
// But that would introduce a second `std.ArrayList` which would make managing the memory a PITA.
//
// So in the end idk if it's worth it.
pub const Slot = struct {
value: Value,
generation: Generation,
next_hole: ?Index,
used: bool
};
slots: std.ArrayList(Slot),
first_hole: ?Index,
last_hole: ?Index,
hole_count: usize,
const empty = Self{
.slots = .empty,
.first_hole = null,
.last_hole = null,
.hole_count = 0,
};
pub const Id = packed struct {
generation: Generation,
index: Index,
pub fn format(self: Id, writer: *std.Io.Writer) std.Io.Writer.Error!void {
try writer.print("Id({s}){{ {}, {} }}", .{ @typeName(Value), self.index, self.generation });
}
};
pub const Iterator = struct {
slot_map: *Self,
index: Index,
pub fn next(self: *Iterator) ?Id {
while (self.index < self.slot_map.slots.items.len) {
const index = self.index;
const slot = self.slot_map.slots.items[index];
self.index += 1;
if (slot.used) {
return Id{
.index = @intCast(index),
.generation = slot.generation
};
}
}
return null;
}
};
pub fn clearRetainingCapacity(self: *Self) void {
var slots = self.slots;
slots.clearRetainingCapacity();
self.* = .init(slots);
}
fn insertHole(self: *Self, index: Index) void {
if (self.last_hole) |last_hole| {
self.slots.items[index].next_hole = last_hole;
self.last_hole = index;
} else {
self.first_hole = index;
self.last_hole = index;
}
self.hole_count += 1;
}
fn removeUnused(self: *Self) ?Index {
if (self.first_hole) |first_hole| {
self.first_hole = self.slots.items[first_hole].next_hole;
if (self.first_hole == null) {
self.last_hole = null;
}
self.hole_count -= 1;
return first_hole;
}
const capacity = @min(self.slots.capacity, std.math.maxInt(Index));
if (self.slots.items.len < capacity) {
const index: Index = @intCast(self.slots.items.len);
self.slots.items.len += 1;
self.slots.items[index] = Slot{
.value = undefined,
.generation = 0,
.next_hole = null,
.used = false
};
return index;
}
return null;
}
pub fn ensureUnusedCapacity(self: *Self, gpa: Allocator, additional_count: usize) Allocator.Error!void {
if (additional_count > self.hole_count) {
const new_capacity, const overflow = @addWithOverflow(
self.slots.capacity,
additional_count - self.hole_count
);
if (overflow != 0) return error.OutOfMemory;
if (new_capacity >= std.math.maxInt(Index)) return error.OutOfMemory;
try self.slots.ensureTotalCapacity(gpa, new_capacity);
}
}
pub fn unusedCapacity(self: *Self) usize {
const capacity = @min(self.slots.capacity, std.math.maxInt(Index));
return capacity - self.slots.items.len + self.hole_count;
}
pub fn insertAssumeCapacity(self: *Self) Id {
const index = self.removeUnused().?;
const slot = &self.slots.items[index];
assert(!slot.used);
slot.used = true;
return Id{
.index = @intCast(index),
.generation = slot.generation
};
}
pub fn insert(self: *Self, gpa: Allocator) Allocator.Error!Id {
try self.ensureUnusedCapacity(gpa, 1);
return self.insertAssumeCapacity();
}
pub fn insertBounded(self: *Self) Allocator.Error!Id {
if (self.unusedCapacity() == 0) {
return error.OutOfMemory;
}
return self.insertAssumeCapacity();
}
pub fn exists(self: *Self, id: Id) bool {
if (id.index >= self.slots.items.len) {
return false;
}
const slot = self.slots.items[id.index];
return slot.used and slot.generation == id.generation;
}
pub fn get(self: *Self, id: Id) ?*Value {
if (self.exists(id)) {
return &self.slots.items[id.index].value;
}
return null;
}
pub fn getAssumeExists(self: *Self, id: Id) *Value {
assert(self.exists(id));
return self.get(id).?;
}
pub fn removeAssumeExists(self: *Self, id: Id) void {
assert(self.exists(id));
const slot = &self.slots.items[id.index];
slot.used = false;
slot.generation +%= 1;
self.insertHole(id.index);
}
pub fn remove(self: *Self, id: Id) bool {
if (!self.exists(id)) {
return false;
}
self.removeAssumeExists(id);
return true;
}
pub fn iterator(self: *Self) Iterator {
return Iterator{
.slot_map = self,
.index = 0
};
}
pub fn init(slots: std.ArrayList(Slot)) Self {
var self: Self = .empty;
self.slots = slots;
return self;
}
pub fn deinit(self: *Self, gpa: Allocator) void {
self.slots.deinit(gpa);
}
};
}
const TestMap = SlotMapType(u24, u8, void);
// TODO: Add more rigorous test for check if generation usage is nicely distributed.
test "insert & remove" {
const expect = std.testing.expect;
const gpa = std.testing.allocator;
var map: TestMap = .empty;
defer map.deinit(gpa);
const id1 = try map.insert(gpa);
try expect(map.exists(id1));
try expect(map.remove(id1));
try expect(!map.exists(id1));
try expect(!map.remove(id1));
const id2 = try map.insert(gpa);
try expect(map.exists(id2));
try expect(!map.exists(id1));
}
test "generation wrap around" {
const expectEqual = std.testing.expectEqual;
const gpa = std.testing.allocator;
var map: TestMap = .empty;
defer map.deinit(gpa);
// Grow array list so that at least 1 slot exists
const id1 = try map.insert(gpa);
map.removeAssumeExists(id1);
// Artificially increase generation count
map.slots.items[id1.index].generation = std.math.maxInt(@FieldType(TestMap.Id, "generation"));
// Check if generation wraps around
const id2 = try map.insert(gpa);
map.removeAssumeExists(id2);
try expectEqual(id1.index, id2.index);
try expectEqual(0, map.slots.items[id1.index].generation);
}
test "iterator" {
const expectEqual = std.testing.expectEqual;
const gpa = std.testing.allocator;
var map: TestMap = .empty;
defer map.deinit(gpa);
// Create array which has a hole
const id1 = try map.insert(gpa);
const id2 = try map.insert(gpa);
const id3 = try map.insert(gpa);
map.removeAssumeExists(id2);
var iter = map.iterator();
try expectEqual(id1, iter.next().?);
try expectEqual(id3, iter.next().?);
try expectEqual(null, iter.next());
}
test "clear retaining capacity" {
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;
const gpa = std.testing.allocator;
var map: TestMap = .empty;
defer map.deinit(gpa);
const id1 = try map.insert(gpa);
try expect(map.exists(id1));
map.clearRetainingCapacity();
const id2 = try map.insert(gpa);
try expect(map.exists(id2));
try expectEqual(id1, id2);
}