diff --git a/.gitignore b/.gitignore index 880cd5d..7e5c1f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ zig-out +zig-pkg .zig-cache diff --git a/build.zig b/build.zig index 925f5f8..2cb0dbd 100644 --- a/build.zig +++ b/build.zig @@ -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("\""); + } + } +}; diff --git a/build.zig.zon b/build.zig.zon index 01b4be6..04326e3 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -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", diff --git a/libs/stb/build.zig b/libs/stb/build.zig new file mode 100644 index 0000000..963b76f --- /dev/null +++ b/libs/stb/build.zig @@ -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 = &.{} + }); + } +} diff --git a/libs/stb/build.zig.zon b/libs/stb/build.zig.zon new file mode 100644 index 0000000..867deaa --- /dev/null +++ b/libs/stb/build.zig.zon @@ -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", + }, +} diff --git a/libs/stb/src/stb_image.zig b/libs/stb/src/stb_image.zig new file mode 100644 index 0000000..1c357a8 --- /dev/null +++ b/libs/stb/src/stb_image.zig @@ -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); +} diff --git a/libs/stb/src/stb_image_impl.c b/libs/stb/src/stb_image_impl.c new file mode 100644 index 0000000..eeee84a --- /dev/null +++ b/libs/stb/src/stb_image_impl.c @@ -0,0 +1,3 @@ +#define STB_IMAGE_IMPLEMENTATION +#define STBI_NO_STDIO +#include "stb_image.h" diff --git a/libs/stb/src/stb_vorbis.zig b/libs/stb/src/stb_vorbis.zig new file mode 100644 index 0000000..ea25583 --- /dev/null +++ b/libs/stb/src/stb_vorbis.zig @@ -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), + }; +} diff --git a/libs/stb/src/stb_vorbis_impl.c b/libs/stb/src/stb_vorbis_impl.c new file mode 100644 index 0000000..4a53456 --- /dev/null +++ b/libs/stb/src/stb_vorbis_impl.c @@ -0,0 +1,3 @@ +#define STB_VORBIS_NO_INTEGER_CONVERSION +#define STB_VORBIS_NO_STDIO +#include "stb_vorbis.c" diff --git a/libs/tracy/build.zig b/libs/tracy/build.zig new file mode 100644 index 0000000..53e8992 --- /dev/null +++ b/libs/tracy/build.zig @@ -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); +} diff --git a/libs/tracy/build.zig.zon b/libs/tracy/build.zig.zon new file mode 100644 index 0000000..db2a6c3 --- /dev/null +++ b/libs/tracy/build.zig.zon @@ -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, +} diff --git a/libs/tracy/src/root.zig b/libs/tracy/src/root.zig new file mode 100644 index 0000000..227867c --- /dev/null +++ b/libs/tracy/src/root.zig @@ -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); + } +}; diff --git a/scripts/png-to-icon.zig b/scripts/png-to-icon.zig new file mode 100644 index 0000000..0638586 --- /dev/null +++ b/scripts/png-to-icon.zig @@ -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 ", .{}); + 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(); +} + diff --git a/src/app.zig b/src/app.zig new file mode 100644 index 0000000..57a8fdf --- /dev/null +++ b/src/app.zig @@ -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 +} diff --git a/src/cli.zig b/src/cli.zig index 9b164b3..0f37f36 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -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; } } diff --git a/src/color.zig b/src/color.zig new file mode 100644 index 0000000..2e5d2a8 --- /dev/null +++ b/src/color.zig @@ -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); +} diff --git a/src/file_watcher.zig b/src/file_watcher.zig index 820d398..c749daa 100644 --- a/src/file_watcher.zig +++ b/src/file_watcher.zig @@ -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); -} diff --git a/src/main.zig b/src/main.zig index 612642c..2959ea6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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 { diff --git a/src/math.zig b/src/math.zig new file mode 100644 index 0000000..abe04e5 --- /dev/null +++ b/src/math.zig @@ -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); +} diff --git a/src/platform.zig b/src/platform.zig new file mode 100644 index 0000000..db880ff --- /dev/null +++ b/src/platform.zig @@ -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 "") + } + ); + } +} diff --git a/src/plugin.zig b/src/plugin.zig deleted file mode 100644 index cd03d01..0000000 --- a/src/plugin.zig +++ /dev/null @@ -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 -}; diff --git a/src/plugin_api.zig b/src/plugin_api.zig deleted file mode 100644 index 623ebb2..0000000 --- a/src/plugin_api.zig +++ /dev/null @@ -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; -} diff --git a/src/plugin_dynamic.zig b/src/plugin_dynamic.zig deleted file mode 100644 index cb1e40c..0000000 --- a/src/plugin_dynamic.zig +++ /dev/null @@ -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 }); - } -}