add sokol, imgui, tracy

This commit is contained in:
Rokas Puzonas 2026-07-01 00:30:00 +03:00
parent 9217965ec1
commit 6bc644764f
23 changed files with 1902 additions and 230 deletions

1
.gitignore vendored
View File

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

239
build.zig
View File

@ -4,84 +4,201 @@ pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const plugin_dynamic_enabled = b.option(bool, "plugin-dynamic", "Enable loading plugin through a dynamic library") orelse true;
const plugin_static_enabled = b.option(bool, "plugin-static", "Enable loading plugin through a dynamic library") orelse true;
const is_wasm = target.result.cpu.arch.isWasm();
const is_debug = (optimize == .Debug);
const build_options = b.addOptions();
build_options.addOption(bool, "plugin_dynamic", plugin_dynamic_enabled);
build_options.addOption(bool, "plugin_static", plugin_static_enabled);
const plugin_api_mod = b.createModule(.{
.root_source_file = b.path("src/plugin_api.zig"),
});
const has_tracy = b.option(bool, "tracy", "Tracy integration") orelse is_debug;
const has_imgui = b.option(bool, "imgui", "ImGui 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);
exe_mod.addOptions("build_options", build_options);
exe_mod.addImport("plugin_api", plugin_api_mod);
const plugin_mod = b.createModule(.{
.root_source_file = b.path("src/plugin.zig"),
const dep_sokol = b.dependency("sokol", .{
.target = target,
.optimize = optimize
.optimize = optimize,
.with_sokol_imgui = has_imgui
});
plugin_mod.addImport("plugin_api", plugin_api_mod);
if (plugin_static_enabled) {
exe_mod.addImport("plugin", plugin_mod);
}
exe_mod.linkLibrary(dep_sokol.artifact("sokol_clib"));
exe_mod.addImport("sokol", dep_sokol.module("sokol"));
const exe = b.addExecutable(.{
.name = "sokol_template_v2",
.root_module = exe_mod,
});
const plugin_dynamic = b.addLibrary(.{
.name = "plugin",
.linkage = .dynamic,
.root_module = b.createModule(.{
.root_source_file = b.path("src/plugin_dynamic.zig"),
if (has_imgui) {
if (b.lazyDependency("cimgui", .{
.target = target,
.optimize = optimize
})
.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"));
const dep_tracy = b.dependency("tracy", .{
.target = target,
.optimize = optimize,
.tracy_enable = has_tracy,
.tracy_only_localhost = true
});
plugin_dynamic.root_module.addImport("plugin_api", plugin_api_mod);
plugin_dynamic.root_module.addImport("plugin", plugin_mod);
const plugin_install_artifact = b.addInstallArtifact(plugin_dynamic, .{ });
if (plugin_dynamic_enabled) {
exe.step.dependOn(&plugin_install_artifact.step);
if (has_tracy) {
exe_mod.linkLibrary(dep_tracy.artifact("tracy"));
}
exe_mod.addImport("tracy", dep_tracy.module("tracy"));
b.installArtifact(exe);
if (is_wasm) {
// TODO:
} else {
exe_mod.resolved_target = target;
exe_mod.optimize = optimize;
const exe = buildNative(b, .{
.name = "sokol_template_v2",
.root_module = exe_mod
});
b.installArtifact(exe);
{
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (plugin_dynamic_enabled) {
run_cmd.addArg("--plugin-path");
run_cmd.addArg(b.getInstallPath(.lib, plugin_dynamic.out_lib_filename));
}
if (b.args) |args| {
run_cmd.addArgs(args);
{
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
{
const exe_tests = b.addTest(.{ .root_module = exe_mod, });
const run_exe_tests = b.addRunArtifact(exe_tests);
{
const run_step = b.step("build-plugin", "Build plugin");
run_step.dependOn(&plugin_install_artifact.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 test_step = b.step("test", "Run tests");
test_step.dependOn(&run_exe_tests.step);
}
}
}
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("\"");
}
}
};

View File

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

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

@ -0,0 +1,38 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const stb_dependency = b.dependency("stb", .{});
{
const mod_stb_image = b.addModule("stb_image", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/stb_image.zig"),
.link_libc = true,
});
mod_stb_image.addIncludePath(stb_dependency.path("."));
mod_stb_image.addCSourceFile(.{
.file = b.path("src/stb_image_impl.c"),
.flags = &.{}
});
}
{
const mod_stb_image = b.addModule("stb_vorbis", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/stb_vorbis.zig"),
.link_libc = true,
});
mod_stb_image.addIncludePath(stb_dependency.path("."));
mod_stb_image.addCSourceFile(.{
.file = b.path("src/stb_vorbis_impl.c"),
.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"

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"

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();
}

42
src/app.zig Normal file
View File

@ -0,0 +1,42 @@
const CLI = @import("./cli.zig");
const App = @This();
pub fn init(self: *App) !?u8 {
_ = self; // autofix
// var cli: CLI = undefined;
// cli.init(plt.io, plt.gpa);
// defer cli.deinit();
//
// const opt_plugin_path = try cli.addOption(.{
// .kind = .single_argument,
// .long_name = "plugin-path"
// });
//
// var result = cli.parse(plt.gpa, opts.cli_args) catch {
// return 1;
// };
// defer result.deinit();
//
// var cwd = Io.Dir.cwd();
// const root_dir = try cwd.realPathFileAlloc(plt.io, ".", plt.gpa);
// defer plt.gpa.free(root_dir);
//
// var plugin_path: ?[]const u8 = null;
// if (result.getOptionArgument(opt_plugin_path)) |absolute_plugin_path| {
// plugin_path = FileWatcher.toRelativePath(root_dir, absolute_plugin_path);
// if (plugin_path == null) {
// return error.PluginPathOutsideOfProject;
// }
//
// log.debug("Loaded dynamic plugin from: '{s}'", .{plugin_path.?});
// }
}
pub fn frame(self: *App) !void {
_ = self; // autofix
}
pub fn deinit(self: *App) void {
_ = self; // autofix
}

View File

@ -244,14 +244,11 @@ const Parser = struct {
}
};
fn printErrorAndExit(self: *CLI, comptime fmt: []const u8, args: anytype) noreturn {
fn printError(self: *CLI, comptime fmt: []const u8, args: anytype) void {
self.stderr.write("ERROR: " ++ fmt ++ "\n", args);
self.stderr.flush();
std.process.exit(1);
}
// TODO: I don't like the fact that this call `std.process.exit()`
// But for now this is the most convenient thing.
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();
@ -270,7 +267,8 @@ pub fn parse(self: *CLI, gpa: std.mem.Allocator, args: []const []const u8) !Pars
if (option.kind == .single_argument) {
const argument = parser.take() orelse {
self.printErrorAndExit("Missing required argument for '{s}'", .{arg});
self.printError("Missing required argument for '{s}'", .{arg});
return error.MissingArgument;
};
result_value.argument = argument;
@ -280,7 +278,8 @@ pub fn parse(self: *CLI, gpa: std.mem.Allocator, args: []const []const u8) !Pars
continue :next_arg;
}
self.printErrorAndExit("Unknown option '{s}'", .{arg});
self.printError("Unknown option '{s}'", .{arg});
return error.UnknownOption;
}
if (result.command == null) {
@ -291,9 +290,11 @@ pub fn parse(self: *CLI, gpa: std.mem.Allocator, args: []const []const u8) !Pars
}
}
self.printErrorAndExit("Unknown command '{s}'", .{arg});
self.printError("Unknown command '{s}'", .{arg});
return error.UnknownCommand;
} else {
self.printErrorAndExit("Unknown argument '{s}'", .{arg});
self.printError("Unknown argument '{s}'", .{arg});
return error.UnknownArgument;
}
}

39
src/color.zig Normal file
View File

@ -0,0 +1,39 @@
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 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);
}

View File

@ -49,6 +49,23 @@ pub fn init(gpa: Allocator, root_dir: []const u8) !FileWatcher {
};
}
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)) {
@ -111,14 +128,22 @@ fn attemptRegisteringWatch(self: *FileWatcher, watch: *Watch) !void {
};
}
fn isInside(outer_path: []const u8, inner_path: []const u8) bool {
pub fn toRelativePath(outer_path: []const u8, inner_path: []const u8) ?[]const u8 {
if (inner_path.len == outer_path.len) {
return std.mem.eql(u8, inner_path, outer_path);
if (std.mem.eql(u8, inner_path, outer_path)) {
return "";
}
} else if (inner_path.len > outer_path.len) {
return std.mem.startsWith(u8, inner_path, outer_path) and inner_path[outer_path.len] == std.fs.path.sep;
} else {
return false;
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 {
@ -293,20 +318,3 @@ pub fn next(self: *FileWatcher, io: Io) !?[]const u8 {
return null;
}
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);
}

View File

@ -1,93 +1,16 @@
const std = @import("std");
const CLI = @import("cli.zig");
const build_options = @import("build_options");
const builtin = @import("builtin");
const FileWatcher = @import("file_watcher.zig");
const Io = std.Io;
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
// TODO: zig build --watch --color off --summary none
const Platform = @import("platform.zig");
const App = @import("app.zig");
const Plugin = @import("plugin_api").Plugin;
pub fn main(init: std.process.Init) void {
Platform.run(App, .{
.init = init,
pub fn getStaticPlugin() !Plugin {
if (!build_options.plugin_static) {
return error.NotSupported;
}
return @import("plugin").api;
}
const DynamicPlugin = struct {
lib: std.DynLib,
plugin: Plugin,
pub fn init(path: []const u8) !DynamicPlugin {
var lib = try std.DynLib.open(path);
errdefer lib.close();
var plugin: Plugin = undefined;
inline for (@typeInfo(Plugin).@"struct".fields) |field| {
@field(plugin, field.name) = lib.lookup(field.type, field.name) orelse return error.SymbolNotFound;
}
return DynamicPlugin{
.lib = lib,
.plugin = plugin
};
}
pub fn deinit(self: *DynamicPlugin) void {
self.lib.close();
}
};
pub fn main(init: std.process.Init) !void {
// This is appropriate for anything that lives as long as the process.
const arena: std.mem.Allocator = init.arena.allocator();
const gpa = init.gpa;
const io = init.io;
var cli: CLI = undefined;
cli.init(io, init.gpa);
defer cli.deinit();
const opt_plugin_path = try cli.addOption(.{
.kind = .single_argument,
.long_name = "plugin-path"
.window_title = "Gaem",
.width = 800,
.height = 600,
});
const args = try init.minimal.args.toSlice(arena);
var result = try cli.parse(gpa, args);
defer result.deinit();
const plugin_path: ?[]const u8 = result.getOptionArgument(opt_plugin_path);
if (plugin_path != null) {
var foo = try DynamicPlugin.init(plugin_path.?);
defer foo.deinit();
}
var cwd = Io.Dir.cwd();
const root_dir = try cwd.realPathFileAlloc(io, ".", gpa);
defer gpa.free(root_dir);
var file_watcher = try FileWatcher.init(gpa, root_dir);
defer file_watcher.deinit(gpa);
try file_watcher.add(gpa, "test.txt", .fromMilliseconds(150));
try file_watcher.add(gpa, "foo/test.txt", .fromMilliseconds(150));
while (true) {
while (try file_watcher.next(io)) |path| {
std.debug.print("path: {s}\n", .{path});
}
try io.sleep(.fromMilliseconds(100), .real);
}
}
test {

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);
}

286
src/platform.zig Normal file
View File

@ -0,0 +1,286 @@
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 sokol = @import("sokol");
const sapp = sokol.app;
const sg = sokol.gfx;
const sglue = sokol.glue;
const sgl = sokol.gl;
const simgui = sokol.imgui;
const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
const tracy = @import("tracy");
const ig = @import("cimgui");
const Color = @import("./color.zig");
fn PlatformType(App: type) type {
_ = App; // autofix
return struct {
const Self = @This();
pass_action: sg.PassAction = .{},
cli_args: []const [:0]const u8,
show_first_window: bool = true,
fn sokolInit(user_data: ?*anyopaque) callconv(.c) void {
const self: *Self = @ptrCast(@alignCast(user_data));
sg.setup(.{
.environment = sglue.environment(),
.logger = .{ .func = sokolLogCallback },
});
simgui.setup(.{
.logger = .{ .func = sokolLogCallback },
});
if (@hasDecl(ig, "ImGuiConfigFlags_DockingEnable")) {
ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_DockingEnable;
}
ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_ViewportsEnable;
self.pass_action.colors[0] = .{
.load_action = .CLEAR,
.clear_value = toSokolColor(.black)
};
}
fn sokolFrame(user_data: ?*anyopaque) callconv(.c) void {
const self: *Self = @ptrCast(@alignCast(user_data));
tracy.frameMark();
simgui.newFrame(.{
.width = sapp.width(),
.height = sapp.height(),
.delta_time = sapp.frameDuration(),
.dpi_scale = sapp.dpiScale(),
});
//=== UI CODE STARTS HERE
{
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_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);
ig.igSetNextWindowPos(.{ .x = 10, .y = 30 }, ig.ImGuiCond_Once);
ig.igSetNextWindowSize(.{ .x = 400, .y = 100 }, ig.ImGuiCond_Once);
if (ig.igBegin("Hello Dear ImGui!", &self.show_first_window, ig.ImGuiWindowFlags_None)) {
_ = ig.igColorEdit3("Background", &self.pass_action.colors[0].clear_value.r, ig.ImGuiColorEditFlags_None);
_ = ig.igText("Dear ImGui Version: %s", ig.IMGUI_VERSION);
}
ig.igEnd();
ig.igEnd();
}
// TODO:
// if (ig.igBeginMainMenuBar()) {
// ig.igEndMainMenuBar();
// }
//=== UI CODE ENDS HERE
// var frame = Platform.Frame{
// .platform = &self.exposed,
// .input = .{},
// .output = .{},
// };
// if (self.frame(&frame)) {
// // ?
// } else |e| {
// log.err("frame() failed: {}", .{e});
// if (@errorReturnTrace()) |trace| {
// std.debug.dumpErrorReturnTrace(trace);
// }
// self.quit();
// }
// var pass_action: sg.PassAction = .{};
// pass_action.colors[0] = .{
// .load_action = .CLEAR,
// .clear_value = toSokolColor(.black)
// };
sg.beginPass(.{ .action = self.pass_action, .swapchain = sglue.swapchain() });
simgui.render();
sg.endPass();
sg.commit();
}
fn sokolEvent(ev: [*c]const sapp.Event, user_data: ?*anyopaque) callconv(.c) void {
const self: *Self = @ptrCast(@alignCast(user_data));
_ = self; // autofix
// forward input events to sokol-imgui
_ = simgui.handleEvent(ev.*);
}
fn sokolCleanup(user_data: ?*anyopaque) callconv(.c) void {
const self: *Self = @ptrCast(@alignCast(user_data));
_ = self; // autofix
// self.deinit(&self.exposed);
simgui.shutdown();
sg.shutdown();
}
};
}
pub const RunOptions = struct {
init: std.process.Init,
window_title: [*c]const u8 = null,
width: i32 = 800,
height: i32 = 600,
};
pub fn run(App: type, opts: RunOptions) void {
const arena = opts.init.arena.allocator();
const gpa = opts.init.gpa;
_ = gpa; // autofix
const Platform = PlatformType(App);
const plt = arena.create(Platform) catch @panic("OOM");
plt.* = Platform{
.cli_args = opts.init.minimal.args.toSlice(arena) catch @panic("OOM")
};
// var init = Platform.Init{
// .cli_args = self.cli_args
// };
// if (self.init(&self.exposed, &init)) |maybe_error_code| {
// if (maybe_error_code) |error_code| {
// std.process.exit(error_code);
// }
// } else |e| {
// log.err("init() failed: {}", .{e});
// if (@errorReturnTrace()) |trace| {
// std.debug.dumpErrorReturnTrace(trace);
// }
// std.process.exit(1);
// }
if (builtin.os.tag == .linux) {
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);
}
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");
sapp.run(.{
.init_userdata_cb = Platform.sokolInit,
.frame_userdata_cb = Platform.sokolFrame,
.cleanup_userdata_cb = Platform.sokolCleanup,
.event_userdata_cb = Platform.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 "")
}
);
}
}

View File

@ -1,11 +0,0 @@
const std = @import("std");
const assert = std.debug.assert;
const PluginAPI = @import("plugin_api");
fn hello() callconv(.c) u32 {
return 1;
}
pub const api = PluginAPI.Plugin{
.hello = hello
};

View File

@ -1,26 +0,0 @@
const std = @import("std");
const assert = std.debug.assert;
pub const Plugin = struct {
hello: *const fn() callconv(.c) u32,
comptime {
for (@typeInfo(Plugin).@"struct".fields) |field| {
assert(isPointerToCFunction(field.type));
}
}
};
fn isPointerToCFunction(T: type) bool {
if (@typeInfo(T) != .pointer) {
return false;
}
const fnInfo = @typeInfo(@typeInfo(T).pointer.child);
if (fnInfo != .@"fn") {
return false;
}
const calling_convention = std.meta.activeTag(fnInfo.@"fn".calling_convention);
return calling_convention == std.builtin.CallingConvention.c;
}

View File

@ -1,16 +0,0 @@
const plugin = @import("plugin");
const PluginAPI = @import("plugin_api");
comptime {
if (@hasField(plugin, "api")) {
@compileError("Missing 'api' field");
}
if (@TypeOf(plugin.api) != PluginAPI.Plugin) {
@compileError("Incorrect 'api' field type");
}
for (@typeInfo(PluginAPI.Plugin).@"struct".fields) |field| {
@export(@field(plugin.api, field.name), .{ .name = field.name });
}
}