Compare commits
No commits in common. "5e9b4b43a9d2f75c8e5479a62d2a2fb83ba0fbd5" and "407da9e0094796ef502e636cc551a5b143ba2af3" have entirely different histories.
5e9b4b43a9
...
407da9e009
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
|||||||
zig-out
|
zig-out
|
||||||
zig-pkg
|
|
||||||
.zig-cache
|
.zig-cache
|
||||||
|
|||||||
@ -21,9 +21,7 @@ pub fn build(b: *std.Build) !void {
|
|||||||
.has_tracy = b.option(bool, "tracy", "Tracy integration"),
|
.has_tracy = b.option(bool, "tracy", "Tracy integration"),
|
||||||
.win32_has_console = b.option(bool, "console", "Show console (Window only)"),
|
.win32_has_console = b.option(bool, "console", "Show console (Window only)"),
|
||||||
.win32_png_icon = b.path("src/assets/icon.png"),
|
.win32_png_icon = b.path("src/assets/icon.png"),
|
||||||
.code_dynamic_linking = b.option(bool, "code-hot-reload", "Dynamically load game code at runtime"),
|
.hot_reload = b.option(bool, "hot-reload", "Dynamically load game code at runtime"),
|
||||||
.asset_hot_reload = b.option(bool, "asset-hot-reload", "Load assets at runtime"),
|
|
||||||
.asset_dir = b.path("src/assets/"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
126
engine/build.zig
126
engine/build.zig
@ -14,98 +14,63 @@ const InitOptions = struct {
|
|||||||
|
|
||||||
dep_engine: *std.Build.Dependency,
|
dep_engine: *std.Build.Dependency,
|
||||||
|
|
||||||
code_static_linking: ?bool = null,
|
statically_linked: ?bool = null,
|
||||||
code_dynamic_linking: ?bool = null,
|
hot_reload: ?bool = null,
|
||||||
has_imgui: ?bool = null,
|
has_imgui: ?bool = null,
|
||||||
has_tracy: ?bool = null,
|
has_tracy: ?bool = null,
|
||||||
win32_has_console: ?bool = null,
|
win32_has_console: ?bool = null,
|
||||||
win32_png_icon: ?std.Build.LazyPath = null,
|
win32_png_icon: ?std.Build.LazyPath = null
|
||||||
asset_hot_reload: ?bool = null,
|
|
||||||
asset_dir: std.Build.LazyPath,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
|
pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
|
||||||
const b = opts.dep_engine.builder;
|
const b = opts.dep_engine.builder;
|
||||||
|
|
||||||
const isWasm = opts.target.result.cpu.arch.isWasm();
|
const isWasm = opts.target.result.cpu.arch.isWasm();
|
||||||
const isDebug = (opts.optimize == .Debug);
|
|
||||||
|
|
||||||
var has_tracy = false;
|
var has_tracy = false;
|
||||||
var code_dynamic_linking = false;
|
var hot_reload = false;
|
||||||
var asset_hot_reload = false;
|
|
||||||
if (!isWasm) {
|
if (!isWasm) {
|
||||||
code_dynamic_linking = opts.code_dynamic_linking orelse isDebug;
|
hot_reload = opts.hot_reload orelse (opts.optimize == .Debug);
|
||||||
has_tracy = opts.has_tracy orelse isDebug;
|
has_tracy = opts.has_tracy orelse (opts.optimize == .Debug);
|
||||||
asset_hot_reload = opts.asset_hot_reload orelse isDebug;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const has_imgui = opts.has_imgui orelse isDebug;
|
const has_imgui = opts.has_imgui orelse (opts.optimize == .Debug);
|
||||||
const code_static_linking = opts.code_static_linking orelse true;
|
const statically_linked = opts.statically_linked orelse !hot_reload;
|
||||||
|
|
||||||
var build_options = b.addOptions();
|
var build_options = b.addOptions();
|
||||||
build_options.addOption(bool, "has_imgui", has_imgui);
|
build_options.addOption(bool, "has_imgui", has_imgui);
|
||||||
build_options.addOption(bool, "has_tracy", has_tracy);
|
build_options.addOption(bool, "has_tracy", has_tracy);
|
||||||
build_options.addOption(bool, "code_dynamic_linking", code_dynamic_linking);
|
build_options.addOption(bool, "hot_reload", hot_reload);
|
||||||
build_options.addOption(bool, "code_static_linking", code_static_linking);
|
build_options.addOption(bool, "statically_linked", statically_linked);
|
||||||
build_options.addOption(bool, "asset_hot_reload", asset_hot_reload);
|
|
||||||
|
|
||||||
const engine_lib = b.createModule(.{
|
const engine_lib = b.createModule(.{
|
||||||
.root_source_file = b.path("src/game_lib/root.zig")
|
.root_source_file = b.path("src/lib/root.zig")
|
||||||
});
|
});
|
||||||
engine_lib.addOptions("build_options", build_options);
|
engine_lib.addOptions("build_options", build_options);
|
||||||
|
opts.root_module.addImport("engine", engine_lib);
|
||||||
|
|
||||||
const dep_stb = b.dependency("stb", .{});
|
const runtime_module = createRuntimeModule(b, .{
|
||||||
engine_lib.addImport("stb_image", dep_stb.module("stb_image"));
|
|
||||||
|
|
||||||
opts.root_module.addImport("engine_lib", engine_lib);
|
|
||||||
|
|
||||||
const engine_module = createEngineModule(b, .{
|
|
||||||
.target = opts.target,
|
.target = opts.target,
|
||||||
.optimize = opts.optimize,
|
.optimize = opts.optimize,
|
||||||
.has_tracy = has_tracy,
|
.has_tracy = has_tracy,
|
||||||
.has_imgui = has_imgui
|
.has_imgui = has_imgui
|
||||||
});
|
});
|
||||||
engine_module.root_module.addImport("engine_lib", engine_lib);
|
runtime_module.root_module.addImport("lib", engine_lib);
|
||||||
if (code_static_linking) {
|
if (statically_linked) {
|
||||||
engine_module.root_module.addImport("game", opts.root_module);
|
runtime_module.root_module.addImport("game", opts.root_module);
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Fix windows cross compile build.
|
|
||||||
// Fails to build asset bundler, because engine module is built for target and not host
|
|
||||||
const assset_bundler_tool = buildAssetBundler(
|
|
||||||
b, b.graph.host, .ReleaseSafe,
|
|
||||||
engine_module.root_module,
|
|
||||||
opts.root_module
|
|
||||||
);
|
|
||||||
|
|
||||||
const asset_bundler_step = b.addRunArtifact(assset_bundler_tool);
|
|
||||||
asset_bundler_step.addDirectoryArg(opts.asset_dir);
|
|
||||||
const assets_bundle_file = asset_bundler_step.addOutputFileArg("assets.bin");
|
|
||||||
|
|
||||||
const runtime_mod = b.createModule(.{
|
|
||||||
.root_source_file = b.path("src/runtime/main.zig"),
|
|
||||||
.target = opts.target,
|
|
||||||
.optimize = opts.optimize,
|
|
||||||
});
|
|
||||||
runtime_mod.addImport("engine", engine_module.root_module);
|
|
||||||
if (!asset_hot_reload) {
|
|
||||||
// TODO: Add compression on asset bundle
|
|
||||||
runtime_mod.addAnonymousImport("asset_bundle", .{
|
|
||||||
.root_source_file = assets_bundle_file
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var run_cmd_step: *std.Build.Step = undefined;
|
var run_cmd_step: *std.Build.Step = undefined;
|
||||||
if (isWasm) {
|
if (isWasm) {
|
||||||
const build_step = buildWasm(b, .{
|
const build_step = buildWasm(b, .{
|
||||||
.name = "index",
|
.name = "index",
|
||||||
.root_module = runtime_mod,
|
.root_module = runtime_module.root_module,
|
||||||
.dep_sokol = engine_module.dep_sokol,
|
.dep_sokol = runtime_module.dep_sokol,
|
||||||
.outer_builder = outer_builder
|
.outer_builder = outer_builder
|
||||||
});
|
});
|
||||||
outer_builder.getInstallStep().dependOn(build_step);
|
outer_builder.getInstallStep().dependOn(build_step);
|
||||||
|
|
||||||
const dep_sokol = engine_module.dep_sokol;
|
const dep_sokol = runtime_module.dep_sokol;
|
||||||
const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{});
|
const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{});
|
||||||
const emrun_step = sokol.emRunStep(outer_builder, .{ .name = "index", .emsdk = dep_emsdk });
|
const emrun_step = sokol.emRunStep(outer_builder, .{ .name = "index", .emsdk = dep_emsdk });
|
||||||
emrun_step.step.dependOn(build_step);
|
emrun_step.step.dependOn(build_step);
|
||||||
@ -113,10 +78,10 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
|
|||||||
|
|
||||||
// TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload
|
// TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload
|
||||||
} else {
|
} else {
|
||||||
runtime_mod.link_libc = true;
|
runtime_module.root_module.link_libc = true;
|
||||||
const exe = buildNative(b, .{
|
const exe = buildNative(b, .{
|
||||||
.name = opts.name,
|
.name = opts.name,
|
||||||
.root_module = runtime_mod,
|
.root_module = runtime_module.root_module,
|
||||||
.win32_has_console = opts.win32_has_console,
|
.win32_has_console = opts.win32_has_console,
|
||||||
.win32_png_icon = opts.win32_png_icon
|
.win32_png_icon = opts.win32_png_icon
|
||||||
});
|
});
|
||||||
@ -127,7 +92,7 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
|
|||||||
if (outer_builder.args) |args| {
|
if (outer_builder.args) |args| {
|
||||||
run_cmd.addArgs(args);
|
run_cmd.addArgs(args);
|
||||||
}
|
}
|
||||||
if (code_dynamic_linking) {
|
if (hot_reload) {
|
||||||
opts.root_module.resolved_target = opts.target;
|
opts.root_module.resolved_target = opts.target;
|
||||||
opts.root_module.optimize = opts.optimize;
|
opts.root_module.optimize = opts.optimize;
|
||||||
const game_lib = b.addLibrary(.{
|
const game_lib = b.addLibrary(.{
|
||||||
@ -137,32 +102,21 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
|
|||||||
});
|
});
|
||||||
const install_game_lib = b.addInstallArtifact(game_lib, .{});
|
const install_game_lib = b.addInstallArtifact(game_lib, .{});
|
||||||
|
|
||||||
run_cmd.addArg("--dynamic-lib-path");
|
|
||||||
run_cmd.addArg(b.getInstallPath(.lib, game_lib.out_lib_filename));
|
run_cmd.addArg(b.getInstallPath(.lib, game_lib.out_lib_filename));
|
||||||
run_cmd.step.dependOn(&install_game_lib.step);
|
run_cmd.step.dependOn(&install_game_lib.step);
|
||||||
|
|
||||||
var game_lib_step = outer_builder.step("game-lib", "Build game dynamic library");
|
var game_lib_step = outer_builder.step("game-lib", "Build game dynamic library");
|
||||||
game_lib_step.dependOn(&install_game_lib.step);
|
game_lib_step.dependOn(&install_game_lib.step);
|
||||||
}
|
}
|
||||||
if (asset_hot_reload) {
|
|
||||||
run_cmd.addArg("--assets-path");
|
|
||||||
run_cmd.addFileArg(opts.asset_dir);
|
|
||||||
}
|
|
||||||
run_cmd_step = &run_cmd.step;
|
run_cmd_step = &run_cmd.step;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const run_step = outer_builder.step("run", "Run game");
|
const run_step = outer_builder.step("run", "Run game");
|
||||||
run_step.dependOn(run_cmd_step);
|
run_step.dependOn(run_cmd_step);
|
||||||
|
|
||||||
{
|
|
||||||
const install_assets_bin = outer_builder.addInstallFileWithDir(assets_bundle_file, .bin, "assets.bin");
|
|
||||||
const assets_step = outer_builder.step("assets", "Build assets file");
|
|
||||||
assets_step.dependOn(&install_assets_bin.step);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EngineModule = struct {
|
const RuntimeModule = struct {
|
||||||
root_module: *std.Build.Module,
|
root_module: *std.Build.Module,
|
||||||
dep_sokol: *std.Build.Dependency,
|
dep_sokol: *std.Build.Dependency,
|
||||||
|
|
||||||
@ -175,9 +129,9 @@ const EngineModule = struct {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule {
|
fn createRuntimeModule(b: *std.Build, opts: RuntimeModule.Options) RuntimeModule {
|
||||||
const mod = b.createModule(.{
|
const mod = b.createModule(.{
|
||||||
.root_source_file = b.path("src/engine/root.zig"),
|
.root_source_file = b.path("src/runtime/main.zig"),
|
||||||
.target = opts.target,
|
.target = opts.target,
|
||||||
.optimize = opts.optimize,
|
.optimize = opts.optimize,
|
||||||
.link_libc = true
|
.link_libc = true
|
||||||
@ -236,7 +190,7 @@ fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule {
|
|||||||
|
|
||||||
mod.addIncludePath(dep_sokol_c.path("util"));
|
mod.addIncludePath(dep_sokol_c.path("util"));
|
||||||
mod.addCSourceFile(.{
|
mod.addCSourceFile(.{
|
||||||
.file = b.path("src/engine/fontstash/sokol_fontstash_impl.c"),
|
.file = b.path("src/runtime/fontstash/sokol_fontstash_impl.c"),
|
||||||
.flags = cflags.items
|
.flags = cflags.items
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -253,36 +207,12 @@ fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule {
|
|||||||
// // TODO: Define buid config for wasm
|
// // TODO: Define buid config for wasm
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return EngineModule{
|
return RuntimeModule{
|
||||||
.root_module = mod,
|
.root_module = mod,
|
||||||
.dep_sokol = dep_sokol
|
.dep_sokol = dep_sokol
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn buildAssetBundler(
|
|
||||||
b: *std.Build,
|
|
||||||
target: std.Build.ResolvedTarget,
|
|
||||||
optimize: std.builtin.OptimizeMode,
|
|
||||||
engine: *std.Build.Module,
|
|
||||||
game: *std.Build.Module
|
|
||||||
) *std.Build.Step.Compile {
|
|
||||||
const mod = b.createModule(.{
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
.root_source_file = b.path("tools/asset-bundler.zig"),
|
|
||||||
});
|
|
||||||
mod.addImport("engine", engine);
|
|
||||||
mod.addImport("game", game);
|
|
||||||
// mod.addImport("asset_bundle", b.createModule(.{
|
|
||||||
// .root_source_file = b.path("src/runtime/asset_bundle.zig")
|
|
||||||
// }));
|
|
||||||
|
|
||||||
return b.addExecutable(.{
|
|
||||||
.name = "asset-bundler",
|
|
||||||
.root_module = mod,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn buildPngToIconTool(
|
fn buildPngToIconTool(
|
||||||
b: *std.Build,
|
b: *std.Build,
|
||||||
target: std.Build.ResolvedTarget,
|
target: std.Build.ResolvedTarget,
|
||||||
@ -371,7 +301,7 @@ fn buildWasm(b: *std.Build, opts: BuildWasmOptions) *std.Build.Step {
|
|||||||
.use_webgl2 = true,
|
.use_webgl2 = true,
|
||||||
.use_emmalloc = true,
|
.use_emmalloc = true,
|
||||||
.use_filesystem = false,
|
.use_filesystem = false,
|
||||||
.shell_file_path = b.path("src/engine/shell.html"),
|
.shell_file_path = b.path("src/runtime/shell.html"),
|
||||||
}) catch unreachable;
|
}) catch unreachable;
|
||||||
|
|
||||||
return &link_step.step;
|
return &link_step.step;
|
||||||
|
|||||||
@ -1,518 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const log = std.log.scoped(.engine);
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
|
|
||||||
const tracy = @import("tracy");
|
|
||||||
|
|
||||||
const Lib = @import("engine_lib");
|
|
||||||
const Math = Lib.Math;
|
|
||||||
const STBVorbis = @import("stb_vorbis");
|
|
||||||
|
|
||||||
const AudioSystem = @This();
|
|
||||||
|
|
||||||
const sokol = @import("sokol");
|
|
||||||
const saudio = sokol.audio;
|
|
||||||
|
|
||||||
const Sound = union(enum) {
|
|
||||||
nil,
|
|
||||||
raw: Raw,
|
|
||||||
vorbis: Vorbis,
|
|
||||||
|
|
||||||
const Raw = struct {
|
|
||||||
channels: [][*]f32,
|
|
||||||
sample_count: u32,
|
|
||||||
sample_rate: u32,
|
|
||||||
|
|
||||||
fn getSampleCount(self: Raw) u32 {
|
|
||||||
return self.sample_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getSampleRate(self: Raw) u32 {
|
|
||||||
return self.sample_rate;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn streamChannel(self: Raw, buffer: []f32, cursor: u32, channel_index: usize) usize {
|
|
||||||
assert(channel_index < self.channels.len); // TODO:
|
|
||||||
|
|
||||||
const channel = self.channels[channel_index];
|
|
||||||
|
|
||||||
var memcpy_len: usize = 0;
|
|
||||||
if (cursor + buffer.len <= self.sample_count) {
|
|
||||||
memcpy_len = buffer.len;
|
|
||||||
} else if (cursor < self.sample_count) {
|
|
||||||
memcpy_len = self.sample_count - cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@memcpy(buffer[0..memcpy_len], channel[cursor..][0..memcpy_len]);
|
|
||||||
return memcpy_len;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn streamChannels(self: Raw, opts: StreamOptions) usize {
|
|
||||||
var sample_count: ?usize = null;
|
|
||||||
for (0.., opts.channels) |i, channel| {
|
|
||||||
const channel_sample_count = self.streamChannel(channel[0..opts.channel_size], opts.cursor, i);
|
|
||||||
if (sample_count == null) {
|
|
||||||
sample_count = channel_sample_count;
|
|
||||||
} else {
|
|
||||||
assert(sample_count.? == channel_sample_count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sample_count orelse 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(self: Raw) void {
|
|
||||||
_ = self; // autofix
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Vorbis = struct {
|
|
||||||
alloc_buffer: []u8,
|
|
||||||
stb_vorbis: STBVorbis,
|
|
||||||
|
|
||||||
fn init(gpa: std.mem.Allocator, data: []const u8, temp_vorbis_alloc_buffer: []u8) !Vorbis {
|
|
||||||
const temp_stb_vorbis = try STBVorbis.init(data, temp_vorbis_alloc_buffer);
|
|
||||||
const alloc_buffer = try gpa.alloc(u8, temp_stb_vorbis.getMinimumAllocBufferSize());
|
|
||||||
errdefer gpa.free(alloc_buffer);
|
|
||||||
|
|
||||||
// This can't fail because `alloc_buffer` is guarenteed to be big enough
|
|
||||||
// And there can't be a decode error because `temp_stb_vorbis` was successfully initialized
|
|
||||||
const stb_vorbis = STBVorbis.init(data, alloc_buffer) catch unreachable;
|
|
||||||
|
|
||||||
return Vorbis{
|
|
||||||
.alloc_buffer = alloc_buffer,
|
|
||||||
.stb_vorbis = stb_vorbis
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getSampleCount(self: Vorbis) u32 {
|
|
||||||
return self.stb_vorbis.getStreamLengthInSamples();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getSampleRate(self: Vorbis) u32 {
|
|
||||||
const info = self.stb_vorbis.getInfo();
|
|
||||||
return info.sample_rate;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getChannels(self: Vorbis) u32 {
|
|
||||||
const info = self.stb_vorbis.getInfo();
|
|
||||||
return info.channels;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn streamChannels(self: Vorbis, opts: StreamOptions) usize {
|
|
||||||
self.stb_vorbis.seek(opts.cursor) catch |e| {
|
|
||||||
log.warn("Failed to seek vorbis: {}", .{e});
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
// self.stb_vorbis.getSamples();
|
|
||||||
|
|
||||||
@panic("TODO");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(self: Vorbis, gpa: std.mem.Allocator) void {
|
|
||||||
gpa.free(self.alloc_buffer);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const StreamOptions = struct {
|
|
||||||
channels: []const [*]f32,
|
|
||||||
channel_size: usize,
|
|
||||||
cursor: u32
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn deinit(self: Sound, gpa: std.mem.Allocator) void {
|
|
||||||
switch (self) {
|
|
||||||
.nil => {},
|
|
||||||
.raw => |raw| raw.deinit(),
|
|
||||||
.vorbis => |vorbis| vorbis.deinit(gpa)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn streamChannels(self: Sound, opts: StreamOptions) usize {
|
|
||||||
return switch (self) {
|
|
||||||
.nil => 0,
|
|
||||||
.raw => |raw| raw.streamChannels(opts),
|
|
||||||
.vorbis => |vorbis| vorbis.streamChannels(opts),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getSampleCount(self: Sound) u32 {
|
|
||||||
return switch (self) {
|
|
||||||
.nil => 0,
|
|
||||||
.raw => |raw| raw.getSampleCount(),
|
|
||||||
.vorbis => |vorbis| vorbis.getSampleCount()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getSampleRate(self: Sound) u32 {
|
|
||||||
return switch (self) {
|
|
||||||
.nil => 0,
|
|
||||||
.raw => |raw| raw.getSampleRate(),
|
|
||||||
.vorbis => |vorbis| vorbis.getSampleRate()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getDuration(self: Sound) Lib.Nanoseconds {
|
|
||||||
const sample_count = self.getSampleCount();
|
|
||||||
const sample_rate = self.getSampleRate();
|
|
||||||
if (sample_rate == 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return @as(Lib.Nanoseconds, sample_count) * std.time.ns_per_s / sample_rate;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const CommandRingBuffer = struct {
|
|
||||||
// TODO: This ring buffer will work in a single producer single consumer configuration
|
|
||||||
// For my game this will be good enough
|
|
||||||
|
|
||||||
items: []Lib.AudioCommand,
|
|
||||||
head: std.atomic.Value(usize),
|
|
||||||
tail: std.atomic.Value(usize),
|
|
||||||
|
|
||||||
fn init(buffer: []Lib.AudioCommand) CommandRingBuffer {
|
|
||||||
return CommandRingBuffer{
|
|
||||||
.items = buffer,
|
|
||||||
.head = .init(0),
|
|
||||||
.tail = .init(0),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn push(self: *CommandRingBuffer, command: Lib.AudioCommand) error{OutOfMemory}!void {
|
|
||||||
const head = self.head.load(.monotonic);
|
|
||||||
const tail = self.tail.load(.monotonic);
|
|
||||||
const next_head = @mod(head + 1, self.items.len);
|
|
||||||
|
|
||||||
// A single slot in the .items array will always not be used.
|
|
||||||
if (next_head == tail) {
|
|
||||||
return error.OutOfMemory;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.items[head] = command;
|
|
||||||
self.head.store(next_head, .monotonic);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pop(self: *CommandRingBuffer) ?Lib.AudioCommand {
|
|
||||||
const head = self.head.load(.monotonic);
|
|
||||||
const tail = self.tail.load(.monotonic);
|
|
||||||
|
|
||||||
if (head == tail) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = self.items[tail];
|
|
||||||
self.tail.store(@mod(tail + 1, self.items.len), .monotonic);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Be mindful when accessing fields on this struct!
|
|
||||||
/// This data will be accessed by the audio thread which runs
|
|
||||||
/// independentaly of the main thread
|
|
||||||
const ThreadState = struct {
|
|
||||||
instances: std.ArrayList(SoundInstance),
|
|
||||||
commands: CommandRingBuffer,
|
|
||||||
temp_channel_buffer: []f32,
|
|
||||||
|
|
||||||
pub const SoundInstance = struct {
|
|
||||||
sound_id: Lib.Sound.Id,
|
|
||||||
volume: f32 = 0,
|
|
||||||
cursor: u32 = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(
|
|
||||||
gpa: std.mem.Allocator,
|
|
||||||
max_instances: usize,
|
|
||||||
max_commands: usize,
|
|
||||||
max_frames_per_channel: usize
|
|
||||||
) !ThreadState {
|
|
||||||
var instances: std.ArrayList(SoundInstance) = try .initCapacity(gpa, max_instances);
|
|
||||||
errdefer instances.deinit(gpa);
|
|
||||||
|
|
||||||
const command_buffer = try gpa.alloc(Lib.AudioCommand, max_commands);
|
|
||||||
errdefer gpa.free(command_buffer);
|
|
||||||
|
|
||||||
const temp_channel_buffer = try gpa.alloc(f32, max_frames_per_channel);
|
|
||||||
errdefer gpa.free(temp_channel_buffer);
|
|
||||||
|
|
||||||
return ThreadState{
|
|
||||||
.instances = instances,
|
|
||||||
.commands = .init(command_buffer),
|
|
||||||
.temp_channel_buffer = temp_channel_buffer
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *ThreadState, gpa: std.mem.Allocator) void {
|
|
||||||
self.instances.deinit(gpa);
|
|
||||||
gpa.free(self.commands.items);
|
|
||||||
gpa.free(self.temp_channel_buffer);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const SokolSetupOptions = struct {
|
|
||||||
logger: saudio.Logger,
|
|
||||||
channels: u32,
|
|
||||||
buffer_frames: u32
|
|
||||||
};
|
|
||||||
|
|
||||||
const SoundList = Lib.SlotMap(u8, u8, Sound);
|
|
||||||
comptime {
|
|
||||||
assert(@bitSizeOf(SoundList.Id) == @bitSizeOf(Lib.Sound.Id));
|
|
||||||
}
|
|
||||||
|
|
||||||
gpa: std.mem.Allocator,
|
|
||||||
running: std.atomic.Value(bool),
|
|
||||||
temp_vorbis_alloc_buffer: []u8,
|
|
||||||
|
|
||||||
sokol_setup: bool,
|
|
||||||
sokol_options: SokolSetupOptions,
|
|
||||||
|
|
||||||
// When possible hold `sounds_mutex` for as short as possbile, otherwise the audio thread will be affected.
|
|
||||||
sounds_mutex: std.Thread.Mutex,
|
|
||||||
sounds_arena: std.heap.ArenaAllocator,
|
|
||||||
sounds: SoundList,
|
|
||||||
|
|
||||||
thread_state: ThreadState,
|
|
||||||
|
|
||||||
const Options = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
logger: saudio.Logger = .{},
|
|
||||||
channels: u32 = 1,
|
|
||||||
max_vorbis_alloc_buffer_size: u32 = 1 * Math.bytes_per_mib,
|
|
||||||
buffer_frames: u32 = 2048,
|
|
||||||
|
|
||||||
max_instances: u32 = 64,
|
|
||||||
max_commands: u32 = 64,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(opts: Options) !AudioSystem {
|
|
||||||
const gpa = opts.allocator;
|
|
||||||
|
|
||||||
const temp_vorbis_alloc_buffer = try gpa.alloc(u8, opts.max_vorbis_alloc_buffer_size);
|
|
||||||
errdefer gpa.free(temp_vorbis_alloc_buffer);
|
|
||||||
|
|
||||||
const thread_state = try ThreadState.init(gpa,
|
|
||||||
opts.max_instances,
|
|
||||||
opts.max_instances,
|
|
||||||
opts.buffer_frames
|
|
||||||
);
|
|
||||||
errdefer thread_state.deinit(gpa);
|
|
||||||
|
|
||||||
return AudioSystem{
|
|
||||||
.gpa = gpa,
|
|
||||||
.running = .init(false),
|
|
||||||
.temp_vorbis_alloc_buffer = temp_vorbis_alloc_buffer,
|
|
||||||
.sounds = .empty,
|
|
||||||
.sounds_arena = std.heap.ArenaAllocator.init(gpa),
|
|
||||||
.thread_state = thread_state,
|
|
||||||
.sounds_mutex = .{},
|
|
||||||
.sokol_setup = false,
|
|
||||||
.sokol_options = .{
|
|
||||||
.logger = opts.logger,
|
|
||||||
.channels = opts.channels,
|
|
||||||
.buffer_frames = opts.buffer_frames
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// IMPORTANT! After this function is called, the `self` pointer can't be moved
|
|
||||||
pub fn setupSokol(self: *AudioSystem) void {
|
|
||||||
assert(!self.sokol_setup);
|
|
||||||
|
|
||||||
const opts = self.sokol_options;
|
|
||||||
saudio.setup(.{
|
|
||||||
.stream_userdata_cb = sokolStreamCallback,
|
|
||||||
.user_data = self,
|
|
||||||
.logger = opts.logger,
|
|
||||||
.num_channels = @intCast(opts.channels),
|
|
||||||
.buffer_frames = @intCast(opts.buffer_frames),
|
|
||||||
});
|
|
||||||
self.sokol_setup = true;
|
|
||||||
self.running.store(true, .seq_cst);
|
|
||||||
|
|
||||||
const sample_rate: f32 = @floatFromInt(saudio.sampleRate());
|
|
||||||
const max_latency: f32 = @as(f32, @floatFromInt(opts.buffer_frames)) / sample_rate;
|
|
||||||
|
|
||||||
log.debug("Audio:", .{});
|
|
||||||
log.debug("- sample_rate: {}", .{saudio.sampleRate()});
|
|
||||||
log.debug("- channels: {}", .{saudio.channels()});
|
|
||||||
log.debug("- buffer_frames: {}", .{saudio.bufferFrames()});
|
|
||||||
log.debug("- max_latency: {D}", .{@as(u64, @intFromFloat(max_latency * std.time.ns_per_s))});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *AudioSystem) void {
|
|
||||||
self.running.store(false, .seq_cst);
|
|
||||||
|
|
||||||
if (self.sokol_setup) {
|
|
||||||
saudio.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.thread_state.deinit(self.gpa);
|
|
||||||
self.gpa.free(self.temp_vorbis_alloc_buffer);
|
|
||||||
self.sounds_arena.deinit();
|
|
||||||
self.sounds.deinit(self.gpa);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn addSound(self: *AudioSystem) !Lib.Sound.Id {
|
|
||||||
self.sounds_mutex.lock();
|
|
||||||
defer self.sounds_mutex.unlock();
|
|
||||||
|
|
||||||
const sound_id = try self.sounds.insert(self.gpa, .{
|
|
||||||
.nil = {}
|
|
||||||
});
|
|
||||||
return @enumFromInt(sound_id.asInt());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn removeSound(self: *AudioSystem, id: Lib.Sound.Id) void {
|
|
||||||
self.sounds_mutex.lock();
|
|
||||||
defer self.sounds_mutex.unlock();
|
|
||||||
|
|
||||||
_ = self.sounds.remove(.fromInt(@intFromEnum(id)));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const DataOptions = struct {
|
|
||||||
// If the decoded size is less than `stream_threshold`, then .decode_once will by default be used.
|
|
||||||
const stream_threshold = 10 * Math.bytes_per_mib;
|
|
||||||
|
|
||||||
format: Lib.Sound.Format,
|
|
||||||
data: []const u8,
|
|
||||||
playback_style: ?Lib.Sound.PlaybackStyle = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn setSoundData(self: *AudioSystem, id: Lib.Sound.Id, opts: DataOptions) !void {
|
|
||||||
const sound = self.sounds.get(.fromInt(@intFromEnum(id))) orelse return error.SoundNotFound;
|
|
||||||
|
|
||||||
assert(opts.format == .vorbis);
|
|
||||||
const vorbis = try Sound.Vorbis.init(self.gpa, opts.data, self.temp_vorbis_alloc_buffer);
|
|
||||||
|
|
||||||
const PlaybackStyle = Lib.Sound.PlaybackStyle;
|
|
||||||
var playback_style: PlaybackStyle = undefined;
|
|
||||||
if (opts.playback_style == null) {
|
|
||||||
const decoded_size = vorbis.getChannels() * vorbis.getSampleCount() * @sizeOf(f32);
|
|
||||||
const default_syle: PlaybackStyle = if (decoded_size < DataOptions.stream_threshold) .decode_once else .stream;
|
|
||||||
|
|
||||||
playback_style = default_syle;
|
|
||||||
} else {
|
|
||||||
playback_style = opts.playback_style.?;
|
|
||||||
}
|
|
||||||
|
|
||||||
var new_sound: Sound = .nil;
|
|
||||||
errdefer new_sound.deinit(self.gpa);
|
|
||||||
|
|
||||||
if (playback_style == .decode_once) {
|
|
||||||
const duration_in_samples = vorbis.getSampleCount();
|
|
||||||
|
|
||||||
const arena_allocator = self.sounds_arena.allocator();
|
|
||||||
const channels = try arena_allocator.alloc([*]f32, vorbis.getChannels());
|
|
||||||
for (channels) |*channel| {
|
|
||||||
channel.* = (try arena_allocator.alloc(f32, duration_in_samples)).ptr;
|
|
||||||
}
|
|
||||||
const samples_decoded = vorbis.stb_vorbis.getSamples(channels, duration_in_samples);
|
|
||||||
assert(samples_decoded == duration_in_samples);
|
|
||||||
|
|
||||||
new_sound = Sound{
|
|
||||||
.raw = .{
|
|
||||||
.channels = channels,
|
|
||||||
.sample_count = duration_in_samples,
|
|
||||||
.sample_rate = vorbis.getSampleRate()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
vorbis.deinit(self.gpa);
|
|
||||||
} else {
|
|
||||||
new_sound = Sound{ .vorbis = vorbis };
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
self.sounds_mutex.lock();
|
|
||||||
defer self.sounds_mutex.unlock();
|
|
||||||
|
|
||||||
sound.deinit(self.gpa);
|
|
||||||
sound.* = new_sound;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: u32, num_channels: u32) void {
|
|
||||||
if (!self.running.load(.seq_cst)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const zone = tracy.initZone(@src(), .{ });
|
|
||||||
defer zone.deinit();
|
|
||||||
|
|
||||||
self.sounds_mutex.lock();
|
|
||||||
defer self.sounds_mutex.unlock();
|
|
||||||
|
|
||||||
const thread_state = &self.thread_state;
|
|
||||||
while (thread_state.commands.pop()) |command| {
|
|
||||||
switch (command) {
|
|
||||||
.play => |opts| {
|
|
||||||
const volume = @max(opts.volume, 0);
|
|
||||||
if (volume == 0) {
|
|
||||||
log.warn("Attempt to play audio with 0 volume", .{});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
thread_state.instances.appendBounded(.{
|
|
||||||
.sound_id = opts.id,
|
|
||||||
.volume = volume,
|
|
||||||
}) catch log.warn("Maximum number of audio instances reached!", .{});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(num_channels == 1); // TODO:
|
|
||||||
const sample_rate: u32 = @intCast(saudio.sampleRate());
|
|
||||||
|
|
||||||
assert(thread_state.temp_channel_buffer.len >= num_frames);
|
|
||||||
|
|
||||||
@memset(buffer[0..@intCast(num_frames * num_channels)], 0);
|
|
||||||
|
|
||||||
for (thread_state.instances.items) |*instance| {
|
|
||||||
const sound = self.sounds.get(.fromInt(@intFromEnum(instance.sound_id))) orelse {
|
|
||||||
log.warn("Attempt to play that doesn't exist", .{});
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if (sound.* == .nil) {
|
|
||||||
log.warn("Attempt to play .nil sound", .{});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
assert(sound.getSampleRate() == sample_rate); // TODO:
|
|
||||||
|
|
||||||
const sample_count = sound.streamChannels(.{
|
|
||||||
.channels = &.{
|
|
||||||
thread_state.temp_channel_buffer.ptr,
|
|
||||||
},
|
|
||||||
.channel_size = num_frames,
|
|
||||||
.cursor = instance.cursor,
|
|
||||||
});
|
|
||||||
|
|
||||||
const samples = thread_state.temp_channel_buffer[0..sample_count];
|
|
||||||
for (0.., samples) |i, sample| {
|
|
||||||
buffer[i] += sample * instance.volume;
|
|
||||||
}
|
|
||||||
instance.cursor += @intCast(samples.len);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < thread_state.instances.items.len) {
|
|
||||||
const instance = thread_state.instances.items[i];
|
|
||||||
|
|
||||||
var is_complete = true;
|
|
||||||
if (self.sounds.get(.fromInt(@intFromEnum(instance.sound_id)))) |sound| {
|
|
||||||
is_complete = instance.cursor >= sound.getSampleCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_complete) {
|
|
||||||
_ = thread_state.instances.swapRemove(i);
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sokolStreamCallback(buffer: [*c]f32, num_frames: i32, num_channels: i32, user_data: ?*anyopaque) callconv(.c) void {
|
|
||||||
const audio: *AudioSystem = @alignCast(@ptrCast(user_data));
|
|
||||||
audio.sokolStream(buffer, @intCast(num_frames), @intCast(num_channels));
|
|
||||||
}
|
|
||||||
@ -1,300 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const log = std.log.scoped(.engine);
|
|
||||||
|
|
||||||
const CLI = @This();
|
|
||||||
|
|
||||||
gpa: std.mem.Allocator,
|
|
||||||
arena: std.heap.ArenaAllocator,
|
|
||||||
|
|
||||||
options: std.ArrayList(Option),
|
|
||||||
commands: std.ArrayList(Command),
|
|
||||||
|
|
||||||
stdout: Channel,
|
|
||||||
stderr: Channel,
|
|
||||||
|
|
||||||
pub const Command = struct {
|
|
||||||
name: []const u8,
|
|
||||||
description: []const u8,
|
|
||||||
|
|
||||||
const Id = enum (u32) { _ };
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Option = struct {
|
|
||||||
long_name: []const u8,
|
|
||||||
description: []const u8,
|
|
||||||
kind: Kind,
|
|
||||||
|
|
||||||
pub const Kind = enum {
|
|
||||||
toggle,
|
|
||||||
single_argument
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Id = enum (u32) { _ };
|
|
||||||
|
|
||||||
const Value = struct {
|
|
||||||
argument: ?[]const u8 = null
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const Channel = struct {
|
|
||||||
file: std.fs.File,
|
|
||||||
buffer: [4 * 4096]u8,
|
|
||||||
writer: std.fs.File.Writer,
|
|
||||||
|
|
||||||
pub fn init(self: *Channel, file: std.fs.File) void {
|
|
||||||
self.* = Channel{
|
|
||||||
.file = file,
|
|
||||||
.buffer = undefined,
|
|
||||||
.writer = file.writer(&self.buffer)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write(self: *Channel, comptime fmt: []const u8, args: anytype) void {
|
|
||||||
self.writer.interface.print(fmt, args) catch |e| {
|
|
||||||
log.err("Failed to write to channel: {}", .{e});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn flush(self: *Channel) void {
|
|
||||||
self.writer.interface.flush() catch |e| {
|
|
||||||
log.err("Failed to flush channel: {}", .{e});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(self: *CLI, gpa: std.mem.Allocator) void {
|
|
||||||
self.* = CLI{
|
|
||||||
.gpa = gpa,
|
|
||||||
.arena = .init(gpa),
|
|
||||||
.options = .empty,
|
|
||||||
.commands = .empty,
|
|
||||||
.stdout = undefined,
|
|
||||||
.stderr = undefined
|
|
||||||
};
|
|
||||||
self.stdout.init(std.fs.File.stdout());
|
|
||||||
self.stderr.init(std.fs.File.stderr());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *CLI) void {
|
|
||||||
self.stderr.flush();
|
|
||||||
self.stdout.flush();
|
|
||||||
|
|
||||||
self.arena.deinit();
|
|
||||||
|
|
||||||
self.options.deinit(self.gpa);
|
|
||||||
self.commands.deinit(self.gpa);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const AddOptionOptions = struct {
|
|
||||||
long_name: []const u8,
|
|
||||||
description: ?[]const u8 = null,
|
|
||||||
kind: Option.Kind = .toggle,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn addOption(self: *CLI, opts: AddOptionOptions) !Option.Id {
|
|
||||||
const arena = self.arena.allocator();
|
|
||||||
|
|
||||||
var owned_description: []const u8 = "";
|
|
||||||
if (opts.description) |description| {
|
|
||||||
owned_description = try arena.dupe(u8, description);
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = self.options.items.len;
|
|
||||||
try self.options.append(self.gpa, .{
|
|
||||||
.long_name = try arena.dupe(u8, opts.long_name),
|
|
||||||
.description = owned_description,
|
|
||||||
.kind = opts.kind
|
|
||||||
});
|
|
||||||
|
|
||||||
return @enumFromInt(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getOptionByLongName(self: *CLI, long_name: []const u8) ?Option.Id {
|
|
||||||
for (0.., self.options.items) |i, option| {
|
|
||||||
if (std.mem.eql(u8, long_name, option.long_name)) {
|
|
||||||
return @enumFromInt(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const AddCommandOptions = struct {
|
|
||||||
name: []const u8,
|
|
||||||
description: ?[]const u8 = null
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn addCommand(self: *CLI, opts: AddCommandOptions) !Command.Id {
|
|
||||||
const arena = self.arena.allocator();
|
|
||||||
|
|
||||||
var owned_description: []const u8 = "";
|
|
||||||
if (opts.description) |description| {
|
|
||||||
owned_description = try arena.dupe(u8, description);
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = self.commands.items.len;
|
|
||||||
try self.commands.append(self.gpa, Command{
|
|
||||||
.name = try arena.dupe(u8, opts.name),
|
|
||||||
.description = owned_description,
|
|
||||||
});
|
|
||||||
|
|
||||||
return @enumFromInt(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn showUsage(self: *CLI, program_name: []const u8) void {
|
|
||||||
self.stderr.write("Usage: {s}", .{program_name});
|
|
||||||
if (self.options.items.len > 0) {
|
|
||||||
self.stderr.write(" [options]", .{});
|
|
||||||
}
|
|
||||||
if (self.commands.items.len > 0) {
|
|
||||||
self.stderr.write(" <command>", .{});
|
|
||||||
}
|
|
||||||
self.stderr.write("\n", .{});
|
|
||||||
|
|
||||||
if (self.commands.items.len > 0) {
|
|
||||||
self.stderr.write("\n", .{});
|
|
||||||
self.stderr.write("Commands:\n", .{});
|
|
||||||
for (self.commands.items) |command| {
|
|
||||||
self.stderr.write(" {s} {s}\n", .{command.name, command.description});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.options.items.len > 0) {
|
|
||||||
self.stderr.write("\n", .{});
|
|
||||||
self.stderr.write("Options:\n", .{});
|
|
||||||
|
|
||||||
for (0.., self.options.items) |i, option| {
|
|
||||||
if (i > 0) {
|
|
||||||
self.stderr.write("\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
self.stderr.write(" --{s}", .{option.long_name});
|
|
||||||
if (option.kind == .single_argument) {
|
|
||||||
// TODO: Make this hint renameable
|
|
||||||
self.stderr.write(" <value>", .{});
|
|
||||||
}
|
|
||||||
self.stderr.write("\n", .{});
|
|
||||||
if (option.description.len > 0) {
|
|
||||||
self.stderr.write(" {s}\n", .{option.description});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.stderr.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const ParseResult = struct {
|
|
||||||
arena: std.heap.ArenaAllocator,
|
|
||||||
|
|
||||||
command: ?Command.Id,
|
|
||||||
options: []?Option.Value,
|
|
||||||
|
|
||||||
pub fn init(gpa: std.mem.Allocator, options_len: usize) !ParseResult {
|
|
||||||
var arena = std.heap.ArenaAllocator.init(gpa);
|
|
||||||
errdefer arena.deinit();
|
|
||||||
|
|
||||||
const options = try arena.allocator().alloc(?Option.Value, options_len);
|
|
||||||
@memset(options, null);
|
|
||||||
|
|
||||||
return ParseResult{
|
|
||||||
.arena = arena,
|
|
||||||
.command = null,
|
|
||||||
.options = options
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *ParseResult) void {
|
|
||||||
self.arena.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getOption(self: ParseResult, id: Option.Id) ?Option.Value {
|
|
||||||
return self.options[@intFromEnum(id)];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isSet(self: ParseResult, id: Option.Id) bool {
|
|
||||||
return self.getOption(id) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: I don't like this function name
|
|
||||||
pub fn getOptionArgument(self: ParseResult, id: Option.Id) ?[]const u8 {
|
|
||||||
if (self.getOption(id)) |option| {
|
|
||||||
return option.argument;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Parser = struct {
|
|
||||||
args: []const []const u8,
|
|
||||||
cursor: u32,
|
|
||||||
|
|
||||||
pub fn peek(self: *Parser) ?[]const u8 {
|
|
||||||
if (self.cursor < self.args.len) {
|
|
||||||
return self.args[self.cursor];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn take(self: *Parser) ?[]const u8 {
|
|
||||||
if (self.peek()) |value| {
|
|
||||||
self.cursor += 1;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn printErrorAndExit(self: *CLI, comptime fmt: []const u8, args: anytype) noreturn {
|
|
||||||
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();
|
|
||||||
|
|
||||||
var parser = Parser{
|
|
||||||
.args = args,
|
|
||||||
.cursor = 1
|
|
||||||
};
|
|
||||||
|
|
||||||
next_arg: while (parser.take()) |arg| {
|
|
||||||
if (std.mem.startsWith(u8, arg, "--")) {
|
|
||||||
const long_option_name = arg[2..];
|
|
||||||
if (self.getOptionByLongName(long_option_name)) |option_id| {
|
|
||||||
const option = self.options.items[@intFromEnum(option_id)];
|
|
||||||
var result_value = Option.Value{};
|
|
||||||
|
|
||||||
if (option.kind == .single_argument) {
|
|
||||||
const argument = parser.take() orelse {
|
|
||||||
self.printErrorAndExit("Missing required argument for '{s}'", .{arg});
|
|
||||||
};
|
|
||||||
|
|
||||||
result_value.argument = argument;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.options[@intFromEnum(option_id)] = result_value;
|
|
||||||
continue :next_arg;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.printErrorAndExit("Unknown option '{s}'", .{arg});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.command == null) {
|
|
||||||
for (0.., self.commands.items) |i, command| {
|
|
||||||
if (std.mem.eql(u8, command.name, arg)) {
|
|
||||||
result.command = @enumFromInt(i);
|
|
||||||
continue :next_arg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.printErrorAndExit("Unknown command '{s}'", .{arg});
|
|
||||||
} else {
|
|
||||||
self.printErrorAndExit("Unknown argument '{s}'", .{arg});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const Environment = @import("./environment.zig");
|
|
||||||
const GameLinking = @import("./game_linking.zig");
|
|
||||||
const Input = @import("./input.zig");
|
|
||||||
|
|
||||||
const DebugSystem = @This();
|
|
||||||
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
|
|
||||||
is_mouse_inside_imgui: bool,
|
|
||||||
|
|
||||||
show_debug: bool,
|
|
||||||
restart_game: bool,
|
|
||||||
recording: ?Recording,
|
|
||||||
|
|
||||||
play_recording: bool,
|
|
||||||
recording_tick: u64,
|
|
||||||
|
|
||||||
dynamic_linking: ?GameLinking.Dynamic,
|
|
||||||
|
|
||||||
const Recording = struct {
|
|
||||||
arena: std.heap.ArenaAllocator,
|
|
||||||
|
|
||||||
initial: Environment.Init,
|
|
||||||
ticks: std.ArrayList(Environment.Tick),
|
|
||||||
|
|
||||||
pub fn init(gpa: std.mem.Allocator, initial: Environment.Init) Recording {
|
|
||||||
return Recording{
|
|
||||||
.arena = .init(gpa),
|
|
||||||
.ticks = .empty,
|
|
||||||
.initial = initial
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn appendTick(self: *Recording, tick: Environment.Tick) !void {
|
|
||||||
const arena = self.arena.allocator();
|
|
||||||
|
|
||||||
try self.ticks.append(arena, .{
|
|
||||||
.dt_ns = tick.dt_ns,
|
|
||||||
.events = try arena.dupe(Input.Event, tick.events)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: Recording) void {
|
|
||||||
self.arena.deinit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator) DebugSystem {
|
|
||||||
return DebugSystem{
|
|
||||||
.allocator = allocator,
|
|
||||||
.is_mouse_inside_imgui = false,
|
|
||||||
.show_debug = false,
|
|
||||||
.restart_game = false,
|
|
||||||
.recording = null,
|
|
||||||
.play_recording = false,
|
|
||||||
.recording_tick = 0,
|
|
||||||
.dynamic_linking = null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *DebugSystem) void {
|
|
||||||
if (self.dynamic_linking) |*dynamic| {
|
|
||||||
dynamic.deinit(self.allocator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,206 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const log = std.log.scoped(.engine);
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
|
|
||||||
const Input = @import("./input.zig");
|
|
||||||
|
|
||||||
const ScreenScalar = @import("./screen_scaler.zig");
|
|
||||||
|
|
||||||
pub const Lib = @import("engine_lib");
|
|
||||||
const GameCallbacks = Lib.Callbacks;
|
|
||||||
|
|
||||||
const Math = Lib.Math;
|
|
||||||
const Vec2 = Math.Vec2;
|
|
||||||
const rgb = Math.rgb;
|
|
||||||
const Vec4 = Math.Vec4;
|
|
||||||
|
|
||||||
const Environment = @This();
|
|
||||||
|
|
||||||
gpa: std.mem.Allocator,
|
|
||||||
|
|
||||||
callbacks: GameCallbacks,
|
|
||||||
state: ?GameCallbacks.State,
|
|
||||||
frame: ?Lib.Frame,
|
|
||||||
|
|
||||||
mouse_position: ?Vec2,
|
|
||||||
started_at: std.time.Instant,
|
|
||||||
last_frame_at: Lib.Nanoseconds,
|
|
||||||
|
|
||||||
pub const Init = struct {
|
|
||||||
screen_size: Vec2,
|
|
||||||
seed: u64
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Tick = struct {
|
|
||||||
dt_ns: u64,
|
|
||||||
events: []Input.Event,
|
|
||||||
};
|
|
||||||
|
|
||||||
const TickResult = struct {
|
|
||||||
audio: []Lib.AudioCommand,
|
|
||||||
graphics: []Lib.GraphicsCommand,
|
|
||||||
clear_color: Vec4,
|
|
||||||
cursor_hidden: bool
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(gpa: std.mem.Allocator, callbacks: GameCallbacks) Environment {
|
|
||||||
return Environment{
|
|
||||||
.gpa = gpa,
|
|
||||||
.mouse_position = null,
|
|
||||||
.started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"),
|
|
||||||
.last_frame_at = 0,
|
|
||||||
.state = null,
|
|
||||||
.frame = null,
|
|
||||||
.callbacks = callbacks
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Environment) void {
|
|
||||||
if (self.frame) |*frame| {
|
|
||||||
frame.deinit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn processEvent(self: *Environment, event: Input.Event) void {
|
|
||||||
assert(self.frame != null);
|
|
||||||
const frame = &self.frame.?;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
.key_pressed => |opts| {
|
|
||||||
if (!opts.repeat) {
|
|
||||||
frame.keyboard.press(opts.code, frame.time_ns);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.key_released => |key_code| {
|
|
||||||
frame.keyboard.release(key_code);
|
|
||||||
},
|
|
||||||
.mouse_leave => {
|
|
||||||
frame.keyboard.releaseAll();
|
|
||||||
|
|
||||||
assert(self.mouse_position != null);
|
|
||||||
self.mouse_position = null;
|
|
||||||
frame.mouse_button = .empty;
|
|
||||||
},
|
|
||||||
.mouse_enter => |pos| {
|
|
||||||
assert(self.mouse_position == null);
|
|
||||||
self.mouse_position = pos;
|
|
||||||
},
|
|
||||||
.mouse_move => |pos| {
|
|
||||||
assert(self.mouse_position != null);
|
|
||||||
self.mouse_position = pos;
|
|
||||||
},
|
|
||||||
.mouse_pressed => |opts| {
|
|
||||||
assert(self.mouse_position != null);
|
|
||||||
self.mouse_position = opts.position;
|
|
||||||
frame.mouse_button.press(opts.button, frame.time_ns);
|
|
||||||
},
|
|
||||||
.mouse_released => |opts| {
|
|
||||||
assert(self.mouse_position != null);
|
|
||||||
self.mouse_position = opts.position;
|
|
||||||
frame.mouse_button.release(opts.button);
|
|
||||||
},
|
|
||||||
.window_resize => |opts| {
|
|
||||||
frame.screen_size = .initFromInt(u32, opts.width, opts.height);
|
|
||||||
},
|
|
||||||
else => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stateInit(self: *Environment, initial: Init, assets: Lib.Asset.List) void {
|
|
||||||
assert(self.frame == null);
|
|
||||||
const opts = Lib.Init{
|
|
||||||
.gpa = self.gpa,
|
|
||||||
.seed = initial.seed,
|
|
||||||
.assets = assets
|
|
||||||
};
|
|
||||||
self.state = self.callbacks.init(&opts);
|
|
||||||
|
|
||||||
self.frame = .init(self.gpa, initial.screen_size, assets);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stateDeinit(self: *Environment) void {
|
|
||||||
assert(self.state != null);
|
|
||||||
self.callbacks.deinit(self.state.?);
|
|
||||||
self.state = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stateTick(self: *Environment, opts: Environment.Tick) !TickResult {
|
|
||||||
assert(self.frame != null);
|
|
||||||
const frame = &self.frame.?;
|
|
||||||
|
|
||||||
_ = frame.arena.reset(.retain_capacity);
|
|
||||||
const arena = frame.arena.allocator();
|
|
||||||
|
|
||||||
const audio_commands_capacity = frame.audio_commands.capacity;
|
|
||||||
frame.audio_commands = .empty;
|
|
||||||
try frame.audio_commands.ensureTotalCapacity(arena, audio_commands_capacity);
|
|
||||||
|
|
||||||
const graphics_commands_capacity = frame.graphics_commands.capacity;
|
|
||||||
frame.graphics_commands = .empty;
|
|
||||||
try frame.graphics_commands.ensureTotalCapacity(arena, graphics_commands_capacity);
|
|
||||||
|
|
||||||
frame.dt_ns = opts.dt_ns;
|
|
||||||
frame.time_ns += opts.dt_ns;
|
|
||||||
|
|
||||||
for (opts.events) |event| {
|
|
||||||
if (self.mouse_position == null) {
|
|
||||||
if (event == .mouse_leave) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const enter_position = switch(event) {
|
|
||||||
.mouse_pressed => |e_opts| e_opts.position,
|
|
||||||
.mouse_released => |e_opts| e_opts.position,
|
|
||||||
.mouse_move => |pos| pos,
|
|
||||||
else => null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (enter_position) |pos| {
|
|
||||||
self.processEvent(.{ .mouse_enter = pos });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (event == .mouse_enter) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.processEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.mouse_position = self.mouse_position;
|
|
||||||
|
|
||||||
var maybe_screen_scaler: ?ScreenScalar = null;
|
|
||||||
if (frame.canvas_size) |canvas_size| {
|
|
||||||
const screen_scaler = ScreenScalar.init(frame.screen_size, canvas_size);
|
|
||||||
maybe_screen_scaler = screen_scaler;
|
|
||||||
screen_scaler.push(frame);
|
|
||||||
|
|
||||||
if (frame.mouse_position) |mouse_position| {
|
|
||||||
frame.mouse_position = mouse_position.sub(screen_scaler.translation).divideScalar(screen_scaler.scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(self.state != null);
|
|
||||||
self.callbacks.tick(self.state.?, frame);
|
|
||||||
|
|
||||||
if (maybe_screen_scaler) |screen_scaler| {
|
|
||||||
screen_scaler.pop(frame, frame.clear_color);
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.keyboard.pressed = .initEmpty();
|
|
||||||
frame.keyboard.released = .initEmpty();
|
|
||||||
frame.mouse_button.pressed = .initEmpty();
|
|
||||||
frame.mouse_button.released = .initEmpty();
|
|
||||||
|
|
||||||
return TickResult{
|
|
||||||
.graphics = frame.graphics_commands.items,
|
|
||||||
.audio = frame.audio_commands.items,
|
|
||||||
.clear_color = frame.clear_color,
|
|
||||||
.cursor_hidden = frame.hide_cursor
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stateDebug(self: *Environment, imgui_ctx: *Lib.ImGui) void {
|
|
||||||
assert(self.state != null);
|
|
||||||
self.callbacks.debug(self.state.?, imgui_ctx);
|
|
||||||
}
|
|
||||||
@ -1,211 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const log = std.log.scoped(.engine);
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
|
|
||||||
const Lib = @import("engine_lib");
|
|
||||||
const GameCallbacks = Lib.Callbacks;
|
|
||||||
const build_options = Lib.build_options;
|
|
||||||
|
|
||||||
const GameLinking = @This();
|
|
||||||
|
|
||||||
pub const Dynamic = struct {
|
|
||||||
fan_fd: std.os.linux.fd_t,
|
|
||||||
path: []const u8,
|
|
||||||
dir_path: [:0]const u8,
|
|
||||||
lib: std.DynLib,
|
|
||||||
last_event_at: ?std.time.Instant,
|
|
||||||
ignore_next_event: bool,
|
|
||||||
reload_index: u32,
|
|
||||||
|
|
||||||
// TODO: Make this configurable
|
|
||||||
const event_debounce_ns = std.time.ns_per_ms * 150;
|
|
||||||
|
|
||||||
fn ensureLookup(lib: *std.DynLib, comptime T: type, name: [:0]const u8) !T {
|
|
||||||
return lib.lookup(T, name) orelse return error.MissingSymbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getCallbacksFromLibrary(lib: *std.DynLib) !GameCallbacks {
|
|
||||||
return GameCallbacks{
|
|
||||||
.init = try ensureLookup(lib, GameCallbacks.InitFn, "init"),
|
|
||||||
.tick = try ensureLookup(lib, GameCallbacks.TickFn, "tick"),
|
|
||||||
.deinit = try ensureLookup(lib, GameCallbacks.DeinitFn, "deinit"),
|
|
||||||
.debug = if (build_options.has_imgui) try ensureLookup(lib, GameCallbacks.DebugFn, "debug") else {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(gpa: std.mem.Allocator, path: []const u8) !Dynamic {
|
|
||||||
const dir_path_z = try gpa.dupeZ(u8, std.fs.path.dirname(path) orelse ".");
|
|
||||||
errdefer gpa.free(dir_path_z);
|
|
||||||
|
|
||||||
const path_dupe = try gpa.dupe(u8, path);
|
|
||||||
errdefer gpa.free(path_dupe);
|
|
||||||
|
|
||||||
const fan_fd: std.c.fd_t = @bitCast(@as(u32, @truncate(
|
|
||||||
std.os.linux.fanotify_init(.{
|
|
||||||
.REPORT_FID = true,
|
|
||||||
.REPORT_DIR_FID = true,
|
|
||||||
.REPORT_NAME = true,
|
|
||||||
.NONBLOCK = true
|
|
||||||
}, @intFromEnum(std.posix.ACCMODE.RDONLY))
|
|
||||||
)));
|
|
||||||
if (fan_fd == -1) {
|
|
||||||
return error.fanotify_init;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mark_err = std.os.linux.fanotify_mark(
|
|
||||||
@intCast(fan_fd),
|
|
||||||
.{ .ADD = true, .ONLYDIR = true },
|
|
||||||
.{
|
|
||||||
.CLOSE_WRITE = true,
|
|
||||||
.CLOSE_NOWRITE = true,
|
|
||||||
.CREATE = true,
|
|
||||||
.MOVED_TO = true,
|
|
||||||
|
|
||||||
.EVENT_ON_CHILD = true,
|
|
||||||
.ONDIR = true,
|
|
||||||
},
|
|
||||||
@intCast(std.c.AT.FDCWD),
|
|
||||||
dir_path_z
|
|
||||||
);
|
|
||||||
assert(mark_err == 0);
|
|
||||||
|
|
||||||
const lib = try std.DynLib.open(path);
|
|
||||||
|
|
||||||
return Dynamic{
|
|
||||||
.fan_fd = fan_fd,
|
|
||||||
.dir_path = dir_path_z,
|
|
||||||
.path = path_dupe,
|
|
||||||
.lib = lib,
|
|
||||||
.last_event_at = null,
|
|
||||||
.reload_index = 0,
|
|
||||||
.ignore_next_event = false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Dynamic, gpa: std.mem.Allocator) void {
|
|
||||||
self.lib.close();
|
|
||||||
gpa.free(self.dir_path);
|
|
||||||
gpa.free(self.path);
|
|
||||||
std.posix.close(self.fan_fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getCallbacks(self: *Dynamic) !GameCallbacks {
|
|
||||||
return getCallbacksFromLibrary(&self.lib);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn checkForChanges(self: *Dynamic) !bool {
|
|
||||||
const fanotify = std.os.linux.fanotify;
|
|
||||||
const M = fanotify.event_metadata;
|
|
||||||
const fan_fd = self.fan_fd;
|
|
||||||
|
|
||||||
var need_to_reload = false;
|
|
||||||
|
|
||||||
// TODO: clean this up.
|
|
||||||
// Reuse the same watching logic for assets
|
|
||||||
var events_buf: [256 + 4096]u8 = undefined;
|
|
||||||
while (true) {
|
|
||||||
var len = std.posix.read(fan_fd, &events_buf) catch |err| switch (err) {
|
|
||||||
error.WouldBlock => break,
|
|
||||||
else => |e| return e,
|
|
||||||
};
|
|
||||||
var meta: [*]align(1) M = @ptrCast(&events_buf);
|
|
||||||
|
|
||||||
while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({
|
|
||||||
len -= meta[0].event_len;
|
|
||||||
meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len);
|
|
||||||
}) {
|
|
||||||
assert(meta[0].vers == M.VERSION);
|
|
||||||
if (meta[0].mask.Q_OVERFLOW) {
|
|
||||||
need_to_reload = true;
|
|
||||||
// TODO:
|
|
||||||
// std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var is_lib_event = false;
|
|
||||||
const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
|
|
||||||
switch (fid.hdr.info_type) {
|
|
||||||
.DFID_NAME => {
|
|
||||||
const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle);
|
|
||||||
const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
|
|
||||||
const file_name = std.mem.span(file_name_z);
|
|
||||||
|
|
||||||
const lib_name = std.fs.path.basename(self.path);
|
|
||||||
is_lib_event = std.mem.eql(u8,file_name, lib_name);
|
|
||||||
},
|
|
||||||
else => |t| log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_lib_event) {
|
|
||||||
need_to_reload = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (need_to_reload) {
|
|
||||||
if (self.ignore_next_event) {
|
|
||||||
self.ignore_next_event = false;
|
|
||||||
} else {
|
|
||||||
self.last_event_at = try std.time.Instant.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.last_event_at) |last_event_at| {
|
|
||||||
const now = try std.time.Instant.now();
|
|
||||||
const time_passed = now.since(last_event_at);
|
|
||||||
if (time_passed > event_debounce_ns) {
|
|
||||||
self.last_event_at = null;
|
|
||||||
|
|
||||||
var tmp_dir = try std.fs.openDirAbsolute("/tmp", .{});
|
|
||||||
defer tmp_dir.close();
|
|
||||||
|
|
||||||
var new_filename_buffer: [std.fs.max_name_bytes]u8 = undefined;
|
|
||||||
const tmp_filename = try std.fmt.bufPrint(
|
|
||||||
&new_filename_buffer,
|
|
||||||
"{s}_{}.so",
|
|
||||||
.{std.fs.path.stem(self.path), self.reload_index}
|
|
||||||
);
|
|
||||||
|
|
||||||
try std.fs.Dir.copyFile(
|
|
||||||
std.fs.cwd(), self.path,
|
|
||||||
tmp_dir, tmp_filename,
|
|
||||||
.{}
|
|
||||||
);
|
|
||||||
|
|
||||||
var tmp_path_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
|
||||||
const tmp_path = try std.fmt.bufPrint(
|
|
||||||
&tmp_path_buffer,
|
|
||||||
"/tmp/{s}",
|
|
||||||
.{tmp_filename}
|
|
||||||
);
|
|
||||||
|
|
||||||
var new_lib = try std.DynLib.open(tmp_path);
|
|
||||||
const new_callbacks = try getCallbacksFromLibrary(&new_lib);
|
|
||||||
// TODO:
|
|
||||||
_ = new_callbacks; // autofix
|
|
||||||
|
|
||||||
try tmp_dir.deleteFile(tmp_filename);
|
|
||||||
|
|
||||||
log.debug("Reload game code", .{});
|
|
||||||
self.lib.close();
|
|
||||||
self.lib = new_lib;
|
|
||||||
self.reload_index += 1;
|
|
||||||
self.ignore_next_event = true;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn getStatic() ?GameCallbacks {
|
|
||||||
const game = @import("game");
|
|
||||||
return GameCallbacks{
|
|
||||||
.init = game.init,
|
|
||||||
.deinit = game.deinit,
|
|
||||||
.tick = game.tick,
|
|
||||||
.debug = if (build_options.has_imgui) game.debug else {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,819 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const log = std.log.scoped(.engine);
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
|
|
||||||
const sokol = @import("sokol");
|
|
||||||
const sapp = sokol.app;
|
|
||||||
|
|
||||||
const Input = @import("./input.zig");
|
|
||||||
|
|
||||||
const ScreenScalar = @import("./screen_scaler.zig");
|
|
||||||
pub const imgui = @import("./imgui.zig");
|
|
||||||
pub const Graphics = @import("./graphics.zig");
|
|
||||||
pub const CLI = @import("./cli.zig");
|
|
||||||
const Audio = @import("./audio.zig");
|
|
||||||
const tracy = @import("tracy");
|
|
||||||
const builtin = @import("builtin");
|
|
||||||
const STBImage = @import("stb_image");
|
|
||||||
|
|
||||||
const Gfx = Graphics;
|
|
||||||
|
|
||||||
pub const Lib = @import("engine_lib");
|
|
||||||
|
|
||||||
const build_options = Lib.build_options;
|
|
||||||
const debug_keybinds = (builtin.mode == .Debug);
|
|
||||||
const GameCallbacks = Lib.Callbacks;
|
|
||||||
pub const Math = Lib.Math;
|
|
||||||
pub const Vec2 = Math.Vec2;
|
|
||||||
const rgb = Math.rgb;
|
|
||||||
const Vec4 = Math.Vec4;
|
|
||||||
|
|
||||||
const DebugSystem = @import("./debug_system.zig");
|
|
||||||
|
|
||||||
const GameLinking = @import("game_linking.zig");
|
|
||||||
|
|
||||||
const Environment = @import("environment.zig");
|
|
||||||
|
|
||||||
const Engine = @This();
|
|
||||||
|
|
||||||
const IniReader = struct {
|
|
||||||
reader: *std.Io.Reader,
|
|
||||||
|
|
||||||
const Tag = union(enum) {
|
|
||||||
section: []const u8,
|
|
||||||
option: struct {
|
|
||||||
key: []const u8,
|
|
||||||
value: []const u8
|
|
||||||
},
|
|
||||||
|
|
||||||
pub fn isOption(self: Tag, key: []const u8) ?[]const u8 {
|
|
||||||
if (self == .option and std.mem.eql(u8, self.option.key, key)) {
|
|
||||||
return self.option.value;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn next(self: *IniReader) !?Tag {
|
|
||||||
while (true) {
|
|
||||||
var line = self.reader.takeDelimiterInclusive('\n') catch |e| switch (e) {
|
|
||||||
error.EndOfStream => break,
|
|
||||||
else => return e
|
|
||||||
};
|
|
||||||
line = @constCast(std.mem.trim(u8, line, &std.ascii.whitespace));
|
|
||||||
if (std.mem.startsWith(u8, line, "#")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.len == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.startsWith(u8, line, "[") and std.mem.endsWith(u8, line, "]")) {
|
|
||||||
const section_name = line[1..(line.len-1)];
|
|
||||||
return Tag{
|
|
||||||
.section = std.mem.trim(u8, section_name, &std.ascii.whitespace)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (std.mem.indexOfScalar(u8, line, '=')) |equals_index| {
|
|
||||||
const key = line[0..equals_index];
|
|
||||||
const value = line[(equals_index+1)..];
|
|
||||||
|
|
||||||
return Tag{
|
|
||||||
.option = .{
|
|
||||||
.key = std.mem.trim(u8, key, &std.ascii.whitespace),
|
|
||||||
.value = std.mem.trim(u8, value, &std.ascii.whitespace),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.InvalidLine;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const LoadOptions = struct {
|
|
||||||
asset_dir: std.fs.Dir,
|
|
||||||
ini_reader: *IniReader,
|
|
||||||
arena: *std.heap.ArenaAllocator
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const AssetSystem = struct {
|
|
||||||
assets: std.ArrayList(Lib.Asset),
|
|
||||||
|
|
||||||
pub fn register(self: AssetSystem) void {
|
|
||||||
_ = self; // autofix
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Subsystems = struct {
|
|
||||||
graphics: *Graphics,
|
|
||||||
audio: *Audio,
|
|
||||||
assets: *AssetSystem,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ImageAsset = struct {
|
|
||||||
pub fn load(subsystems: Subsystems, opts: LoadOptions) !void {
|
|
||||||
const ini_reader = opts.ini_reader;
|
|
||||||
const arena = opts.arena;
|
|
||||||
const dir = opts.asset_dir;
|
|
||||||
|
|
||||||
var maybe_image_path: ?[]const u8 = null;
|
|
||||||
while (try ini_reader.next()) |tag| {
|
|
||||||
if (tag.isOption("path")) |opt| {
|
|
||||||
maybe_image_path = try arena.allocator().dupe(u8, opt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maybe_image_path) |image_path| {
|
|
||||||
const image_data = try readFileAll(arena.allocator(), dir, image_path);
|
|
||||||
const image = try STBImage.load(image_data);
|
|
||||||
defer image.deinit();
|
|
||||||
std.debug.print("{s}\n", .{image_path});
|
|
||||||
const texture_id = try subsystems.graphics.addTexture();
|
|
||||||
try subsystems.graphics.setTextureData(texture_id, .{
|
|
||||||
.width = image.width,
|
|
||||||
.height = image.height,
|
|
||||||
.rgba = image.rgba8_pixels,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// subsystems.assets.register("icon");
|
|
||||||
// try subsystems.assets.append(self.assets_arena.allocator(), Lib.Asset{
|
|
||||||
// .id = @enumFromInt(self.assets.items.len),
|
|
||||||
// .key = try self.assets_arena.allocator().dupe(u8, asset_id),
|
|
||||||
// .data = &[_]u8{}
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
graphics: Graphics,
|
|
||||||
audio: Audio,
|
|
||||||
|
|
||||||
seed: u64,
|
|
||||||
environment: Environment,
|
|
||||||
|
|
||||||
assets_arena: std.heap.ArenaAllocator,
|
|
||||||
assets: std.ArrayList(Lib.Asset),
|
|
||||||
|
|
||||||
queued_events: std.ArrayList(Input.Event),
|
|
||||||
is_mouse_inside: bool,
|
|
||||||
|
|
||||||
debug: DebugSystem,
|
|
||||||
|
|
||||||
pub const RunOptions = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
do_recording: bool = (builtin.mode == .Debug),
|
|
||||||
screen_width: u32 = 640,
|
|
||||||
screen_height: u32 = 480,
|
|
||||||
|
|
||||||
dynamic_library_path: ?[]const u8 = null,
|
|
||||||
assets_path: ?[]const u8 = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn run(self: *Engine, opts: RunOptions) !void {
|
|
||||||
var debug_system = DebugSystem.init(opts.allocator);
|
|
||||||
|
|
||||||
var maybe_callbacks: ?GameCallbacks = null;
|
|
||||||
if (build_options.code_static_linking) {
|
|
||||||
maybe_callbacks = GameLinking.getStatic();
|
|
||||||
}
|
|
||||||
if (build_options.code_dynamic_linking and opts.dynamic_library_path != null) {
|
|
||||||
var dynamic = try GameLinking.Dynamic.init(opts.allocator, opts.dynamic_library_path.?);
|
|
||||||
maybe_callbacks = try dynamic.getCallbacks();
|
|
||||||
debug_system.dynamic_linking = dynamic;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maybe_callbacks == null) {
|
|
||||||
return error.NotLinked;
|
|
||||||
}
|
|
||||||
const callbacks = maybe_callbacks.?;
|
|
||||||
|
|
||||||
const initial = Environment.Init{
|
|
||||||
.seed = @bitCast(std.time.milliTimestamp()),
|
|
||||||
.screen_size = .initFromInt(u32, opts.screen_width, opts.screen_height)
|
|
||||||
};
|
|
||||||
|
|
||||||
self.* = Engine{
|
|
||||||
.allocator = opts.allocator,
|
|
||||||
.graphics = try .init(.{
|
|
||||||
.allocator = opts.allocator,
|
|
||||||
.logger = .{ .func = sokolLogCallback },
|
|
||||||
// TODO:
|
|
||||||
// .imgui_font = .{
|
|
||||||
// .ttf_data = @embedFile("../assets/roboto-font/Roboto-Regular.ttf"),
|
|
||||||
// }
|
|
||||||
}),
|
|
||||||
.audio = try .init(.{
|
|
||||||
.allocator = opts.allocator,
|
|
||||||
.logger = .{ .func = sokolLogCallback },
|
|
||||||
}),
|
|
||||||
.environment = .init(opts.allocator, callbacks),
|
|
||||||
.queued_events = .empty,
|
|
||||||
.seed = initial.seed,
|
|
||||||
.is_mouse_inside = false,
|
|
||||||
.debug = debug_system,
|
|
||||||
.assets = .empty,
|
|
||||||
.assets_arena = std.heap.ArenaAllocator.init(opts.allocator)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (opts.assets_path) |assets_path| {
|
|
||||||
var temp_arena = std.heap.ArenaAllocator.init(opts.allocator);
|
|
||||||
defer temp_arena.deinit();
|
|
||||||
|
|
||||||
var result_arena = std.heap.ArenaAllocator.init(opts.allocator);
|
|
||||||
defer result_arena.deinit();
|
|
||||||
|
|
||||||
var dir = try std.fs.openDirAbsolute(assets_path, .{ .iterate = false });
|
|
||||||
defer dir.close();
|
|
||||||
|
|
||||||
const result = try listFilesRecursively(assets_path, temp_arena.allocator(), result_arena.allocator(), ".ini");
|
|
||||||
for (result.items) |path| {
|
|
||||||
const path_without_ini = path[0..(path.len-4)];
|
|
||||||
const index_of_dot = std.mem.lastIndexOfScalar(u8, path_without_ini, '.') orelse continue;
|
|
||||||
const asset_type = path_without_ini[(index_of_dot+1)..];
|
|
||||||
const asset_id = path_without_ini[0..index_of_dot];
|
|
||||||
|
|
||||||
std.debug.print("{s} {s}\n", .{asset_id, asset_type});
|
|
||||||
if (std.mem.eql(u8, asset_type, "image")) {
|
|
||||||
const f = try dir.openFile(path, .{ });
|
|
||||||
defer f.close();
|
|
||||||
|
|
||||||
var reader_buffer: [Math.bytes_per_kib * 4]u8 = undefined;
|
|
||||||
var file_reader = f.reader(&reader_buffer);
|
|
||||||
var ini_reader = IniReader{ .reader = &file_reader.interface };
|
|
||||||
|
|
||||||
var asset_system = AssetSystem{
|
|
||||||
.assets = .empty
|
|
||||||
};
|
|
||||||
try ImageAsset.load(.{
|
|
||||||
.audio = &self.audio,
|
|
||||||
.graphics = &self.graphics,
|
|
||||||
.assets = &asset_system
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.arena = &temp_arena,
|
|
||||||
.ini_reader = &ini_reader,
|
|
||||||
.asset_dir = dir
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// for (result)
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.assets.append(self.allocator, Lib.Asset{
|
|
||||||
.data = &.{}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.environment.stateInit(initial, .{
|
|
||||||
.items = self.assets.items
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// if (opts.do_recording) {
|
|
||||||
// self.recording = .init(opts.allocator, initial);
|
|
||||||
// }
|
|
||||||
|
|
||||||
tracy.setThreadName("Main");
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("Build options:", .{});
|
|
||||||
inline for (@typeInfo(build_options).@"struct".decls) |decl| {
|
|
||||||
const value = @field(build_options, decl.name);
|
|
||||||
if (@TypeOf(value) == []const u8) {
|
|
||||||
log.debug("- {s}: {s}", .{decl.name, value});
|
|
||||||
} else {
|
|
||||||
log.debug("- {s}: {}", .{decl.name, value});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Don't hard code icon path, allow changing through options
|
|
||||||
// var icon_data = try STBImage.load(@embedFile("../assets/icon.png"));
|
|
||||||
// defer icon_data.deinit();
|
|
||||||
|
|
||||||
var icon: sapp.IconDesc = .{};
|
|
||||||
icon.sokol_default = true;
|
|
||||||
// TODO:
|
|
||||||
// icon.images[0] = .{
|
|
||||||
// .width = @intCast(icon_data.width),
|
|
||||||
// .height = @intCast(icon_data.height),
|
|
||||||
// .pixels = .{
|
|
||||||
// .ptr = icon_data.rgba8_pixels,
|
|
||||||
// .size = icon_data.width * icon_data.height * 4
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
sapp.run(.{
|
|
||||||
.init_userdata_cb = sokolInitCallback,
|
|
||||||
.frame_userdata_cb = sokolFrameCallback,
|
|
||||||
.cleanup_userdata_cb = sokolCleanupCallback,
|
|
||||||
.event_userdata_cb = sokolEventCallback,
|
|
||||||
.user_data = self,
|
|
||||||
.width = @intCast(opts.screen_width),
|
|
||||||
.height = @intCast(opts.screen_height),
|
|
||||||
.icon = icon,
|
|
||||||
.window_title = "Game",
|
|
||||||
.logger = .{ .func = sokolLogCallback },
|
|
||||||
.win32 = .{
|
|
||||||
.console_utf8 = true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn listFilesRecursively(
|
|
||||||
absolute_path: []const u8,
|
|
||||||
temp_allocator: std.mem.Allocator,
|
|
||||||
result_arena: std.mem.Allocator,
|
|
||||||
extension: []const u8
|
|
||||||
) !std.ArrayList([]u8) {
|
|
||||||
var result: std.ArrayList([]u8) = .empty;
|
|
||||||
var dir = try std.fs.openDirAbsolute(absolute_path, .{ .iterate = true });
|
|
||||||
defer dir.close();
|
|
||||||
|
|
||||||
var walker = try dir.walk(temp_allocator);
|
|
||||||
defer walker.deinit();
|
|
||||||
|
|
||||||
while (try walker.next()) |entry| {
|
|
||||||
if (entry.kind == .file) {
|
|
||||||
if (std.mem.endsWith(u8, entry.path, extension)) {
|
|
||||||
try result.append(result_arena, try result_arena.dupe(u8, entry.path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn readFileAll(gpa: std.mem.Allocator, dir: std.fs.Dir, sub_path: []const u8) ![]u8 {
|
|
||||||
const f = try dir.openFile(sub_path, .{});
|
|
||||||
defer f.close();
|
|
||||||
|
|
||||||
var wa: std.Io.Writer.Allocating = .init(gpa);
|
|
||||||
defer wa.deinit();
|
|
||||||
|
|
||||||
var buffer: [4 * 4096]u8 = undefined;
|
|
||||||
var reader = f.reader(&buffer);
|
|
||||||
_ = try reader.interface.stream(&wa.writer, .unlimited);
|
|
||||||
|
|
||||||
return try wa.toOwnedSlice();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sokolInit(self: *Engine) !void {
|
|
||||||
const zone = tracy.initZone(@src(), .{ });
|
|
||||||
defer zone.deinit();
|
|
||||||
|
|
||||||
try self.graphics.setupSokol();
|
|
||||||
self.audio.setupSokol();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sokolCleanup(self: *Engine) void {
|
|
||||||
const zone = tracy.initZone(@src(), .{ });
|
|
||||||
defer zone.deinit();
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// if (self.recording) |recording| {
|
|
||||||
// recording.deinit();
|
|
||||||
// }
|
|
||||||
|
|
||||||
self.queued_events.deinit(self.allocator);
|
|
||||||
self.environment.stateDeinit();
|
|
||||||
self.environment.deinit();
|
|
||||||
self.audio.deinit();
|
|
||||||
self.graphics.deinit(self.allocator);
|
|
||||||
self.debug.deinit();
|
|
||||||
self.assets_arena.deinit();
|
|
||||||
// TODO:
|
|
||||||
// self.game_linking.deinit(self.allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sokolFrame(self: *Engine) !void {
|
|
||||||
tracy.frameMark();
|
|
||||||
|
|
||||||
const zone = tracy.initZone(@src(), .{ });
|
|
||||||
defer zone.deinit();
|
|
||||||
|
|
||||||
if (self.debug.dynamic_linking) |*dynamic| {
|
|
||||||
if (try dynamic.checkForChanges()) {
|
|
||||||
self.environment.callbacks = try dynamic.getCallbacks();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = std.time.Instant.now() catch @panic("Instant.now() unsupported");
|
|
||||||
const time_passed = now.since(self.environment.started_at);
|
|
||||||
const dt_ns = time_passed - self.environment.last_frame_at;
|
|
||||||
self.environment.last_frame_at = time_passed;
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// if (self.play_recording) {
|
|
||||||
// assert(self.recording != null);
|
|
||||||
// if (self.recording_tick >= self.recording.?.ticks.items.len) {
|
|
||||||
// self.play_recording = false;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
var tick: Environment.Tick = undefined;
|
|
||||||
// if (self.play_recording) {
|
|
||||||
// assert(self.recording != null);
|
|
||||||
// const ticks = self.recording.?.ticks.items;
|
|
||||||
// tick = ticks[self.recording_tick];
|
|
||||||
// self.recording_tick += 1;
|
|
||||||
// } else {
|
|
||||||
tick = Environment.Tick{
|
|
||||||
.dt_ns = dt_ns,
|
|
||||||
.events = self.queued_events.items
|
|
||||||
};
|
|
||||||
//
|
|
||||||
// if (self.recording) |*recording| {
|
|
||||||
// try recording.appendTick(tick);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const tick_result = try self.environment.stateTick(tick);
|
|
||||||
|
|
||||||
self.queued_events.clearRetainingCapacity();
|
|
||||||
|
|
||||||
{
|
|
||||||
sapp.showMouse(!tick_result.cursor_hidden);
|
|
||||||
|
|
||||||
{
|
|
||||||
try self.graphics.beginFrame();
|
|
||||||
defer self.graphics.endFrame(tick_result.clear_color);
|
|
||||||
|
|
||||||
self.graphics.drawCommands(tick_result.graphics);
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// if (self.show_debug and build_options.has_imgui) {
|
|
||||||
// try self.showDebugWindow();
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
for (tick_result.audio) |command| {
|
|
||||||
try self.audio.thread_state.commands.push(command);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// if (self.restart_game) {
|
|
||||||
// self.restart_game = false;
|
|
||||||
//
|
|
||||||
// const initial = Environment.Init{
|
|
||||||
// .seed = self.seed,
|
|
||||||
// .screen_size = .init(sapp.widthf(), sapp.heightf())
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// self.environment.runGameDeinit(self.game_linking.callbacks.deinit);
|
|
||||||
// self.environment.deinit();
|
|
||||||
//
|
|
||||||
// self.environment = .init(self.allocator);
|
|
||||||
// self.environment.runGameInit(self.game_linking.callbacks.init, initial);
|
|
||||||
//
|
|
||||||
// // TODO:
|
|
||||||
// // if (self.recording) |recording| {
|
|
||||||
// // recording.deinit();
|
|
||||||
// // self.recording = .init(self.allocator, initial);
|
|
||||||
// // }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn showDebugWindow(self: *Engine) !void {
|
|
||||||
if (!imgui.beginWindow(.{
|
|
||||||
.name = "Debug",
|
|
||||||
.pos = Vec2.init(20, 20),
|
|
||||||
.size = Vec2.init(200, 200),
|
|
||||||
})) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
defer imgui.endWindow();
|
|
||||||
|
|
||||||
_ = imgui.beginTabBar("debug");
|
|
||||||
defer imgui.endTabBar();
|
|
||||||
|
|
||||||
if (imgui.beginTabItem("Game")) {
|
|
||||||
defer imgui.endTabItem();
|
|
||||||
|
|
||||||
var imgui_ctx = Lib.ImGui.init(self.allocator);
|
|
||||||
defer imgui_ctx.deinit();
|
|
||||||
|
|
||||||
self.environment.runGameDebug(self.game_linking.callbacks.debug, &imgui_ctx);
|
|
||||||
for (imgui_ctx.commands.items) |cmd| {
|
|
||||||
switch (cmd) {
|
|
||||||
.text => |str| imgui.text(str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imgui.beginTabItem("Engine")) {
|
|
||||||
defer imgui.endTabItem();
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// if (imgui.button("Restart")) {
|
|
||||||
// self.restart_game = true;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// if (self.recording) |recording| {
|
|
||||||
// imgui.beginDisabled(self.play_recording);
|
|
||||||
// defer imgui.endDisabled();
|
|
||||||
//
|
|
||||||
// if (imgui.button("Replay")) {
|
|
||||||
// self.environment.runGameDeinit(self.game_linking.callbacks.deinit);
|
|
||||||
// self.environment.deinit();
|
|
||||||
//
|
|
||||||
// self.environment = .init(self.allocator);
|
|
||||||
// self.environment.runGameInit(self.game_linking.callbacks.init, recording.initial);
|
|
||||||
//
|
|
||||||
// self.play_recording = true;
|
|
||||||
// self.recording_tick = 0;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// if (self.game_linking.kind == .dynamic) {
|
|
||||||
// imgui.textFmt("Linking: dynamic (from {s})", .{ self.game_linking.kind.dynamic.path });
|
|
||||||
// } else {
|
|
||||||
// imgui.textFmt("Linking: static", .{ });
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (build_options.asset_hot_reload) {
|
|
||||||
imgui.textFmt("Asset loading: runtime (from {s})", .{build_options.asset_dir});
|
|
||||||
} else {
|
|
||||||
imgui.textFmt("Asset loading: builtin", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
imgui.textFmt("Seed: 0x{x:08}", .{ self.seed });
|
|
||||||
|
|
||||||
const time_ms: f64 = @floatFromInt(@divFloor(self.environment.last_frame_at, std.time.ns_per_ms));
|
|
||||||
imgui.textFmt("Time: {:.2}", .{ time_ms / 1000 });
|
|
||||||
|
|
||||||
// imgui.textFmt("Draw commands: {}\n", .{
|
|
||||||
// frame.graphics_commands.items.len,
|
|
||||||
// });
|
|
||||||
// TODO:
|
|
||||||
// imgui.textFmt("Audio instances: {}/{}\n", .{
|
|
||||||
// Audio.mixer.instances.items.len,
|
|
||||||
// Audio.mixer.instances.capacity
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
|
|
||||||
const zone = tracy.initZone(@src(), .{ });
|
|
||||||
defer zone.deinit();
|
|
||||||
|
|
||||||
const e = e_ptr.*;
|
|
||||||
|
|
||||||
if (imgui.handleEvent(e)) {
|
|
||||||
if (self.is_mouse_inside) {
|
|
||||||
try self.queued_events.append(self.allocator, .{
|
|
||||||
.mouse_leave = {}
|
|
||||||
});
|
|
||||||
self.is_mouse_inside = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debug_keybinds and e.type == .KEY_DOWN) {
|
|
||||||
if (e.key_code == .F4) {
|
|
||||||
// TODO:
|
|
||||||
// self.show_debug = !self.show_debug;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key_code == .F5) {
|
|
||||||
// TODO:
|
|
||||||
// self.restart_game = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// if (self.play_recording) {
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
blk: switch (e.type) {
|
|
||||||
.MOUSE_DOWN => {
|
|
||||||
const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk;
|
|
||||||
|
|
||||||
try self.queued_events.append(self.allocator, .{
|
|
||||||
.mouse_pressed = .{
|
|
||||||
.button = mouse_button,
|
|
||||||
.position = Vec2.init(e.mouse_x, e.mouse_y)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.MOUSE_UP => {
|
|
||||||
const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk;
|
|
||||||
|
|
||||||
try self.queued_events.append(self.allocator, .{
|
|
||||||
.mouse_released = .{
|
|
||||||
.button = mouse_button,
|
|
||||||
.position = Vec2.init(e.mouse_x, e.mouse_y)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.MOUSE_MOVE => {
|
|
||||||
if (self.is_mouse_inside) {
|
|
||||||
try self.queued_events.append(self.allocator, .{
|
|
||||||
.mouse_move = Vec2.init(e.mouse_x, e.mouse_y)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
try self.queued_events.append(self.allocator, .{
|
|
||||||
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
|
|
||||||
});
|
|
||||||
self.is_mouse_inside = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.MOUSE_ENTER => {
|
|
||||||
if (!self.is_mouse_inside) {
|
|
||||||
try self.queued_events.append(self.allocator, .{
|
|
||||||
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
|
|
||||||
});
|
|
||||||
self.is_mouse_inside = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.RESIZED => {
|
|
||||||
if (self.is_mouse_inside) {
|
|
||||||
try self.queued_events.append(self.allocator, .{
|
|
||||||
.mouse_leave = {}
|
|
||||||
});
|
|
||||||
self.is_mouse_inside = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.queued_events.append(self.allocator, .{
|
|
||||||
.window_resize = .{
|
|
||||||
.width = @intCast(e.window_width),
|
|
||||||
.height = @intCast(e.window_height)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.MOUSE_LEAVE => {
|
|
||||||
if (self.is_mouse_inside) {
|
|
||||||
try self.queued_events.append(self.allocator, .{
|
|
||||||
.mouse_leave = {}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.is_mouse_inside = false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.MOUSE_SCROLL => {
|
|
||||||
try self.queued_events.append(self.allocator, .{
|
|
||||||
.mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y)
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.KEY_DOWN => {
|
|
||||||
const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk;
|
|
||||||
|
|
||||||
try self.queued_events.append(self.allocator, .{
|
|
||||||
.key_pressed = .{
|
|
||||||
.code = key_code,
|
|
||||||
.repeat = e.key_repeat
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.KEY_UP => {
|
|
||||||
const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk;
|
|
||||||
|
|
||||||
try self.queued_events.append(self.allocator, .{
|
|
||||||
.key_released = key_code
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.CHAR => {
|
|
||||||
try self.queued_events.append(self.allocator, .{
|
|
||||||
.char = @intCast(e.char_code)
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.QUIT_REQUESTED => {
|
|
||||||
// TODO: handle quit request. Maybe show confirmation window in certain cases.
|
|
||||||
},
|
|
||||||
else => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sokolEventCallback(e_ptr: [*c]const sapp.Event, userdata: ?*anyopaque) callconv(.c) void {
|
|
||||||
const engine: *Engine = @alignCast(@ptrCast(userdata));
|
|
||||||
|
|
||||||
const consume_event = engine.sokolEvent(e_ptr) catch |e| blk: {
|
|
||||||
log.err("sokolEvent() failed: {}", .{e});
|
|
||||||
if (@errorReturnTrace()) |trace| {
|
|
||||||
std.debug.dumpStackTrace(trace.*);
|
|
||||||
}
|
|
||||||
break :blk false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (consume_event) {
|
|
||||||
sapp.consumeEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sokolCleanupCallback(user_data: ?*anyopaque) callconv(.c) void {
|
|
||||||
const engine: *Engine = @alignCast(@ptrCast(user_data));
|
|
||||||
|
|
||||||
engine.sokolCleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sokolInitCallback(user_data: ?*anyopaque) callconv(.c) void {
|
|
||||||
const engine: *Engine = @alignCast(@ptrCast(user_data));
|
|
||||||
|
|
||||||
engine.sokolInit() catch |e| {
|
|
||||||
log.err("sokolInit() failed: {}", .{e});
|
|
||||||
if (@errorReturnTrace()) |trace| {
|
|
||||||
std.debug.dumpStackTrace(trace.*);
|
|
||||||
}
|
|
||||||
sapp.requestQuit();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sokolFrameCallback(user_data: ?*anyopaque) callconv(.c) void {
|
|
||||||
const engine: *Engine = @alignCast(@ptrCast(user_data));
|
|
||||||
|
|
||||||
engine.sokolFrame() catch |e| {
|
|
||||||
log.err("sokolFrame() failed: {}", .{e});
|
|
||||||
if (@errorReturnTrace()) |trace| {
|
|
||||||
std.debug.dumpStackTrace(trace.*);
|
|
||||||
}
|
|
||||||
sapp.requestQuit();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void {
|
|
||||||
const log_sokol = std.log.scoped(.sokol);
|
|
||||||
|
|
||||||
if (log_level == 0) {
|
|
||||||
log_sokol.err(format, args);
|
|
||||||
} else if (log_level == 1) {
|
|
||||||
log_sokol.err(format, args);
|
|
||||||
} else if (log_level == 2) {
|
|
||||||
log_sokol.warn(format, args);
|
|
||||||
} else {
|
|
||||||
log_sokol.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 "")
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn posixSignalHandler(sig: i32) callconv(.c) void {
|
|
||||||
_ = sig;
|
|
||||||
sapp.requestQuit();
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
id: Id,
|
|
||||||
info: Info,
|
|
||||||
|
|
||||||
pub const Info = extern struct {
|
|
||||||
format: Format,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Format = enum(u8) {
|
|
||||||
ttf
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Id = enum(u16) {
|
|
||||||
_,
|
|
||||||
|
|
||||||
pub const nil: Id = @enumFromInt(std.math.maxInt(@typeInfo(Id).@"enum".tag_type));
|
|
||||||
};
|
|
||||||
@ -1,489 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const tracy = @import("tracy");
|
|
||||||
const builtin = @import("builtin");
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
/// This array provides:
|
|
||||||
/// - O(1) insertion
|
|
||||||
/// - Unique IDs for every inserted item (assuming that generation doesn't overflow)
|
|
||||||
/// TODO: Use a stack of unused indexes to implement O(1) insertion
|
|
||||||
/// TODO: Implement a way to use a list *assumeCapacity() style of functions.
|
|
||||||
pub fn SlotMap(
|
|
||||||
Index: type,
|
|
||||||
Generation: type,
|
|
||||||
Item: type
|
|
||||||
) type {
|
|
||||||
assert(@bitSizeOf(Generation) % 8 == 0);
|
|
||||||
assert(@bitSizeOf(Index) % 8 == 0);
|
|
||||||
|
|
||||||
return struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
items: [*]Item,
|
|
||||||
generations: [*]Generation,
|
|
||||||
unused: [*]u8,
|
|
||||||
|
|
||||||
len: u32,
|
|
||||||
capacity: u32,
|
|
||||||
|
|
||||||
pub const empty = Self{
|
|
||||||
.items = &[_]Item{},
|
|
||||||
.generations = &[_]Generation{},
|
|
||||||
.unused = &[_]u8{},
|
|
||||||
.capacity = 0,
|
|
||||||
.len = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Id = packed struct {
|
|
||||||
pub const Int = @Type(.{
|
|
||||||
.int = .{
|
|
||||||
.bits = @bitSizeOf(Id),
|
|
||||||
.signedness = .unsigned
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
generation: Generation,
|
|
||||||
index: Index,
|
|
||||||
|
|
||||||
// TODO: Maybe `Id.Optional` type should be created to ensure .wrap() and .toOptional()
|
|
||||||
pub const none = Id{
|
|
||||||
.generation = std.math.maxInt(Generation),
|
|
||||||
.index = std.math.maxInt(Index),
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn format(self: Id, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
|
||||||
if (self == Id.none) {
|
|
||||||
try writer.print("Id({s}){{ .none }}", .{ @typeName(Item) });
|
|
||||||
} else {
|
|
||||||
try writer.print("Id({s}){{ {}, {} }}", .{ @typeName(Item), self.index, self.generation });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn asInt(self: Id) Int {
|
|
||||||
return @bitCast(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fromInt(self: Int) Id {
|
|
||||||
return @bitCast(self);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const ItemWithId = struct {
|
|
||||||
id: Id,
|
|
||||||
item: *Item,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Iterator = struct {
|
|
||||||
array_list: *Self,
|
|
||||||
index: Index,
|
|
||||||
|
|
||||||
pub fn nextId(self: *Iterator) ?Id {
|
|
||||||
while (self.index < self.array_list.len) {
|
|
||||||
const index = self.index;
|
|
||||||
self.index += 1;
|
|
||||||
|
|
||||||
// TODO: Inline the `byte_index` calculate for better speed.
|
|
||||||
// Probably not needed. Idk
|
|
||||||
if (self.array_list.isUnused(index)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Id{
|
|
||||||
.index = @intCast(index),
|
|
||||||
.generation = self.array_list.generations[index]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn nextItem(self: *Iterator) ?*Item {
|
|
||||||
if (self.nextId()) |id| {
|
|
||||||
return &self.array_list.items[id.index];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next(self: *Iterator) ?ItemWithId {
|
|
||||||
if (self.nextId()) |id| {
|
|
||||||
return ItemWithId{
|
|
||||||
.id = id,
|
|
||||||
.item = &self.array_list.items[id.index]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Metadata = extern struct {
|
|
||||||
len: u32,
|
|
||||||
count: u32
|
|
||||||
};
|
|
||||||
|
|
||||||
fn divCeilGeneration(num: u32) u32 {
|
|
||||||
return std.math.divCeil(u32, num, @bitSizeOf(Generation)) catch unreachable;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn divFloorGeneration(num: u32) u32 {
|
|
||||||
return @divFloor(num, @bitSizeOf(Generation));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ensureTotalCapacityPrecise(self: *Self, allocator: Allocator, new_capacity: u32) !void {
|
|
||||||
if (new_capacity > std.math.maxInt(Index)) {
|
|
||||||
return error.OutOfIndexSpace;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Shrinking is not supported
|
|
||||||
assert(new_capacity >= self.capacity);
|
|
||||||
|
|
||||||
const unused_bit_array_len = divCeilGeneration(self.capacity);
|
|
||||||
const new_unused_bit_array_len = divCeilGeneration(new_capacity);
|
|
||||||
|
|
||||||
// TODO: Handle allocation failure case
|
|
||||||
const new_unused = try allocator.realloc(self.unused[0..unused_bit_array_len], new_unused_bit_array_len);
|
|
||||||
const new_items = try allocator.realloc(self.items[0..self.capacity], new_capacity);
|
|
||||||
const new_generations = try allocator.realloc(self.generations[0..self.capacity], new_capacity);
|
|
||||||
|
|
||||||
self.unused = new_unused.ptr;
|
|
||||||
self.items = new_items.ptr;
|
|
||||||
self.generations = new_generations.ptr;
|
|
||||||
self.capacity = new_capacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn growCapacity(current: u32, minimum: u32) u32 {
|
|
||||||
const init_capacity = @as(comptime_int, @max(1, std.atomic.cache_line / @sizeOf(Item)));
|
|
||||||
|
|
||||||
var new = current;
|
|
||||||
while (true) {
|
|
||||||
new +|= new / 2 + init_capacity;
|
|
||||||
if (new >= minimum) {
|
|
||||||
return new;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ensureTotalCapacity(self: *Self, allocator: Allocator, new_capacity: u32) !void {
|
|
||||||
if (self.capacity >= new_capacity) return;
|
|
||||||
|
|
||||||
const better_capacity = Self.growCapacity(self.capacity, new_capacity);
|
|
||||||
try self.ensureTotalCapacityPrecise(allocator, better_capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clearRetainingCapacity(self: *Self) void {
|
|
||||||
self.count = 0;
|
|
||||||
self.len = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ensureUnusedCapacity(self: *Self, allocator: Allocator, unused_capacity: u32) !void {
|
|
||||||
try self.ensureTotalCapacity(allocator, self.len + unused_capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn findFirstUnused(self: *Self) ?Index {
|
|
||||||
for (0..divCeilGeneration(self.len)) |byte_index| {
|
|
||||||
if (self.unused[byte_index] != 0) {
|
|
||||||
const found = @ctz(self.unused[byte_index]) + byte_index * @bitSizeOf(Generation);
|
|
||||||
if (found < self.len) {
|
|
||||||
return @intCast(found);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn markUnused(self: *Self, index: Index, unused: bool) void {
|
|
||||||
assert(index < self.len);
|
|
||||||
|
|
||||||
const byte_index = divFloorGeneration(index);
|
|
||||||
const bit_index = @mod(index, @bitSizeOf(Generation));
|
|
||||||
const bit_flag = @as(u8, 1) << @intCast(bit_index);
|
|
||||||
if (unused) {
|
|
||||||
self.unused[byte_index] |= bit_flag;
|
|
||||||
} else {
|
|
||||||
self.unused[byte_index] &= ~bit_flag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isUnused(self: *Self, index: Index) bool {
|
|
||||||
assert(index < self.len);
|
|
||||||
|
|
||||||
const byte_index = divFloorGeneration(index);
|
|
||||||
const bit_index = @mod(index, @bitSizeOf(Generation));
|
|
||||||
const bit_flag = @as(u8, 1) << @intCast(bit_index);
|
|
||||||
return (self.unused[byte_index] & bit_flag) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insertUndefined(self: *Self, allocator: Allocator) !Id {
|
|
||||||
var unused_index: Index = undefined;
|
|
||||||
|
|
||||||
if (self.findFirstUnused()) |index| {
|
|
||||||
unused_index = index;
|
|
||||||
} else {
|
|
||||||
try self.ensureUnusedCapacity(allocator, 1);
|
|
||||||
|
|
||||||
unused_index = @intCast(self.len);
|
|
||||||
self.len += 1;
|
|
||||||
self.generations[unused_index] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.markUnused(unused_index, false);
|
|
||||||
|
|
||||||
const id = Id{
|
|
||||||
.index = @intCast(unused_index),
|
|
||||||
.generation = self.generations[unused_index]
|
|
||||||
};
|
|
||||||
|
|
||||||
assert(id != Id.none);
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert(self: *Self, allocator: Allocator, item: Item) !Id {
|
|
||||||
const id = try self.insertUndefined(allocator);
|
|
||||||
|
|
||||||
const new_item_ptr = self.getAssumeExists(id);
|
|
||||||
new_item_ptr.* = item;
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exists(self: *Self, id: Id) bool {
|
|
||||||
if (id.index >= self.len) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.isUnused(id.index)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.generations[id.index] != id.generation) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn removeAssumeExists(self: *Self, id: Id) void {
|
|
||||||
assert(self.exists(id));
|
|
||||||
|
|
||||||
self.markUnused(id.index, true);
|
|
||||||
// TODO: Maybe a log should be shown when a wrap-around occurs?
|
|
||||||
self.generations[id.index] +%= 1;
|
|
||||||
self.count -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove(self: *Self, id: Id) bool {
|
|
||||||
if (!self.exists(id)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.removeAssumeExists(id);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getAssumeExists(self: *Self, id: Id) *Item {
|
|
||||||
assert(self.exists(id));
|
|
||||||
|
|
||||||
return &self.items[id.index];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(self: *Self, id: Id) ?*Item {
|
|
||||||
if (self.exists(id)) {
|
|
||||||
return self.getAssumeExists(id);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iterator(self: *Self) Iterator {
|
|
||||||
return Iterator{
|
|
||||||
.array_list = self,
|
|
||||||
.index = 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Self, allocator: Allocator) void {
|
|
||||||
allocator.free(self.unused[0..divCeilGeneration(self.capacity)]);
|
|
||||||
allocator.free(self.generations[0..self.capacity]);
|
|
||||||
allocator.free(self.items[0..self.capacity]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clone(self: *Self, allocator: Allocator) !Self {
|
|
||||||
const items = try allocator.dupe(Item, self.items[0..self.capacity]);
|
|
||||||
errdefer allocator.free(items);
|
|
||||||
|
|
||||||
const generations = try allocator.dupe(Generation, self.generations[0..self.capacity]);
|
|
||||||
errdefer allocator.free(generations);
|
|
||||||
|
|
||||||
const unused = try allocator.dupe(u8, self.unused[0..divCeilGeneration(self.capacity)]);
|
|
||||||
errdefer allocator.free(unused);
|
|
||||||
|
|
||||||
return Self{
|
|
||||||
.items = items.ptr,
|
|
||||||
.generations = generations.ptr,
|
|
||||||
.unused = unused.ptr,
|
|
||||||
.len = self.len,
|
|
||||||
.capacity = self.capacity
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getMetadata(self: *Self) Metadata {
|
|
||||||
return Metadata{
|
|
||||||
.len = self.len,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write(self: *Self, writer: *std.Io.Writer, endian: std.builtin.Endian) !void {
|
|
||||||
const zone = tracy.beginZone(@src(), .{ .name = "gen array list write" });
|
|
||||||
defer zone.end();
|
|
||||||
|
|
||||||
try writer.writeSliceEndian(Item, self.items[0..self.len], endian);
|
|
||||||
try writer.writeSliceEndian(Generation, self.generations[0..self.len], endian);
|
|
||||||
try writer.writeAll(self.unused[0..divCeilGeneration(self.len)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read(
|
|
||||||
self: *Self,
|
|
||||||
allocator: Allocator,
|
|
||||||
reader: *std.Io.Reader,
|
|
||||||
endian: std.builtin.Endian,
|
|
||||||
metadata: Metadata
|
|
||||||
) !void {
|
|
||||||
const zone = tracy.beginZone(@src(), .{ .name = "gen array list read" });
|
|
||||||
defer zone.end();
|
|
||||||
|
|
||||||
try self.ensureTotalCapacity(allocator, metadata.len);
|
|
||||||
|
|
||||||
try reader.readSliceEndian(Item, self.items[0..metadata.len], endian);
|
|
||||||
try reader.readSliceEndian(Generation, self.generations[0..metadata.len], endian);
|
|
||||||
try reader.readSliceAll(self.unused[0..divCeilGeneration(metadata.len)]);
|
|
||||||
|
|
||||||
self.len = metadata.len;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const TestMap = SlotMap(u24, u8, u32);
|
|
||||||
|
|
||||||
test "insert & remove" {
|
|
||||||
const expect = std.testing.expect;
|
|
||||||
const gpa = std.testing.allocator;
|
|
||||||
|
|
||||||
var array_list: TestMap = .empty;
|
|
||||||
defer array_list.deinit(gpa);
|
|
||||||
|
|
||||||
const id1 = try array_list.insert(gpa, 10);
|
|
||||||
try expect(array_list.exists(id1));
|
|
||||||
try expect(array_list.remove(id1));
|
|
||||||
try expect(!array_list.exists(id1));
|
|
||||||
try expect(!array_list.remove(id1));
|
|
||||||
|
|
||||||
const id2 = try array_list.insert(gpa, 10);
|
|
||||||
try expect(array_list.exists(id2));
|
|
||||||
try expect(!array_list.exists(id1));
|
|
||||||
try expect(id1.index == id2.index);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "generation wrap around" {
|
|
||||||
const expectEqual = std.testing.expectEqual;
|
|
||||||
const gpa = std.testing.allocator;
|
|
||||||
|
|
||||||
var array_list: TestMap = .empty;
|
|
||||||
defer array_list.deinit(gpa);
|
|
||||||
|
|
||||||
// Grow array list so that at least 1 slot exists
|
|
||||||
const id1 = try array_list.insert(gpa, 10);
|
|
||||||
array_list.removeAssumeExists(id1);
|
|
||||||
|
|
||||||
// Artificially increase generation count
|
|
||||||
array_list.generations[id1.index] = std.math.maxInt(@FieldType(TestMap.Id, "generation"));
|
|
||||||
|
|
||||||
// Check if generation wraps around
|
|
||||||
const id2 = try array_list.insert(gpa, 10);
|
|
||||||
array_list.removeAssumeExists(id2);
|
|
||||||
try expectEqual(id1.index, id2.index);
|
|
||||||
try expectEqual(0, array_list.generations[id1.index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "iterator" {
|
|
||||||
const expectEqual = std.testing.expectEqual;
|
|
||||||
const gpa = std.testing.allocator;
|
|
||||||
|
|
||||||
var array_list: TestMap = .empty;
|
|
||||||
defer array_list.deinit(gpa);
|
|
||||||
|
|
||||||
// Create array which has a hole
|
|
||||||
const id1 = try array_list.insert(gpa, 1);
|
|
||||||
const id2 = try array_list.insert(gpa, 2);
|
|
||||||
const id3 = try array_list.insert(gpa, 3);
|
|
||||||
|
|
||||||
array_list.removeAssumeExists(id2);
|
|
||||||
|
|
||||||
var iter = array_list.iterator();
|
|
||||||
try expectEqual(
|
|
||||||
TestMap.ItemWithId{
|
|
||||||
.id = id1,
|
|
||||||
.item = array_list.getAssumeExists(id1)
|
|
||||||
},
|
|
||||||
iter.next().?
|
|
||||||
);
|
|
||||||
try expectEqual(
|
|
||||||
TestMap.ItemWithId{
|
|
||||||
.id = id3,
|
|
||||||
.item = array_list.getAssumeExists(id3)
|
|
||||||
},
|
|
||||||
iter.next().?
|
|
||||||
);
|
|
||||||
try expectEqual(null, iter.next());
|
|
||||||
}
|
|
||||||
|
|
||||||
test "read & write" {
|
|
||||||
const expectEqual = std.testing.expectEqual;
|
|
||||||
const gpa = std.testing.allocator;
|
|
||||||
|
|
||||||
var array_list1: TestMap = .empty;
|
|
||||||
defer array_list1.deinit(gpa);
|
|
||||||
|
|
||||||
var array_list2: TestMap = .empty;
|
|
||||||
defer array_list2.deinit(gpa);
|
|
||||||
|
|
||||||
const id1 = try array_list1.insert(gpa, 1);
|
|
||||||
const id2 = try array_list1.insert(gpa, 2);
|
|
||||||
const id3 = try array_list1.insert(gpa, 3);
|
|
||||||
|
|
||||||
var buffer: [1024]u8 = undefined;
|
|
||||||
var writer = std.Io.Writer.fixed(&buffer);
|
|
||||||
const native_endian = builtin.cpu.arch.endian();
|
|
||||||
|
|
||||||
try array_list1.write(&writer, native_endian);
|
|
||||||
|
|
||||||
var reader = std.Io.Reader.fixed(writer.buffered());
|
|
||||||
try array_list2.read(gpa, &reader, native_endian, array_list1.getMetadata());
|
|
||||||
|
|
||||||
try expectEqual(array_list1.getAssumeExists(id1).*, array_list2.getAssumeExists(id1).*);
|
|
||||||
try expectEqual(array_list1.getAssumeExists(id2).*, array_list2.getAssumeExists(id2).*);
|
|
||||||
try expectEqual(array_list1.getAssumeExists(id3).*, array_list2.getAssumeExists(id3).*);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "clear retaining capacity" {
|
|
||||||
const expect = std.testing.expect;
|
|
||||||
const expectEqual = std.testing.expectEqual;
|
|
||||||
const gpa = std.testing.allocator;
|
|
||||||
|
|
||||||
var array_list: TestMap = .empty;
|
|
||||||
defer array_list.deinit(gpa);
|
|
||||||
|
|
||||||
const id1 = try array_list.insert(gpa, 10);
|
|
||||||
try expect(array_list.exists(id1));
|
|
||||||
array_list.clearRetainingCapacity();
|
|
||||||
|
|
||||||
const id2 = try array_list.insert(gpa, 10);
|
|
||||||
try expect(array_list.exists(id2));
|
|
||||||
|
|
||||||
try expectEqual(id1, id2);
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
id: Id,
|
|
||||||
info: Info,
|
|
||||||
|
|
||||||
pub const Info = extern struct {
|
|
||||||
format: Format,
|
|
||||||
playback_style: PlaybackStyle
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const PlaybackStyle = enum(u8) {
|
|
||||||
stream,
|
|
||||||
decode_once,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Format = enum(u8) {
|
|
||||||
vorbis
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Id = enum(u16) {
|
|
||||||
_,
|
|
||||||
|
|
||||||
pub const nil: Id = @enumFromInt(std.math.maxInt(@typeInfo(Id).@"enum".tag_type));
|
|
||||||
};
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
id: Id,
|
|
||||||
info: Info,
|
|
||||||
|
|
||||||
pub const Info = extern struct {
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
format: Format,
|
|
||||||
|
|
||||||
pub const nil = Info{
|
|
||||||
.width = 0,
|
|
||||||
.height = 0,
|
|
||||||
.format = .rgba
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Format = enum(u8) {
|
|
||||||
png,
|
|
||||||
rgba
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Id = enum(u16) {
|
|
||||||
_,
|
|
||||||
|
|
||||||
pub const nil: Id = @enumFromInt(std.math.maxInt(@typeInfo(Id).@"enum".tag_type));
|
|
||||||
};
|
|
||||||
@ -367,11 +367,6 @@ pub const Rect = struct {
|
|||||||
.size = 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 {
|
pub fn init(x: f32, y: f32, width: f32, height: f32) Rect {
|
||||||
return Rect{
|
return Rect{
|
||||||
.pos = Vec2.init(x, y),
|
.pos = Vec2.init(x, y),
|
||||||
@ -4,8 +4,6 @@ const log = std.log.scoped(.engine);
|
|||||||
pub const build_options = @import("build_options");
|
pub const build_options = @import("build_options");
|
||||||
pub const ImGui = @import("./imgui.zig");
|
pub const ImGui = @import("./imgui.zig");
|
||||||
pub const Math = @import("./math.zig");
|
pub const Math = @import("./math.zig");
|
||||||
pub const SlotMap = @import("./slot_map.zig").SlotMap;
|
|
||||||
const STBImage = @import("stb_image");
|
|
||||||
|
|
||||||
const rgb = Math.rgb;
|
const rgb = Math.rgb;
|
||||||
const Rect = Math.Rect;
|
const Rect = Math.Rect;
|
||||||
@ -143,12 +141,12 @@ pub const MouseButton = enum {
|
|||||||
middle,
|
middle,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Texture = @import("./texture.zig");
|
pub const TextureId = enum(u16) { _ };
|
||||||
pub const Font = @import("./font.zig");
|
pub const FontId = enum(u16) { _ };
|
||||||
pub const Sound = @import("./sound.zig");
|
pub const AudioId = enum(u16) { _ };
|
||||||
|
|
||||||
pub const Sprite = struct {
|
pub const Sprite = struct {
|
||||||
texture: Texture.Id,
|
texture: TextureId,
|
||||||
uv: Rect
|
uv: Rect
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -210,7 +208,7 @@ pub const GraphicsCommand = union(enum) {
|
|||||||
pub const DrawText = struct {
|
pub const DrawText = struct {
|
||||||
pos: Vec2,
|
pos: Vec2,
|
||||||
text: []const u8,
|
text: []const u8,
|
||||||
font: Font.Id,
|
font: FontId,
|
||||||
size: f32,
|
size: f32,
|
||||||
color: Vec4,
|
color: Vec4,
|
||||||
};
|
};
|
||||||
@ -229,7 +227,7 @@ pub const GraphicsCommand = union(enum) {
|
|||||||
|
|
||||||
pub const AudioCommand = union(enum) {
|
pub const AudioCommand = union(enum) {
|
||||||
pub const Play = struct {
|
pub const Play = struct {
|
||||||
id: Sound.Id,
|
id: AudioId,
|
||||||
volume: f32 = 1
|
volume: f32 = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -270,27 +268,6 @@ pub const KeyState = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Asset = struct {
|
|
||||||
data: []const u8,
|
|
||||||
// type: *std.builtin.Type,
|
|
||||||
|
|
||||||
pub const Id = u16;
|
|
||||||
|
|
||||||
pub const List = struct {
|
|
||||||
items: []Asset,
|
|
||||||
|
|
||||||
pub fn get(self: List, comptime T: type, id: Id) *const T {
|
|
||||||
if (id >= self.items.len) {
|
|
||||||
@panic("foo");
|
|
||||||
}
|
|
||||||
if (@sizeOf(T) != self.items[id].data.len) {
|
|
||||||
@panic("foo");
|
|
||||||
}
|
|
||||||
return @ptrCast(@alignCast(self.items[id].data.ptr));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Frame = struct {
|
pub const Frame = struct {
|
||||||
arena: std.heap.ArenaAllocator,
|
arena: std.heap.ArenaAllocator,
|
||||||
time_ns: Nanoseconds,
|
time_ns: Nanoseconds,
|
||||||
@ -309,13 +286,7 @@ pub const Frame = struct {
|
|||||||
screen_size: Vec2,
|
screen_size: Vec2,
|
||||||
clear_color: Vec4,
|
clear_color: Vec4,
|
||||||
|
|
||||||
assets: Asset.List,
|
pub fn init(gpa: std.mem.Allocator) Frame {
|
||||||
|
|
||||||
pub fn init(
|
|
||||||
gpa: std.mem.Allocator,
|
|
||||||
screen_size: Vec2,
|
|
||||||
assets: Asset.List,
|
|
||||||
) Frame {
|
|
||||||
return Frame{
|
return Frame{
|
||||||
.arena = std.heap.ArenaAllocator.init(gpa),
|
.arena = std.heap.ArenaAllocator.init(gpa),
|
||||||
.time_ns = 0,
|
.time_ns = 0,
|
||||||
@ -329,8 +300,7 @@ pub const Frame = struct {
|
|||||||
.clear_color = rgb(0, 0, 0),
|
.clear_color = rgb(0, 0, 0),
|
||||||
.audio_commands = .empty,
|
.audio_commands = .empty,
|
||||||
.hide_cursor = false,
|
.hide_cursor = false,
|
||||||
.screen_size = screen_size,
|
.screen_size = .init(0, 0),
|
||||||
.assets = assets
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -423,7 +393,7 @@ pub const Frame = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const DrawTextOptions = struct {
|
pub const DrawTextOptions = struct {
|
||||||
font: Font.Id,
|
font: FontId,
|
||||||
size: f32 = 16,
|
size: f32 = 16,
|
||||||
color: Vec4 = rgb(255, 255, 255),
|
color: Vec4 = rgb(255, 255, 255),
|
||||||
};
|
};
|
||||||
@ -497,8 +467,7 @@ pub const Frame = struct {
|
|||||||
|
|
||||||
pub const Init = struct {
|
pub const Init = struct {
|
||||||
gpa: std.mem.Allocator,
|
gpa: std.mem.Allocator,
|
||||||
seed: u64,
|
seed: u64
|
||||||
assets: Asset.List,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Callbacks = struct {
|
pub const Callbacks = struct {
|
||||||
@ -521,8 +490,7 @@ pub fn exportCallbacksIfNeeded(
|
|||||||
comptime deinit: Callbacks.DeinitFn,
|
comptime deinit: Callbacks.DeinitFn,
|
||||||
comptime debug: Callbacks.DebugFn
|
comptime debug: Callbacks.DebugFn
|
||||||
) void {
|
) void {
|
||||||
const builtin = @import("builtin");
|
if (build_options.statically_linked) {
|
||||||
if (builtin.output_mode != .Lib) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
75
engine/src/runtime/audio/data.zig
Normal file
75
engine/src/runtime/audio/data.zig
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const STBVorbis = @import("stb_vorbis");
|
||||||
|
const Lib = @import("lib");
|
||||||
|
|
||||||
|
pub const Data = union(enum) {
|
||||||
|
raw: struct {
|
||||||
|
channels: [][*]f32,
|
||||||
|
sample_count: u32,
|
||||||
|
sample_rate: u32
|
||||||
|
},
|
||||||
|
vorbis: struct {
|
||||||
|
alloc_buffer: []u8,
|
||||||
|
stb_vorbis: STBVorbis,
|
||||||
|
},
|
||||||
|
|
||||||
|
pub fn streamChannel(
|
||||||
|
self: Data,
|
||||||
|
buffer: []f32,
|
||||||
|
cursor: u32,
|
||||||
|
channel_index: u32,
|
||||||
|
sample_rate: u32
|
||||||
|
) []f32 {
|
||||||
|
// var result: std.ArrayList(f32) = .initBuffer(buffer);
|
||||||
|
switch (self) {
|
||||||
|
.raw => |opts| {
|
||||||
|
if (opts.sample_rate == sample_rate) {
|
||||||
|
assert(channel_index < opts.channels.len); // TODO:
|
||||||
|
const channel = opts.channels[channel_index];
|
||||||
|
|
||||||
|
var memcpy_len: usize = 0;
|
||||||
|
if (cursor + buffer.len <= opts.sample_count) {
|
||||||
|
memcpy_len = buffer.len;
|
||||||
|
} else if (cursor < opts.sample_count) {
|
||||||
|
memcpy_len = opts.sample_count - cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@memcpy(buffer[0..memcpy_len], channel[cursor..][0..memcpy_len]);
|
||||||
|
return buffer[0..memcpy_len];
|
||||||
|
} else {
|
||||||
|
// const in_sample_rate: f32 = @floatFromInt(opts.sample_rate);
|
||||||
|
// const out_sample_rate: f32 = @floatFromInt(sample_rate);
|
||||||
|
// const increment = in_sample_rate / out_sample_rate;
|
||||||
|
// _ = increment; // autofix
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.vorbis => |opts| {
|
||||||
|
_ = opts; // autofix
|
||||||
|
unreachable;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// return result.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getSampleCount(self: Data) u32 {
|
||||||
|
return switch (self) {
|
||||||
|
.raw => |opts| opts.sample_count,
|
||||||
|
.vorbis => |opts| opts.stb_vorbis.getStreamLengthInSamples()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getSampleRate(self: Data) u32 {
|
||||||
|
return switch (self) {
|
||||||
|
.raw => |opts| opts.sample_rate,
|
||||||
|
.vorbis => |opts| blk: {
|
||||||
|
const info = opts.stb_vorbis.getInfo();
|
||||||
|
break :blk info.sample_rate;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Id = Lib.AudioId;
|
||||||
|
};
|
||||||
146
engine/src/runtime/audio/mixer.zig
Normal file
146
engine/src/runtime/audio/mixer.zig
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const log = std.log.scoped(.audio);
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const sokol = @import("sokol");
|
||||||
|
const saudio = sokol.audio;
|
||||||
|
|
||||||
|
const Lib = @import("lib");
|
||||||
|
const AudioData = @import("./data.zig").Data;
|
||||||
|
pub const Store = @import("./store.zig");
|
||||||
|
|
||||||
|
const Mixer = @This();
|
||||||
|
|
||||||
|
pub const Instance = struct {
|
||||||
|
data_id: Lib.AudioId,
|
||||||
|
volume: f32 = 0,
|
||||||
|
cursor: u32 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Command = Lib.AudioCommand;
|
||||||
|
|
||||||
|
pub const RingBuffer = struct {
|
||||||
|
// TODO: This ring buffer will work in a single producer single consumer configuration
|
||||||
|
// For my game this will be good enough
|
||||||
|
|
||||||
|
items: []Command,
|
||||||
|
head: std.atomic.Value(usize) = .init(0),
|
||||||
|
tail: std.atomic.Value(usize) = .init(0),
|
||||||
|
|
||||||
|
pub fn push(self: *RingBuffer, command: Command) error{OutOfMemory}!void {
|
||||||
|
const head = self.head.load(.monotonic);
|
||||||
|
const tail = self.tail.load(.monotonic);
|
||||||
|
const next_head = @mod(head + 1, self.items.len);
|
||||||
|
|
||||||
|
// A single slot in the .items array will always not be used.
|
||||||
|
if (next_head == tail) {
|
||||||
|
return error.OutOfMemory;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.items[head] = command;
|
||||||
|
self.head.store(next_head, .monotonic);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop(self: *RingBuffer) ?Command {
|
||||||
|
const head = self.head.load(.monotonic);
|
||||||
|
const tail = self.tail.load(.monotonic);
|
||||||
|
|
||||||
|
if (head == tail) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = self.items[tail];
|
||||||
|
self.tail.store(@mod(tail + 1, self.items.len), .monotonic);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Tracks
|
||||||
|
|
||||||
|
instances: std.ArrayList(Instance),
|
||||||
|
commands: RingBuffer,
|
||||||
|
working_buffer: []f32,
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
gpa: Allocator,
|
||||||
|
max_instances: u32,
|
||||||
|
max_commands: u32,
|
||||||
|
working_buffer_size: u32
|
||||||
|
) !Mixer {
|
||||||
|
var instances = try std.ArrayList(Instance).initCapacity(gpa, max_instances);
|
||||||
|
errdefer instances.deinit(gpa);
|
||||||
|
|
||||||
|
const commands = try gpa.alloc(Command, max_commands);
|
||||||
|
errdefer gpa.free(commands);
|
||||||
|
|
||||||
|
const working_buffer = try gpa.alloc(f32, working_buffer_size);
|
||||||
|
errdefer gpa.free(working_buffer);
|
||||||
|
|
||||||
|
return Mixer{
|
||||||
|
.working_buffer = working_buffer,
|
||||||
|
.instances = instances,
|
||||||
|
.commands = .{
|
||||||
|
.items = commands
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Mixer, gpa: Allocator) void {
|
||||||
|
self.instances.deinit(gpa);
|
||||||
|
gpa.free(self.commands.items);
|
||||||
|
gpa.free(self.working_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue(self: *Mixer, command: Command) void {
|
||||||
|
self.commands.push(command) catch log.warn("Maximum number of audio commands reached!", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stream(self: *Mixer, store: Store, buffer: []f32, num_frames: u32, num_channels: u32) !void {
|
||||||
|
while (self.commands.pop()) |command| {
|
||||||
|
switch (command) {
|
||||||
|
.play => |opts| {
|
||||||
|
const volume = @max(opts.volume, 0);
|
||||||
|
if (volume == 0) {
|
||||||
|
log.warn("Attempt to play audio with 0 volume", .{});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.instances.appendBounded(.{
|
||||||
|
.data_id = opts.id,
|
||||||
|
.volume = volume,
|
||||||
|
}) catch log.warn("Maximum number of audio instances reached!", .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(num_channels == 1); // TODO:
|
||||||
|
const sample_rate: u32 = @intCast(saudio.sampleRate());
|
||||||
|
|
||||||
|
@memset(buffer, 0);
|
||||||
|
assert(self.working_buffer.len >= num_frames);
|
||||||
|
|
||||||
|
for (self.instances.items) |*instance| {
|
||||||
|
const audio_data = store.get(instance.data_id);
|
||||||
|
const samples = audio_data.streamChannel(self.working_buffer[0..num_frames], instance.cursor, 0, sample_rate);
|
||||||
|
for (0.., samples) |i, sample| {
|
||||||
|
buffer[i] += sample * instance.volume;
|
||||||
|
}
|
||||||
|
instance.cursor += @intCast(samples.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < self.instances.items.len) {
|
||||||
|
const instance = self.instances.items[i];
|
||||||
|
const audio_data = store.get(instance.data_id);
|
||||||
|
const is_complete = instance.cursor == audio_data.getSampleCount();
|
||||||
|
|
||||||
|
if (is_complete) {
|
||||||
|
_ = self.instances.swapRemove(i);
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
engine/src/runtime/audio/root.zig
Normal file
111
engine/src/runtime/audio/root.zig
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const log = std.log.scoped(.audio);
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const tracy = @import("tracy");
|
||||||
|
|
||||||
|
const Math = @import("lib").Math;
|
||||||
|
const STBVorbis = @import("stb_vorbis");
|
||||||
|
|
||||||
|
pub const Data = @import("./data.zig").Data;
|
||||||
|
pub const Store = @import("./store.zig");
|
||||||
|
pub const Mixer = @import("./mixer.zig");
|
||||||
|
|
||||||
|
pub const Command = Mixer.Command;
|
||||||
|
|
||||||
|
const Nanoseconds = @import("lib").Nanoseconds;
|
||||||
|
const sokol = @import("sokol");
|
||||||
|
const saudio = sokol.audio;
|
||||||
|
|
||||||
|
var stopped: bool = true;
|
||||||
|
var gpa: std.mem.Allocator = undefined;
|
||||||
|
var store: Store = undefined;
|
||||||
|
pub var mixer: Mixer = undefined;
|
||||||
|
|
||||||
|
const Options = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
logger: saudio.Logger = .{},
|
||||||
|
channels: u32 = 1,
|
||||||
|
max_vorbis_alloc_buffer_size: u32 = 1 * Math.bytes_per_mib,
|
||||||
|
buffer_frames: u32 = 2048,
|
||||||
|
max_instances: u32 = 64
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(opts: Options) !void {
|
||||||
|
gpa = opts.allocator;
|
||||||
|
|
||||||
|
store = try Store.init(.{
|
||||||
|
.allocator = opts.allocator,
|
||||||
|
.max_vorbis_alloc_buffer_size = opts.max_vorbis_alloc_buffer_size,
|
||||||
|
});
|
||||||
|
|
||||||
|
mixer = try Mixer.init(gpa,
|
||||||
|
opts.max_instances,
|
||||||
|
opts.max_instances,
|
||||||
|
opts.buffer_frames
|
||||||
|
);
|
||||||
|
|
||||||
|
saudio.setup(.{
|
||||||
|
.logger = opts.logger,
|
||||||
|
.stream_cb = sokolStreamCallback,
|
||||||
|
.num_channels = @intCast(opts.channels),
|
||||||
|
.buffer_frames = @intCast(opts.buffer_frames)
|
||||||
|
});
|
||||||
|
stopped = false;
|
||||||
|
|
||||||
|
const sample_rate: f32 = @floatFromInt(saudio.sampleRate());
|
||||||
|
const audio_latency: f32 = @as(f32, @floatFromInt(opts.buffer_frames)) / sample_rate;
|
||||||
|
log.debug("Audio latency = {D}", .{@as(u64, @intFromFloat(audio_latency * std.time.ns_per_s))});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit() void {
|
||||||
|
stopped = true;
|
||||||
|
saudio.shutdown();
|
||||||
|
mixer.deinit(gpa);
|
||||||
|
store.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(opts: Store.LoadOptions) !Data.Id {
|
||||||
|
return try store.load(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Info = struct {
|
||||||
|
sample_count: u32,
|
||||||
|
sample_rate: u32,
|
||||||
|
|
||||||
|
pub fn getDuration(self: Info) Nanoseconds {
|
||||||
|
return @as(Nanoseconds, self.sample_count) * std.time.ns_per_s / self.sample_rate;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn getInfo(id: Data.Id) Info {
|
||||||
|
const data = store.get(id);
|
||||||
|
return Info{
|
||||||
|
.sample_count = data.getSampleCount(),
|
||||||
|
.sample_rate = data.getSampleRate(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sokolStreamCallback(buffer: [*c]f32, num_frames: i32, num_channels: i32) callconv(.c) void {
|
||||||
|
if (stopped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const zone = tracy.initZone(@src(), .{ });
|
||||||
|
defer zone.deinit();
|
||||||
|
|
||||||
|
const num_frames_u32: u32 = @intCast(num_frames);
|
||||||
|
const num_channels_u32: u32 = @intCast(num_channels);
|
||||||
|
|
||||||
|
mixer.stream(
|
||||||
|
store,
|
||||||
|
buffer[0..(num_frames_u32 * num_channels_u32)],
|
||||||
|
num_frames_u32,
|
||||||
|
num_channels_u32
|
||||||
|
) catch |e| {
|
||||||
|
log.err("mixer.stream() failed: {}", .{e});
|
||||||
|
if (@errorReturnTrace()) |trace| {
|
||||||
|
std.debug.dumpStackTrace(trace.*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
106
engine/src/runtime/audio/store.zig
Normal file
106
engine/src/runtime/audio/store.zig
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const Math = @import("lib").Math;
|
||||||
|
const STBVorbis = @import("stb_vorbis");
|
||||||
|
|
||||||
|
const AudioData = @import("./data.zig").Data;
|
||||||
|
|
||||||
|
const Store = @This();
|
||||||
|
|
||||||
|
arena: std.heap.ArenaAllocator,
|
||||||
|
list: std.ArrayList(AudioData),
|
||||||
|
temp_vorbis_alloc_buffer: []u8,
|
||||||
|
|
||||||
|
const Options = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
max_vorbis_alloc_buffer_size: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(opts: Options) !Store {
|
||||||
|
const gpa = opts.allocator;
|
||||||
|
|
||||||
|
const temp_vorbis_alloc_buffer = try gpa.alloc(u8, opts.max_vorbis_alloc_buffer_size);
|
||||||
|
errdefer gpa.free(temp_vorbis_alloc_buffer);
|
||||||
|
|
||||||
|
return Store{
|
||||||
|
.arena = std.heap.ArenaAllocator.init(gpa),
|
||||||
|
.list = .empty,
|
||||||
|
.temp_vorbis_alloc_buffer = temp_vorbis_alloc_buffer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Store) void {
|
||||||
|
const gpa = self.arena.child_allocator;
|
||||||
|
gpa.free(self.temp_vorbis_alloc_buffer);
|
||||||
|
self.list.deinit(gpa);
|
||||||
|
self.arena.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const LoadOptions = struct {
|
||||||
|
const PlaybackStyle = enum {
|
||||||
|
stream,
|
||||||
|
decode_once,
|
||||||
|
|
||||||
|
// If the decoded size is less than `stream_threshold`, then .decode_once will by default be used.
|
||||||
|
const stream_threshold = 10 * Math.bytes_per_mib;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Format = enum {
|
||||||
|
vorbis
|
||||||
|
};
|
||||||
|
|
||||||
|
format: Format,
|
||||||
|
data: []const u8,
|
||||||
|
playback_style: ?PlaybackStyle = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn load(self: *Store, opts: LoadOptions) !AudioData.Id {
|
||||||
|
const gpa = self.arena.child_allocator;
|
||||||
|
|
||||||
|
const id = self.list.items.len;
|
||||||
|
try self.list.ensureUnusedCapacity(gpa, 1);
|
||||||
|
|
||||||
|
const PlaybackStyle = LoadOptions.PlaybackStyle;
|
||||||
|
const temp_stb_vorbis = try STBVorbis.init(opts.data, self.temp_vorbis_alloc_buffer);
|
||||||
|
const info = temp_stb_vorbis.getInfo();
|
||||||
|
const duration_in_samples = temp_stb_vorbis.getStreamLengthInSamples();
|
||||||
|
const decoded_size = info.channels * duration_in_samples * @sizeOf(f32);
|
||||||
|
const stream_threshold = PlaybackStyle.stream_threshold;
|
||||||
|
const default_playback_style: PlaybackStyle = if (decoded_size < stream_threshold) .decode_once else .stream;
|
||||||
|
|
||||||
|
const arena_allocator = self.arena.allocator();
|
||||||
|
const playback_style = opts.playback_style orelse default_playback_style;
|
||||||
|
if (playback_style == .decode_once) {
|
||||||
|
const channels = try arena_allocator.alloc([*]f32, info.channels);
|
||||||
|
for (channels) |*channel| {
|
||||||
|
channel.* = (try arena_allocator.alloc(f32, duration_in_samples)).ptr;
|
||||||
|
}
|
||||||
|
const samples_decoded = temp_stb_vorbis.getSamples(channels, duration_in_samples);
|
||||||
|
assert(samples_decoded == duration_in_samples);
|
||||||
|
|
||||||
|
self.list.appendAssumeCapacity(AudioData{
|
||||||
|
.raw = .{
|
||||||
|
.channels = channels,
|
||||||
|
.sample_count = duration_in_samples,
|
||||||
|
.sample_rate = info.sample_rate
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const alloc_buffer = try arena_allocator.alloc(u8, temp_stb_vorbis.getMinimumAllocBufferSize());
|
||||||
|
const stb_vorbis = STBVorbis.init(opts.data, alloc_buffer) catch unreachable;
|
||||||
|
|
||||||
|
self.list.appendAssumeCapacity(AudioData{
|
||||||
|
.vorbis = .{
|
||||||
|
.alloc_buffer = alloc_buffer,
|
||||||
|
.stb_vorbis = stb_vorbis
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return @enumFromInt(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: Store, id: AudioData.Id) AudioData {
|
||||||
|
return self.list.items[@intFromEnum(id)];
|
||||||
|
}
|
||||||
788
engine/src/runtime/engine.zig
Normal file
788
engine/src/runtime/engine.zig
Normal file
@ -0,0 +1,788 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const log = std.log.scoped(.engine);
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const sokol = @import("sokol");
|
||||||
|
const sapp = sokol.app;
|
||||||
|
|
||||||
|
pub const Input = @import("./input.zig");
|
||||||
|
|
||||||
|
const ScreenScalar = @import("./screen_scaler.zig");
|
||||||
|
pub const imgui = @import("./imgui.zig");
|
||||||
|
pub const Graphics = @import("./graphics.zig");
|
||||||
|
pub const Audio = @import("./audio/root.zig");
|
||||||
|
const tracy = @import("tracy");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const STBImage = @import("stb_image");
|
||||||
|
|
||||||
|
const Gfx = Graphics;
|
||||||
|
|
||||||
|
const Lib = @import("lib");
|
||||||
|
const build_options = Lib.build_options;
|
||||||
|
const debug_keybinds = (builtin.mode == .Debug);
|
||||||
|
const GameCallbacks = Lib.Callbacks;
|
||||||
|
pub const Math = Lib.Math;
|
||||||
|
pub const Vec2 = Math.Vec2;
|
||||||
|
const rgb = Math.rgb;
|
||||||
|
|
||||||
|
const GameLinking = struct {
|
||||||
|
kind: union(enum) {
|
||||||
|
static,
|
||||||
|
dynamic: struct {
|
||||||
|
fan_fd: std.os.linux.fd_t,
|
||||||
|
path: []const u8,
|
||||||
|
dir_path: [:0]const u8,
|
||||||
|
lib: std.DynLib,
|
||||||
|
last_event_at: ?std.time.Instant,
|
||||||
|
ignore_next_event: bool,
|
||||||
|
reload_index: u32
|
||||||
|
},
|
||||||
|
},
|
||||||
|
callbacks: GameCallbacks,
|
||||||
|
|
||||||
|
// TODO: Make this configurable
|
||||||
|
const event_debounce_ns = std.time.ns_per_ms * 150;
|
||||||
|
|
||||||
|
pub fn initDynamic(gpa: std.mem.Allocator, path: []const u8) !GameLinking {
|
||||||
|
if (!build_options.hot_reload) {
|
||||||
|
return error.NotSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dir_path_z = try gpa.dupeZ(u8, std.fs.path.dirname(path) orelse ".");
|
||||||
|
errdefer gpa.free(dir_path_z);
|
||||||
|
|
||||||
|
const path_dupe = try gpa.dupe(u8, path);
|
||||||
|
errdefer gpa.free(path_dupe);
|
||||||
|
|
||||||
|
const fan_fd: std.c.fd_t = @bitCast(@as(u32, @truncate(
|
||||||
|
std.os.linux.fanotify_init(.{
|
||||||
|
.REPORT_FID = true,
|
||||||
|
.REPORT_DIR_FID = true,
|
||||||
|
.REPORT_NAME = true,
|
||||||
|
.NONBLOCK = true
|
||||||
|
}, @intFromEnum(std.posix.ACCMODE.RDONLY))
|
||||||
|
)));
|
||||||
|
if (fan_fd == -1) {
|
||||||
|
return error.fanotify_init;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mark_err = std.os.linux.fanotify_mark(
|
||||||
|
@intCast(fan_fd),
|
||||||
|
.{ .ADD = true, .ONLYDIR = true },
|
||||||
|
.{
|
||||||
|
.CLOSE_WRITE = true,
|
||||||
|
.CLOSE_NOWRITE = true,
|
||||||
|
.CREATE = true,
|
||||||
|
.MOVED_TO = true,
|
||||||
|
|
||||||
|
.EVENT_ON_CHILD = true,
|
||||||
|
.ONDIR = true,
|
||||||
|
},
|
||||||
|
@intCast(std.c.AT.FDCWD),
|
||||||
|
dir_path_z
|
||||||
|
);
|
||||||
|
assert(mark_err == 0);
|
||||||
|
|
||||||
|
var lib = try std.DynLib.open(path);
|
||||||
|
const callbacks = try getCallbacksFromLibrary(&lib);
|
||||||
|
|
||||||
|
return GameLinking{
|
||||||
|
.kind = .{
|
||||||
|
.dynamic = .{
|
||||||
|
.fan_fd = fan_fd,
|
||||||
|
.dir_path = dir_path_z,
|
||||||
|
.path = path_dupe,
|
||||||
|
.lib = lib,
|
||||||
|
.last_event_at = null,
|
||||||
|
.reload_index = 0,
|
||||||
|
.ignore_next_event = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.callbacks = callbacks
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initStatic() !GameLinking {
|
||||||
|
if (!build_options.statically_linked) {
|
||||||
|
return error.NotStaticallyLinked;
|
||||||
|
}
|
||||||
|
|
||||||
|
const game = @import("game");
|
||||||
|
return GameLinking{
|
||||||
|
.kind = .static,
|
||||||
|
.callbacks = GameCallbacks{
|
||||||
|
.init = game.init,
|
||||||
|
.deinit = game.deinit,
|
||||||
|
.tick = game.tick,
|
||||||
|
.debug = if (build_options.has_imgui) game.debug else {},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *GameLinking, gpa: std.mem.Allocator) void {
|
||||||
|
if (build_options.hot_reload and self.kind == .dynamic) {
|
||||||
|
self.kind.dynamic.lib.close();
|
||||||
|
gpa.free(self.kind.dynamic.dir_path);
|
||||||
|
gpa.free(self.kind.dynamic.path);
|
||||||
|
std.posix.close(self.kind.dynamic.fan_fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getCallbacksFromLibrary(lib: *std.DynLib) !GameCallbacks {
|
||||||
|
return GameCallbacks{
|
||||||
|
.init = lib.lookup(GameCallbacks.InitFn, "init") orelse return error.MissingFunction,
|
||||||
|
.tick = lib.lookup(GameCallbacks.TickFn, "tick") orelse return error.MissingFunction,
|
||||||
|
.deinit = lib.lookup(GameCallbacks.DeinitFn, "deinit") orelse return error.MissingFunction,
|
||||||
|
.debug = if (build_options.has_imgui) lib.lookup(GameCallbacks.DebugFn, "debug") orelse return error.MissingFunction else {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check(self: *GameLinking) !void {
|
||||||
|
if (self.kind == .static) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!build_options.hot_reload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fanotify = std.os.linux.fanotify;
|
||||||
|
const M = fanotify.event_metadata;
|
||||||
|
const fan_fd = self.kind.dynamic.fan_fd;
|
||||||
|
|
||||||
|
var need_to_reload = false;
|
||||||
|
|
||||||
|
var events_buf: [256 + 4096]u8 = undefined;
|
||||||
|
while (true) {
|
||||||
|
var len = std.posix.read(fan_fd, &events_buf) catch |err| switch (err) {
|
||||||
|
error.WouldBlock => break,
|
||||||
|
else => |e| return e,
|
||||||
|
};
|
||||||
|
var meta: [*]align(1) M = @ptrCast(&events_buf);
|
||||||
|
|
||||||
|
while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({
|
||||||
|
len -= meta[0].event_len;
|
||||||
|
meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len);
|
||||||
|
}) {
|
||||||
|
assert(meta[0].vers == M.VERSION);
|
||||||
|
if (meta[0].mask.Q_OVERFLOW) {
|
||||||
|
need_to_reload = true;
|
||||||
|
// TODO:
|
||||||
|
// std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_lib_event = false;
|
||||||
|
const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
|
||||||
|
switch (fid.hdr.info_type) {
|
||||||
|
.DFID_NAME => {
|
||||||
|
const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle);
|
||||||
|
const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
|
||||||
|
const file_name = std.mem.span(file_name_z);
|
||||||
|
|
||||||
|
const lib_name = std.fs.path.basename(self.kind.dynamic.path);
|
||||||
|
is_lib_event = std.mem.eql(u8,file_name, lib_name);
|
||||||
|
},
|
||||||
|
else => |t| log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_lib_event) {
|
||||||
|
need_to_reload = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (need_to_reload) {
|
||||||
|
if (self.kind.dynamic.ignore_next_event) {
|
||||||
|
self.kind.dynamic.ignore_next_event = false;
|
||||||
|
} else {
|
||||||
|
self.kind.dynamic.last_event_at = try std.time.Instant.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.kind.dynamic.last_event_at) |last_event_at| {
|
||||||
|
const now = try std.time.Instant.now();
|
||||||
|
const time_passed = now.since(last_event_at);
|
||||||
|
if (time_passed > event_debounce_ns) {
|
||||||
|
self.kind.dynamic.last_event_at = null;
|
||||||
|
|
||||||
|
var tmp_dir = try std.fs.openDirAbsolute("/tmp", .{});
|
||||||
|
defer tmp_dir.close();
|
||||||
|
|
||||||
|
var new_filename_buffer: [std.fs.max_name_bytes]u8 = undefined;
|
||||||
|
const tmp_filename = try std.fmt.bufPrint(
|
||||||
|
&new_filename_buffer,
|
||||||
|
"{s}_{}.so",
|
||||||
|
.{std.fs.path.stem(self.kind.dynamic.path), self.kind.dynamic.reload_index}
|
||||||
|
);
|
||||||
|
|
||||||
|
try std.fs.Dir.copyFile(
|
||||||
|
std.fs.cwd(), self.kind.dynamic.path,
|
||||||
|
tmp_dir, tmp_filename,
|
||||||
|
.{}
|
||||||
|
);
|
||||||
|
|
||||||
|
var tmp_path_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||||
|
const tmp_path = try std.fmt.bufPrint(
|
||||||
|
&tmp_path_buffer,
|
||||||
|
"/tmp/{s}",
|
||||||
|
.{tmp_filename}
|
||||||
|
);
|
||||||
|
|
||||||
|
var new_lib = try std.DynLib.open(tmp_path);
|
||||||
|
const new_callbacks = try getCallbacksFromLibrary(&new_lib);
|
||||||
|
|
||||||
|
try tmp_dir.deleteFile(tmp_filename);
|
||||||
|
|
||||||
|
log.debug("Reload game code", .{});
|
||||||
|
self.kind.dynamic.lib.close();
|
||||||
|
self.kind.dynamic.lib = new_lib;
|
||||||
|
self.callbacks = new_callbacks;
|
||||||
|
self.kind.dynamic.reload_index += 1;
|
||||||
|
self.kind.dynamic.ignore_next_event = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const GameState = struct {
|
||||||
|
state: GameCallbacks.State,
|
||||||
|
|
||||||
|
input: Input,
|
||||||
|
frame: Lib.Frame,
|
||||||
|
started_at: std.time.Instant,
|
||||||
|
last_frame_at: Lib.Nanoseconds,
|
||||||
|
previous_tick_canvas_size: ?Vec2,
|
||||||
|
seed: u64,
|
||||||
|
|
||||||
|
const Options = struct {
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
seed: u64
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(opts: Options) GameState {
|
||||||
|
return GameState{
|
||||||
|
.input = .initial,
|
||||||
|
.frame = .init(opts.gpa),
|
||||||
|
.started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"),
|
||||||
|
.last_frame_at = 0,
|
||||||
|
.previous_tick_canvas_size = null,
|
||||||
|
.seed = opts.seed,
|
||||||
|
.state = undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *GameState) void {
|
||||||
|
self.frame.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Engine = @This();
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
graphics: Graphics,
|
||||||
|
|
||||||
|
game_linking: GameLinking,
|
||||||
|
game_state: GameState,
|
||||||
|
|
||||||
|
seed: u64,
|
||||||
|
|
||||||
|
show_debug: bool,
|
||||||
|
restart_game: bool,
|
||||||
|
|
||||||
|
const RunOptions = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
game_library_path: ?[]const u8 = null
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn run(self: *Engine, opts: RunOptions) !void {
|
||||||
|
self.* = Engine{
|
||||||
|
.allocator = opts.allocator,
|
||||||
|
.graphics = undefined,
|
||||||
|
.game_state = undefined,
|
||||||
|
.game_linking = undefined,
|
||||||
|
.show_debug = false,
|
||||||
|
.restart_game = false,
|
||||||
|
.seed = @bitCast(std.time.milliTimestamp()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opts.game_library_path) |game_library_path| {
|
||||||
|
self.game_linking = try .initDynamic(self.allocator, game_library_path);
|
||||||
|
} else {
|
||||||
|
self.game_linking = try .initStatic();
|
||||||
|
}
|
||||||
|
|
||||||
|
tracy.setThreadName("Main");
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Build options:", .{});
|
||||||
|
inline for (@typeInfo(build_options).@"struct".decls) |decl| {
|
||||||
|
log.debug("- {s}: {}", .{decl.name, @field(build_options, decl.name)});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Don't hard code icon path, allow changing through options
|
||||||
|
// var icon_data = try STBImage.load(@embedFile("../assets/icon.png"));
|
||||||
|
// defer icon_data.deinit();
|
||||||
|
|
||||||
|
var icon: sapp.IconDesc = .{};
|
||||||
|
icon.sokol_default = true;
|
||||||
|
// TODO:
|
||||||
|
// icon.images[0] = .{
|
||||||
|
// .width = @intCast(icon_data.width),
|
||||||
|
// .height = @intCast(icon_data.height),
|
||||||
|
// .pixels = .{
|
||||||
|
// .ptr = icon_data.rgba8_pixels,
|
||||||
|
// .size = icon_data.width * icon_data.height * 4
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
sapp.run(.{
|
||||||
|
.init_userdata_cb = sokolInitCallback,
|
||||||
|
.frame_userdata_cb = sokolFrameCallback,
|
||||||
|
.cleanup_userdata_cb = sokolCleanupCallback,
|
||||||
|
.event_userdata_cb = sokolEventCallback,
|
||||||
|
.user_data = self,
|
||||||
|
.width = 640,
|
||||||
|
.height = 480,
|
||||||
|
.icon = icon,
|
||||||
|
.window_title = "Game",
|
||||||
|
.logger = .{ .func = sokolLogCallback },
|
||||||
|
.win32 = .{
|
||||||
|
.console_utf8 = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sokolInit(self: *Engine) !void {
|
||||||
|
const zone = tracy.initZone(@src(), .{ });
|
||||||
|
defer zone.deinit();
|
||||||
|
|
||||||
|
try self.graphics.init(.{
|
||||||
|
.allocator = self.allocator,
|
||||||
|
.logger = .{ .func = sokolLogCallback },
|
||||||
|
// TODO:
|
||||||
|
// .imgui_font = .{
|
||||||
|
// .ttf_data = @embedFile("../assets/roboto-font/Roboto-Regular.ttf"),
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
try Audio.init(.{
|
||||||
|
.allocator = self.allocator,
|
||||||
|
.logger = .{ .func = sokolLogCallback },
|
||||||
|
});
|
||||||
|
|
||||||
|
self.game_state = .init(.{
|
||||||
|
.gpa = self.allocator,
|
||||||
|
.seed = self.seed
|
||||||
|
});
|
||||||
|
|
||||||
|
const opts = Lib.Init{
|
||||||
|
.gpa = self.allocator,
|
||||||
|
.seed = self.game_state.seed,
|
||||||
|
};
|
||||||
|
self.game_state.state = self.game_linking.callbacks.init(&opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sokolCleanup(self: *Engine) void {
|
||||||
|
const zone = tracy.initZone(@src(), .{ });
|
||||||
|
defer zone.deinit();
|
||||||
|
|
||||||
|
self.game_linking.callbacks.deinit(self.game_state.state);
|
||||||
|
self.game_state.deinit();
|
||||||
|
Audio.deinit();
|
||||||
|
self.graphics.deinit(self.allocator);
|
||||||
|
self.game_linking.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sokolFrame(self: *Engine) !void {
|
||||||
|
tracy.frameMark();
|
||||||
|
|
||||||
|
const zone = tracy.initZone(@src(), .{ });
|
||||||
|
defer zone.deinit();
|
||||||
|
|
||||||
|
const frame = &self.game_state.frame;
|
||||||
|
|
||||||
|
try self.game_linking.check();
|
||||||
|
|
||||||
|
const screen_size = Vec2.init(sapp.widthf(), sapp.heightf());
|
||||||
|
|
||||||
|
var maybe_screen_scaler: ?ScreenScalar = null;
|
||||||
|
if (self.game_state.previous_tick_canvas_size) |canvas_size| {
|
||||||
|
maybe_screen_scaler = ScreenScalar.init(screen_size, canvas_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const now = std.time.Instant.now() catch @panic("Instant.now() unsupported");
|
||||||
|
const time_passed = now.since(self.game_state.started_at);
|
||||||
|
defer self.game_state.last_frame_at = time_passed;
|
||||||
|
|
||||||
|
_ = frame.arena.reset(.retain_capacity);
|
||||||
|
const arena = frame.arena.allocator();
|
||||||
|
|
||||||
|
const audio_commands_capacity = frame.audio_commands.capacity;
|
||||||
|
frame.audio_commands = .empty;
|
||||||
|
try frame.audio_commands.ensureTotalCapacity(arena, audio_commands_capacity);
|
||||||
|
|
||||||
|
const graphics_commands_capacity = frame.graphics_commands.capacity;
|
||||||
|
frame.graphics_commands = .empty;
|
||||||
|
try frame.graphics_commands.ensureTotalCapacity(arena, graphics_commands_capacity);
|
||||||
|
|
||||||
|
frame.screen_size = screen_size;
|
||||||
|
frame.time_ns = time_passed;
|
||||||
|
frame.dt_ns = time_passed - self.game_state.last_frame_at;
|
||||||
|
|
||||||
|
frame.mouse_position = self.game_state.input.mouse_position;
|
||||||
|
if (maybe_screen_scaler) |screen_scaler| {
|
||||||
|
screen_scaler.push(frame);
|
||||||
|
|
||||||
|
if (frame.mouse_position) |mouse_position| {
|
||||||
|
frame.mouse_position = mouse_position.sub(screen_scaler.translation).divideScalar(screen_scaler.scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debug_keybinds) {
|
||||||
|
if (frame.isKeyPressed(.F3)) {
|
||||||
|
self.show_debug = !self.show_debug;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.isKeyPressed(.F5)) {
|
||||||
|
self.restart_game = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.game_linking.callbacks.tick(self.game_state.state, frame);
|
||||||
|
|
||||||
|
if (maybe_screen_scaler) |screen_scaler| {
|
||||||
|
screen_scaler.pop(frame, frame.clear_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.keyboard.pressed = .initEmpty();
|
||||||
|
frame.keyboard.released = .initEmpty();
|
||||||
|
frame.mouse_button.pressed = .initEmpty();
|
||||||
|
frame.mouse_button.released = .initEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canvas size modification must always be applied a frame later.
|
||||||
|
// So that mouse coordinate transformations are consistent.
|
||||||
|
self.game_state.previous_tick_canvas_size = frame.canvas_size;
|
||||||
|
|
||||||
|
sapp.showMouse(!frame.hide_cursor);
|
||||||
|
|
||||||
|
{
|
||||||
|
self.graphics.beginFrame();
|
||||||
|
defer self.graphics.endFrame(frame.clear_color);
|
||||||
|
|
||||||
|
self.graphics.drawCommands(frame.graphics_commands.items);
|
||||||
|
|
||||||
|
if (self.show_debug and build_options.has_imgui) {
|
||||||
|
try self.showDebugWindow(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (frame.audio_commands.items) |command| {
|
||||||
|
try Audio.mixer.commands.push(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.restart_game) {
|
||||||
|
self.restart_game = false;
|
||||||
|
|
||||||
|
self.game_linking.callbacks.deinit(self.game_state.state);
|
||||||
|
|
||||||
|
self.game_state.deinit();
|
||||||
|
self.game_state = .init(.{
|
||||||
|
.gpa = self.allocator,
|
||||||
|
.seed = self.seed
|
||||||
|
});
|
||||||
|
|
||||||
|
const opts = Lib.Init{
|
||||||
|
.gpa = self.allocator,
|
||||||
|
.seed = self.game_state.seed,
|
||||||
|
};
|
||||||
|
self.game_state.state = self.game_linking.callbacks.init(&opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void {
|
||||||
|
if (!imgui.beginWindow(.{
|
||||||
|
.name = "Debug",
|
||||||
|
.pos = Vec2.init(20, 20),
|
||||||
|
.size = Vec2.init(200, 200),
|
||||||
|
})) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
defer imgui.endWindow();
|
||||||
|
|
||||||
|
_ = imgui.beginTabBar("debug");
|
||||||
|
defer imgui.endTabBar();
|
||||||
|
|
||||||
|
if (imgui.beginTabItem("Game")) {
|
||||||
|
defer imgui.endTabItem();
|
||||||
|
|
||||||
|
var imgui_ctx = Lib.ImGui.init(self.allocator);
|
||||||
|
defer imgui_ctx.deinit();
|
||||||
|
self.game_linking.callbacks.debug(self.game_state.state, &imgui_ctx);
|
||||||
|
for (imgui_ctx.commands.items) |cmd| {
|
||||||
|
switch (cmd) {
|
||||||
|
.text => |str| imgui.text(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imgui.beginTabItem("Engine")) {
|
||||||
|
defer imgui.endTabItem();
|
||||||
|
|
||||||
|
if (imgui.button("Restart")) {
|
||||||
|
self.restart_game = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linking_label = if (self.game_linking.kind == .static) "static" else "dynamic";
|
||||||
|
imgui.textFmt("Linking: {s}", .{ linking_label });
|
||||||
|
|
||||||
|
imgui.textFmt("Seed: 0x{x:08}", .{ self.seed });
|
||||||
|
|
||||||
|
const time_ms: f64 = @floatFromInt(@divFloor(self.game_state.last_frame_at, std.time.ns_per_ms));
|
||||||
|
imgui.textFmt("Time: {:.2}", .{ time_ms / 1000 });
|
||||||
|
|
||||||
|
imgui.textFmt("Draw commands: {}\n", .{
|
||||||
|
frame.graphics_commands.items.len,
|
||||||
|
});
|
||||||
|
imgui.textFmt("Audio instances: {}/{}\n", .{
|
||||||
|
Audio.mixer.instances.items.len,
|
||||||
|
Audio.mixer.instances.capacity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
|
||||||
|
const zone = tracy.initZone(@src(), .{ });
|
||||||
|
defer zone.deinit();
|
||||||
|
|
||||||
|
const e = e_ptr.*;
|
||||||
|
|
||||||
|
const input = &self.game_state.input;
|
||||||
|
const frame = &self.game_state.frame;
|
||||||
|
|
||||||
|
if (imgui.handleEvent(e)) {
|
||||||
|
if (input.mouse_position != null) {
|
||||||
|
input.processEvent(frame, .{
|
||||||
|
.mouse_leave = {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
input.mouse_position = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
blk: switch (e.type) {
|
||||||
|
.MOUSE_DOWN => {
|
||||||
|
const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk;
|
||||||
|
|
||||||
|
input.processEvent(frame, .{
|
||||||
|
.mouse_pressed = .{
|
||||||
|
.button = mouse_button,
|
||||||
|
.position = Vec2.init(e.mouse_x, e.mouse_y)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
.MOUSE_UP => {
|
||||||
|
const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk;
|
||||||
|
|
||||||
|
input.processEvent(frame, .{
|
||||||
|
.mouse_released = .{
|
||||||
|
.button = mouse_button,
|
||||||
|
.position = Vec2.init(e.mouse_x, e.mouse_y)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
.MOUSE_MOVE => {
|
||||||
|
if (input.mouse_position == null) {
|
||||||
|
input.processEvent(frame, .{
|
||||||
|
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
input.processEvent(frame, .{
|
||||||
|
.mouse_move = Vec2.init(e.mouse_x, e.mouse_y)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
.MOUSE_ENTER => {
|
||||||
|
if (input.mouse_position == null) {
|
||||||
|
input.processEvent(frame, .{
|
||||||
|
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
.RESIZED => {
|
||||||
|
if (input.mouse_position != null) {
|
||||||
|
input.processEvent(frame, .{
|
||||||
|
.mouse_leave = {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
input.processEvent(frame, .{
|
||||||
|
.window_resize = {}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
.MOUSE_LEAVE => {
|
||||||
|
if (input.mouse_position != null) {
|
||||||
|
input.processEvent(frame, .{
|
||||||
|
.mouse_leave = {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
.MOUSE_SCROLL => {
|
||||||
|
input.processEvent(frame, .{
|
||||||
|
.mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y)
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
.KEY_DOWN => {
|
||||||
|
const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk;
|
||||||
|
|
||||||
|
input.processEvent(frame, .{
|
||||||
|
.key_pressed = .{
|
||||||
|
.code = key_code,
|
||||||
|
.repeat = e.key_repeat
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
.KEY_UP => {
|
||||||
|
const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk;
|
||||||
|
|
||||||
|
input.processEvent(frame, .{
|
||||||
|
.key_released = key_code
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
.CHAR => {
|
||||||
|
input.processEvent(frame, .{
|
||||||
|
.char = @intCast(e.char_code)
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
.QUIT_REQUESTED => {
|
||||||
|
// TODO: handle quit request. Maybe show confirmation window in certain cases.
|
||||||
|
},
|
||||||
|
else => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sokolEventCallback(e_ptr: [*c]const sapp.Event, userdata: ?*anyopaque) callconv(.c) void {
|
||||||
|
const engine: *Engine = @alignCast(@ptrCast(userdata));
|
||||||
|
|
||||||
|
const consume_event = engine.sokolEvent(e_ptr) catch |e| blk: {
|
||||||
|
log.err("sokolEvent() failed: {}", .{e});
|
||||||
|
if (@errorReturnTrace()) |trace| {
|
||||||
|
std.debug.dumpStackTrace(trace.*);
|
||||||
|
}
|
||||||
|
break :blk false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (consume_event) {
|
||||||
|
sapp.consumeEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sokolCleanupCallback(userdata: ?*anyopaque) callconv(.c) void {
|
||||||
|
const engine: *Engine = @alignCast(@ptrCast(userdata));
|
||||||
|
|
||||||
|
engine.sokolCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sokolInitCallback(userdata: ?*anyopaque) callconv(.c) void {
|
||||||
|
const engine: *Engine = @alignCast(@ptrCast(userdata));
|
||||||
|
|
||||||
|
engine.sokolInit() catch |e| {
|
||||||
|
log.err("sokolInit() failed: {}", .{e});
|
||||||
|
if (@errorReturnTrace()) |trace| {
|
||||||
|
std.debug.dumpStackTrace(trace.*);
|
||||||
|
}
|
||||||
|
sapp.requestQuit();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sokolFrameCallback(userdata: ?*anyopaque) callconv(.c) void {
|
||||||
|
const engine: *Engine = @alignCast(@ptrCast(userdata));
|
||||||
|
|
||||||
|
engine.sokolFrame() catch |e| {
|
||||||
|
log.err("sokolFrame() failed: {}", .{e});
|
||||||
|
if (@errorReturnTrace()) |trace| {
|
||||||
|
std.debug.dumpStackTrace(trace.*);
|
||||||
|
}
|
||||||
|
sapp.requestQuit();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void {
|
||||||
|
const log_sokol = std.log.scoped(.sokol);
|
||||||
|
|
||||||
|
if (log_level == 0) {
|
||||||
|
log_sokol.err(format, args);
|
||||||
|
} else if (log_level == 1) {
|
||||||
|
log_sokol.err(format, args);
|
||||||
|
} else if (log_level == 2) {
|
||||||
|
log_sokol.warn(format, args);
|
||||||
|
} else {
|
||||||
|
log_sokol.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 "")
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn posixSignalHandler(sig: i32) callconv(.c) void {
|
||||||
|
_ = sig;
|
||||||
|
sapp.requestQuit();
|
||||||
|
}
|
||||||
@ -6,13 +6,11 @@ const sapp = sokol.app;
|
|||||||
const simgui = sokol.imgui;
|
const simgui = sokol.imgui;
|
||||||
const sgl = sokol.gl;
|
const sgl = sokol.gl;
|
||||||
|
|
||||||
const Lib = @import("engine_lib");
|
const Math = @import("lib").Math;
|
||||||
const Math = Lib.Math;
|
|
||||||
const Vec2 = Math.Vec2;
|
const Vec2 = Math.Vec2;
|
||||||
const Vec4 = Math.Vec4;
|
const Vec4 = Math.Vec4;
|
||||||
const rgb = Math.rgb;
|
const rgb = Math.rgb;
|
||||||
const Rect = Math.Rect;
|
const Rect = Math.Rect;
|
||||||
const Command = Lib.GraphicsCommand;
|
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const log = std.log.scoped(.graphics);
|
const log = std.log.scoped(.graphics);
|
||||||
@ -21,6 +19,10 @@ const assert = std.debug.assert;
|
|||||||
const imgui = @import("imgui.zig");
|
const imgui = @import("imgui.zig");
|
||||||
const tracy = @import("tracy");
|
const tracy = @import("tracy");
|
||||||
const fontstash = @import("./fontstash/root.zig");
|
const fontstash = @import("./fontstash/root.zig");
|
||||||
|
pub const Font = fontstash.Font;
|
||||||
|
|
||||||
|
const Lib = @import("lib");
|
||||||
|
const Command = Lib.GraphicsCommand;
|
||||||
|
|
||||||
const Graphics = @This();
|
const Graphics = @This();
|
||||||
|
|
||||||
@ -31,80 +33,43 @@ const Graphics = @This();
|
|||||||
// * https://github.com/libsdl-org/SDL/issues/11618
|
// * https://github.com/libsdl-org/SDL/issues/11618
|
||||||
// * https://github.com/nimgl/nimgl/issues/59
|
// * https://github.com/nimgl/nimgl/issues/59
|
||||||
|
|
||||||
pub const Sprite = Lib.Sprite;
|
const Options = struct {
|
||||||
|
const ImguiFont = struct {
|
||||||
|
ttf_data: []const u8,
|
||||||
|
size: f32 = 16
|
||||||
|
};
|
||||||
|
|
||||||
const SokolState = struct {
|
allocator: std.mem.Allocator,
|
||||||
main_pipeline: sgl.Pipeline,
|
logger: sg.Logger = .{},
|
||||||
linear_sampler: sg.Sampler,
|
imgui_font: ?ImguiFont = null
|
||||||
nearest_sampler: sg.Sampler,
|
|
||||||
font_context: fontstash.Context,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ImguiFont = struct {
|
|
||||||
ttf_data: []const u8,
|
|
||||||
size: f32 = 16
|
|
||||||
};
|
|
||||||
|
|
||||||
const SokolOptions = struct {
|
|
||||||
logger: sg.Logger,
|
|
||||||
imgui_font: ?ImguiFont,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Texture = struct {
|
const Texture = struct {
|
||||||
image: sg.Image,
|
image: sg.Image,
|
||||||
view: sg.View,
|
view: sg.View,
|
||||||
|
info: Info,
|
||||||
|
|
||||||
|
const Info = struct {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
};
|
||||||
|
|
||||||
const Data = struct {
|
const Data = struct {
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
rgba: [*]u8,
|
rgba: [*]u8
|
||||||
|
|
||||||
pub fn clone(self: Data, allocator: std.mem.Allocator) !Data {
|
|
||||||
const pixel_count = self.width*self.height;
|
|
||||||
const rgba = try allocator.dupe(u8, self.rgba[0..(pixel_count * 4)]);
|
|
||||||
errdefer allocator.free(rgba);
|
|
||||||
|
|
||||||
return Data{
|
|
||||||
.width = self.width,
|
|
||||||
.height = self.height,
|
|
||||||
.rgba = rgba.ptr,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn destroy(self: *Texture) void {
|
|
||||||
sg.destroyView(self.view);
|
|
||||||
sg.destroyImage(self.image);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
pub const TextureId = Lib.TextureId;
|
||||||
|
pub const TextureInfo = Texture.Info;
|
||||||
|
|
||||||
const Font = struct {
|
pub const Sprite = Lib.Sprite;
|
||||||
fontstash_id: ?fontstash.Font.Id,
|
|
||||||
data: ?[]const u8,
|
|
||||||
|
|
||||||
pub fn deinit(self: Font, gpa: std.mem.Allocator) void {
|
main_pipeline: sgl.Pipeline,
|
||||||
if (self.data) |data| {
|
linear_sampler: sg.Sampler,
|
||||||
gpa.free(data);
|
nearest_sampler: sg.Sampler,
|
||||||
}
|
font_context: fontstash.Context,
|
||||||
}
|
textures: std.ArrayList(Texture) = .empty,
|
||||||
};
|
|
||||||
|
|
||||||
const FontArray = Lib.SlotMap(u8, u8, Font);
|
|
||||||
comptime {
|
|
||||||
assert(@bitSizeOf(FontArray.Id) == @bitSizeOf(Lib.Font.Id));
|
|
||||||
assert(FontArray.Id.none.asInt() == @intFromEnum(Lib.Font.Id.nil));
|
|
||||||
}
|
|
||||||
|
|
||||||
const TexturesArray = Lib.SlotMap(u8, u8, Texture);
|
|
||||||
comptime {
|
|
||||||
// TODO: This is PITA, can this be refactored?
|
|
||||||
assert(@bitSizeOf(TexturesArray.Id) == @bitSizeOf(Lib.Texture.Id));
|
|
||||||
assert(TexturesArray.Id.none.asInt() == @intFromEnum(Lib.Texture.Id.nil));
|
|
||||||
}
|
|
||||||
|
|
||||||
gpa: std.mem.Allocator,
|
|
||||||
textures: TexturesArray = .empty,
|
|
||||||
fonts: FontArray = .empty,
|
|
||||||
|
|
||||||
scale_stack_buffer: [32]Vec2 = undefined,
|
scale_stack_buffer: [32]Vec2 = undefined,
|
||||||
scale_stack: std.ArrayList(Vec2) = .empty,
|
scale_stack: std.ArrayList(Vec2) = .empty,
|
||||||
@ -112,42 +77,18 @@ scale_stack: std.ArrayList(Vec2) = .empty,
|
|||||||
scissor_stack_buffer: [32]Rect = undefined,
|
scissor_stack_buffer: [32]Rect = undefined,
|
||||||
scissor_stack: std.ArrayList(Rect) = .empty,
|
scissor_stack: std.ArrayList(Rect) = .empty,
|
||||||
|
|
||||||
sokol_state: ?SokolState,
|
font_id_lookup: std.AutoArrayHashMapUnmanaged(Lib.FontId, fontstash.Font.Id) = .empty,
|
||||||
sokol_options: SokolOptions,
|
|
||||||
|
|
||||||
deferred_textures_arena: std.heap.ArenaAllocator,
|
|
||||||
deferred_textures: std.AutoArrayHashMapUnmanaged(Lib.Texture.Id, Texture.Data) = .empty,
|
|
||||||
|
|
||||||
const Options = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
logger: sg.Logger = .{},
|
|
||||||
imgui_font: ?ImguiFont = null
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(options: Options) !Graphics {
|
|
||||||
return Graphics{
|
|
||||||
.gpa = options.allocator,
|
|
||||||
.sokol_state = null,
|
|
||||||
.deferred_textures_arena = std.heap.ArenaAllocator.init(options.allocator),
|
|
||||||
.sokol_options = .{
|
|
||||||
.logger = options.logger,
|
|
||||||
.imgui_font = options.imgui_font,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setupSokol(self: *Graphics) !void {
|
|
||||||
const opts = self.sokol_options;
|
|
||||||
|
|
||||||
|
pub fn init(self: *Graphics, options: Options) !void {
|
||||||
sg.setup(.{
|
sg.setup(.{
|
||||||
.logger = opts.logger,
|
.logger = options.logger,
|
||||||
.environment = sglue.environment(),
|
.environment = sglue.environment(),
|
||||||
});
|
});
|
||||||
|
|
||||||
sgl.setup(.{
|
sgl.setup(.{
|
||||||
.logger = .{
|
.logger = .{
|
||||||
.func = opts.logger.func,
|
.func = options.logger.func,
|
||||||
.user_data = opts.logger.user_data
|
.user_data = options.logger.user_data
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -169,12 +110,12 @@ pub fn setupSokol(self: *Graphics) !void {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
imgui.setup(self.gpa, .{
|
imgui.setup(options.allocator, .{
|
||||||
.logger = .{
|
.logger = .{
|
||||||
.func = opts.logger.func,
|
.func = options.logger.func,
|
||||||
.user_data = opts.logger.user_data
|
.user_data = options.logger.user_data
|
||||||
},
|
},
|
||||||
.no_default_font = opts.imgui_font != null,
|
.no_default_font = options.imgui_font != null,
|
||||||
|
|
||||||
// TODO: Figure out a way to make imgui play nicely with UI
|
// TODO: Figure out a way to make imgui play nicely with UI
|
||||||
// Ideally when mouse is inside a Imgui window, then the imgui cursor should be used.
|
// Ideally when mouse is inside a Imgui window, then the imgui cursor should be used.
|
||||||
@ -182,7 +123,7 @@ pub fn setupSokol(self: *Graphics) !void {
|
|||||||
.disable_set_mouse_cursor = true
|
.disable_set_mouse_cursor = true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (opts.imgui_font) |imgui_font| {
|
if (options.imgui_font) |imgui_font| {
|
||||||
imgui.addFont(imgui_font.ttf_data, imgui_font.size);
|
imgui.addFont(imgui_font.ttf_data, imgui_font.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,46 +149,24 @@ pub fn setupSokol(self: *Graphics) !void {
|
|||||||
.height = @intCast(atlas_dim),
|
.height = @intCast(atlas_dim),
|
||||||
});
|
});
|
||||||
|
|
||||||
self.sokol_state = SokolState{
|
self.* = Graphics{
|
||||||
.main_pipeline = main_pipeline,
|
.main_pipeline = main_pipeline,
|
||||||
.linear_sampler = linear_sampler,
|
.linear_sampler = linear_sampler,
|
||||||
.nearest_sampler = nearest_sampler,
|
.nearest_sampler = nearest_sampler,
|
||||||
.font_context = font_context,
|
.font_context = font_context,
|
||||||
};
|
};
|
||||||
|
|
||||||
var iter = self.deferred_textures.iterator();
|
|
||||||
while (iter.next()) |entry| {
|
|
||||||
try self.setTextureData(entry.key_ptr.*, entry.value_ptr.*);
|
|
||||||
}
|
|
||||||
_ = self.deferred_textures_arena.reset(.free_all);
|
|
||||||
self.deferred_textures = .empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Graphics, gpa: std.mem.Allocator) void {
|
pub fn deinit(self: *Graphics, gpa: std.mem.Allocator) void {
|
||||||
var fonts_iter = self.fonts.iterator();
|
self.font_id_lookup.deinit(gpa);
|
||||||
while (fonts_iter.nextItem()) |font| {
|
|
||||||
font.deinit(self.gpa);
|
|
||||||
}
|
|
||||||
self.fonts.deinit(gpa);
|
|
||||||
|
|
||||||
var textures_iter = self.textures.iterator();
|
|
||||||
while (textures_iter.nextItem()) |texture| {
|
|
||||||
texture.destroy();
|
|
||||||
}
|
|
||||||
self.textures.deinit(gpa);
|
self.textures.deinit(gpa);
|
||||||
|
imgui.shutdown();
|
||||||
if (self.sokol_state) |*sokol_state| {
|
self.font_context.deinit();
|
||||||
imgui.shutdown();
|
sgl.shutdown();
|
||||||
sokol_state.font_context.deinit();
|
sg.shutdown();
|
||||||
sgl.shutdown();
|
|
||||||
sg.shutdown();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn drawCommand(self: *Graphics, command: Command) void {
|
pub fn drawCommand(self: *Graphics, command: Command) void {
|
||||||
assert(self.sokol_state != null);
|
|
||||||
const sokol_state = self.sokol_state.?;
|
|
||||||
|
|
||||||
switch(command) {
|
switch(command) {
|
||||||
.push_transformation => |opts| {
|
.push_transformation => |opts| {
|
||||||
self.pushTransform(opts.translation, opts.scale);
|
self.pushTransform(opts.translation, opts.scale);
|
||||||
@ -301,24 +220,22 @@ pub fn drawCommand(self: *Graphics, command: Command) void {
|
|||||||
|
|
||||||
sgl.scale(1/font_resolution_scale.x, 1/font_resolution_scale.y, 1);
|
sgl.scale(1/font_resolution_scale.x, 1/font_resolution_scale.y, 1);
|
||||||
|
|
||||||
const font = self.fonts.get(.fromInt(@intFromEnum(opts.font))) orelse {
|
const font_id = self.font_id_lookup.get(opts.font) orelse {
|
||||||
log.warn("Attempt to use font that doesn't exist", .{});
|
log.warn("Attempt to use font that doesn't exist", .{});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
const fontstash_id = font.fontstash_id orelse return;
|
self.font_context.setFont(font_id);
|
||||||
const font_context = sokol_state.font_context;
|
self.font_context.setSize(opts.size * font_resolution_scale.y);
|
||||||
font_context.setFont(fontstash_id);
|
self.font_context.setAlign(.{ .x = .left, .y = .top });
|
||||||
font_context.setSize(opts.size * font_resolution_scale.y);
|
self.font_context.setSpacing(0);
|
||||||
font_context.setAlign(.{ .x = .left, .y = .top });
|
|
||||||
font_context.setSpacing(0);
|
|
||||||
|
|
||||||
const r: u8 = @intFromFloat(opts.color.x * 255);
|
const r: u8 = @intFromFloat(opts.color.x * 255);
|
||||||
const g: u8 = @intFromFloat(opts.color.y * 255);
|
const g: u8 = @intFromFloat(opts.color.y * 255);
|
||||||
const b: u8 = @intFromFloat(opts.color.z * 255);
|
const b: u8 = @intFromFloat(opts.color.z * 255);
|
||||||
const a: u8 = @intFromFloat(opts.color.w * 255);
|
const a: u8 = @intFromFloat(opts.color.w * 255);
|
||||||
const color: u32 = r | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, a) << 24);
|
const color: u32 = r | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, a) << 24);
|
||||||
font_context.setColor(color);
|
self.font_context.setColor(color);
|
||||||
font_context.drawText(
|
self.font_context.drawText(
|
||||||
opts.pos.x * font_resolution_scale.x,
|
opts.pos.x * font_resolution_scale.x,
|
||||||
opts.pos.y * font_resolution_scale.y,
|
opts.pos.y * font_resolution_scale.y,
|
||||||
opts.text
|
opts.text
|
||||||
@ -333,9 +250,9 @@ pub fn drawCommands(self: *Graphics, commands: []const Command) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn beginFrame(self: *Graphics) !void {
|
pub fn beginFrame(self: *Graphics) void {
|
||||||
assert(self.sokol_state != null);
|
const zone = tracy.initZone(@src(), .{ });
|
||||||
const sokol_state = self.sokol_state.?;
|
defer zone.deinit();
|
||||||
|
|
||||||
imgui.newFrame(.{
|
imgui.newFrame(.{
|
||||||
.width = sapp.width(),
|
.width = sapp.width(),
|
||||||
@ -350,16 +267,16 @@ pub fn beginFrame(self: *Graphics) !void {
|
|||||||
self.scissor_stack = .initBuffer(&self.scissor_stack_buffer);
|
self.scissor_stack = .initBuffer(&self.scissor_stack_buffer);
|
||||||
self.scissor_stack.appendAssumeCapacity(.init(0, 0, sapp.widthf(), sapp.heightf()));
|
self.scissor_stack.appendAssumeCapacity(.init(0, 0, sapp.widthf(), sapp.heightf()));
|
||||||
|
|
||||||
sokol_state.font_context.clearState();
|
self.font_context.clearState();
|
||||||
sgl.defaults();
|
sgl.defaults();
|
||||||
sgl.matrixModeProjection();
|
sgl.matrixModeProjection();
|
||||||
sgl.ortho(0, sapp.widthf(), sapp.heightf(), 0, -1, 1);
|
sgl.ortho(0, sapp.widthf(), sapp.heightf(), 0, -1, 1);
|
||||||
sgl.loadPipeline(sokol_state.main_pipeline);
|
sgl.loadPipeline(self.main_pipeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn endFrame(self: *Graphics, clear_color: Vec4) void {
|
pub fn endFrame(self: *Graphics, clear_color: Vec4) void {
|
||||||
assert(self.sokol_state != null);
|
const zone = tracy.initZone(@src(), .{ });
|
||||||
const sokol_state = self.sokol_state.?;
|
defer zone.deinit();
|
||||||
|
|
||||||
var pass_action: sg.PassAction = .{};
|
var pass_action: sg.PassAction = .{};
|
||||||
|
|
||||||
@ -373,7 +290,7 @@ pub fn endFrame(self: *Graphics, clear_color: Vec4) void {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sokol_state.font_context.flush();
|
self.font_context.flush();
|
||||||
|
|
||||||
{
|
{
|
||||||
sg.beginPass(.{
|
sg.beginPass(.{
|
||||||
@ -407,24 +324,13 @@ const Vertex = struct {
|
|||||||
uv: Vec2
|
uv: Vec2
|
||||||
};
|
};
|
||||||
|
|
||||||
fn drawQuad(self: *Graphics, quad: [4]Vertex, color: Vec4, texture_id: Lib.Texture.Id) void {
|
fn drawQuad(self: *Graphics, quad: [4]Vertex, color: Vec4, texture_id: TextureId) void {
|
||||||
assert(self.sokol_state != null);
|
|
||||||
const sokol_state = self.sokol_state.?;
|
|
||||||
|
|
||||||
var view: ?sg.View = null;
|
|
||||||
if (self.textures.get(.fromInt(@intFromEnum(texture_id)))) |texture| {
|
|
||||||
view = texture.view;
|
|
||||||
} else {
|
|
||||||
log.warn("Attempt to use texture that doesn't exist, id={}", .{texture_id});
|
|
||||||
}
|
|
||||||
|
|
||||||
sgl.enableTexture();
|
sgl.enableTexture();
|
||||||
defer sgl.disableTexture();
|
defer sgl.disableTexture();
|
||||||
|
|
||||||
|
const view = self.textures.items[@intFromEnum(texture_id)].view;
|
||||||
// TODO: Make sampler configurable
|
// TODO: Make sampler configurable
|
||||||
if (view != null) {
|
sgl.texture(view, self.nearest_sampler);
|
||||||
sgl.texture(view.?, sokol_state.nearest_sampler);
|
|
||||||
}
|
|
||||||
|
|
||||||
sgl.beginQuads();
|
sgl.beginQuads();
|
||||||
defer sgl.end();
|
defer sgl.end();
|
||||||
@ -502,9 +408,21 @@ fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------- Textures ----------------- //
|
fn addFont(self: *Graphics, name: [*c]const u8, data: []const u8) !Font.Id {
|
||||||
|
return try self.font_context.addFont(name, data);
|
||||||
|
}
|
||||||
|
|
||||||
fn initImageWithMipMaps(image: sg.Image, mipmaps: []const Texture.Data) !void {
|
fn makeView(image: sg.Image) !sg.View {
|
||||||
|
const image_view = sg.makeView(.{
|
||||||
|
.texture = .{ .image = image }
|
||||||
|
});
|
||||||
|
if (image_view.id == sg.invalid_id) {
|
||||||
|
return error.InvalidView;
|
||||||
|
}
|
||||||
|
return image_view;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image {
|
||||||
if (mipmaps.len == 0) {
|
if (mipmaps.len == 0) {
|
||||||
return error.NoMipMaps;
|
return error.NoMipMaps;
|
||||||
}
|
}
|
||||||
@ -519,7 +437,7 @@ fn initImageWithMipMaps(image: sg.Image, mipmaps: []const Texture.Data) !void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sg.initImage(image, .{
|
const image = sg.makeImage(.{
|
||||||
.width = @intCast(mipmaps[0].width),
|
.width = @intCast(mipmaps[0].width),
|
||||||
.height = @intCast(mipmaps[0].height),
|
.height = @intCast(mipmaps[0].height),
|
||||||
.pixel_format = .RGBA8,
|
.pixel_format = .RGBA8,
|
||||||
@ -529,86 +447,35 @@ fn initImageWithMipMaps(image: sg.Image, mipmaps: []const Texture.Data) !void {
|
|||||||
.num_mipmaps = @intCast(mip_levels.items.len),
|
.num_mipmaps = @intCast(mip_levels.items.len),
|
||||||
.data = data
|
.data = data
|
||||||
});
|
});
|
||||||
if (sg.queryImageState(image) != .VALID) {
|
if (image.id == sg.invalid_id) {
|
||||||
return error.InitImage;
|
return error.InvalidImage;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn addTexture(self: *Graphics) !Lib.Texture.Id {
|
|
||||||
const texture_id = try self.textures.insert(self.gpa, .{
|
|
||||||
.image = .{ .id = sg.invalid_id },
|
|
||||||
.view = .{ .id = sg.invalid_id }
|
|
||||||
});
|
|
||||||
return @enumFromInt(texture_id.asInt());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn removeTexture(self: *Graphics, id: Lib.Texture.Id) void {
|
|
||||||
_ = self.textures.remove(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setTextureData(self: *Graphics, id: Lib.Texture.Id, data: Texture.Data) !void {
|
|
||||||
if (self.sokol_state == null) {
|
|
||||||
const arena = self.deferred_textures_arena.allocator();
|
|
||||||
try self.deferred_textures.put(arena, id, try data.clone(arena));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const texture = self.textures.get(.fromInt(@intFromEnum(id))) orelse return;
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
if (texture.image.id == sg.invalid_id) {
|
pub fn addTexture(self: *Graphics, gpa: std.mem.Allocator, mipmaps: []const Texture.Data) !TextureId {
|
||||||
texture.image = sg.allocImage();
|
const image = try makeImageWithMipMaps(mipmaps);
|
||||||
if (texture.image.id == sg.invalid_id) {
|
errdefer sg.deallocImage(image);
|
||||||
return error.AllocImage;
|
|
||||||
|
const view = try makeView(image);
|
||||||
|
errdefer sg.deallocView(view);
|
||||||
|
|
||||||
|
assert(mipmaps.len > 0);
|
||||||
|
const index = self.textures.items.len;
|
||||||
|
try self.textures.append(gpa, .{
|
||||||
|
.image = image,
|
||||||
|
.view = view,
|
||||||
|
.info = .{
|
||||||
|
.width = mipmaps[0].width,
|
||||||
|
.height = mipmaps[0].height,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (texture.view.id == sg.invalid_id) {
|
|
||||||
texture.view = sg.allocView();
|
|
||||||
if (texture.view.id == sg.invalid_id) {
|
|
||||||
return error.AllocView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sg.uninitView(texture.view);
|
|
||||||
sg.uninitImage(texture.image);
|
|
||||||
|
|
||||||
try initImageWithMipMaps(texture.image, &.{ data });
|
|
||||||
|
|
||||||
sg.initView(texture.view, .{
|
|
||||||
.texture = .{ .image = texture.image }
|
|
||||||
});
|
});
|
||||||
if (sg.queryViewState(texture.view) != .VALID) {
|
|
||||||
return error.InitView;
|
return @enumFromInt(index);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------- Fonts ----------------- //
|
pub fn getTextureInfo(self: *Graphics, id: TextureId) TextureInfo {
|
||||||
|
const texture = self.textures.items[@intFromEnum(id)];
|
||||||
pub fn addFont(self: *Graphics) !Lib.Font.Id {
|
return texture.info;
|
||||||
const font_id = try self.fonts.insert(self.gpa, .{
|
|
||||||
.fontstash_id = null,
|
|
||||||
.data = null
|
|
||||||
});
|
|
||||||
return @enumFromInt(font_id.asInt());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn removeFont(self: *Graphics, id: Lib.Font.Id) void {
|
|
||||||
_ = self.fonts.remove(.fromInt(@intFromEnum(id)));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setFontData(self: *Graphics, id: Lib.Font.Id, data: []const u8) !void {
|
|
||||||
const sokol_state = self.sokol_state orelse return error.SokolNotSetup;
|
|
||||||
|
|
||||||
const font = self.fonts.get(.fromInt(@intFromEnum(id))) orelse return;
|
|
||||||
|
|
||||||
const new_data = try self.gpa.dupe(u8, data);
|
|
||||||
errdefer self.gpa.free(new_data);
|
|
||||||
|
|
||||||
const new_fontstash_id = try sokol_state.font_context.addFont("", new_data);
|
|
||||||
|
|
||||||
if (font.data) |old_data| {
|
|
||||||
self.gpa.free(old_data);
|
|
||||||
}
|
|
||||||
font.data = new_data;
|
|
||||||
font.fontstash_id = new_fontstash_id;
|
|
||||||
}
|
}
|
||||||
@ -9,7 +9,7 @@ const sokol = @import("sokol");
|
|||||||
const sapp = sokol.app;
|
const sapp = sokol.app;
|
||||||
const simgui = sokol.imgui;
|
const simgui = sokol.imgui;
|
||||||
|
|
||||||
const Lib = @import("engine_lib");
|
const Lib = @import("lib");
|
||||||
const build_options = Lib.build_options;
|
const build_options = Lib.build_options;
|
||||||
const enabled = build_options.has_imgui;
|
const enabled = build_options.has_imgui;
|
||||||
|
|
||||||
@ -2,11 +2,13 @@ const std = @import("std");
|
|||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const sokol = @import("sokol");
|
const sokol = @import("sokol");
|
||||||
|
|
||||||
const lib = @import("engine_lib");
|
const lib = @import("lib");
|
||||||
const Frame = lib.Frame;
|
const Frame = lib.Frame;
|
||||||
const Nanoseconds = lib.Nanoseconds;
|
const Nanoseconds = lib.Nanoseconds;
|
||||||
const Vec2 = lib.Math.Vec2;
|
const Vec2 = lib.Math.Vec2;
|
||||||
|
|
||||||
|
const Input = @This();
|
||||||
|
|
||||||
pub const Event = union(enum) {
|
pub const Event = union(enum) {
|
||||||
mouse_pressed: struct {
|
mouse_pressed: struct {
|
||||||
button: lib.MouseButton,
|
button: lib.MouseButton,
|
||||||
@ -25,14 +27,54 @@ pub const Event = union(enum) {
|
|||||||
repeat: bool
|
repeat: bool
|
||||||
},
|
},
|
||||||
key_released: lib.KeyCode,
|
key_released: lib.KeyCode,
|
||||||
window_resize: struct {
|
window_resize,
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
},
|
|
||||||
char: u21,
|
char: u21,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const EventEnum = @typeInfo(Event).@"union".tag_type.?;
|
mouse_position: ?Vec2,
|
||||||
|
|
||||||
|
pub const initial = Input{
|
||||||
|
.mouse_position = null
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn processEvent(self: *Input, frame: *Frame, event: Event) void {
|
||||||
|
switch (event) {
|
||||||
|
.key_pressed => |opts| {
|
||||||
|
if (!opts.repeat) {
|
||||||
|
frame.keyboard.press(opts.code, frame.time_ns);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.key_released => |key_code| {
|
||||||
|
frame.keyboard.release(key_code);
|
||||||
|
},
|
||||||
|
.mouse_leave => {
|
||||||
|
frame.keyboard.releaseAll();
|
||||||
|
|
||||||
|
assert(self.mouse_position != null);
|
||||||
|
self.mouse_position = null;
|
||||||
|
frame.mouse_button = .empty;
|
||||||
|
},
|
||||||
|
.mouse_enter => |pos| {
|
||||||
|
assert(self.mouse_position == null);
|
||||||
|
self.mouse_position = pos;
|
||||||
|
},
|
||||||
|
.mouse_move => |pos| {
|
||||||
|
assert(self.mouse_position != null);
|
||||||
|
self.mouse_position = pos;
|
||||||
|
},
|
||||||
|
.mouse_pressed => |opts| {
|
||||||
|
assert(self.mouse_position != null);
|
||||||
|
self.mouse_position = opts.position;
|
||||||
|
frame.mouse_button.press(opts.button, frame.time_ns);
|
||||||
|
},
|
||||||
|
.mouse_released => |opts| {
|
||||||
|
assert(self.mouse_position != null);
|
||||||
|
self.mouse_position = opts.position;
|
||||||
|
frame.mouse_button.release(opts.button);
|
||||||
|
},
|
||||||
|
else => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getKeyCodeFromSokol(key_code: sokol.app.Keycode) ?lib.KeyCode {
|
pub fn getKeyCodeFromSokol(key_code: sokol.app.Keycode) ?lib.KeyCode {
|
||||||
return switch (key_code) {
|
return switch (key_code) {
|
||||||
@ -1,12 +1,6 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const Engine = @import("engine");
|
const Engine = @import("./engine.zig");
|
||||||
const CLI = Engine.CLI;
|
|
||||||
const build_options = Engine.Lib.build_options;
|
|
||||||
|
|
||||||
pub const std_options: std.Options = .{
|
|
||||||
// TODO: Override .logFn for logging to a file
|
|
||||||
};
|
|
||||||
|
|
||||||
var engine: Engine = undefined;
|
var engine: Engine = undefined;
|
||||||
|
|
||||||
@ -14,8 +8,6 @@ pub fn main() !void {
|
|||||||
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
|
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
|
||||||
defer _ = debug_allocator.deinit();
|
defer _ = debug_allocator.deinit();
|
||||||
|
|
||||||
const isWasm = builtin.cpu.arch.isWasm();
|
|
||||||
|
|
||||||
// TODO: Use tracy TracingAllocator
|
// TODO: Use tracy TracingAllocator
|
||||||
var allocator: std.mem.Allocator = undefined;
|
var allocator: std.mem.Allocator = undefined;
|
||||||
if (builtin.cpu.arch.isWasm()) {
|
if (builtin.cpu.arch.isWasm()) {
|
||||||
@ -26,102 +18,17 @@ pub fn main() !void {
|
|||||||
allocator = std.heap.smp_allocator;
|
allocator = std.heap.smp_allocator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const args = try std.process.argsAlloc(allocator);
|
||||||
|
defer std.process.argsFree(allocator, args);
|
||||||
|
|
||||||
var game_library_path: ?[]const u8 = null;
|
var game_library_path: ?[]const u8 = null;
|
||||||
defer if (game_library_path) |str| allocator.free(str);
|
if (args.len >= 2) {
|
||||||
|
game_library_path = args[1];
|
||||||
var assets_path: ?[]const u8 = null;
|
|
||||||
defer if (assets_path) |str| allocator.free(str);
|
|
||||||
|
|
||||||
// var asset_loading: Engine.RunOptions.AssetLoading = undefined;
|
|
||||||
// if (!build_options.asset_hot_reload) {
|
|
||||||
// asset_loading = .{
|
|
||||||
// .bundle_bytes = @embedFile("asset_bundle")
|
|
||||||
// };
|
|
||||||
// } else {
|
|
||||||
// asset_loading = .{
|
|
||||||
// .dir_path = build_options.asset_dir
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!isWasm) {
|
|
||||||
var cli: CLI = undefined;
|
|
||||||
cli.init(allocator);
|
|
||||||
defer cli.deinit();
|
|
||||||
|
|
||||||
const opt_help = try cli.addOption(.{
|
|
||||||
.long_name = "help",
|
|
||||||
.description = "Show this message"
|
|
||||||
});
|
|
||||||
|
|
||||||
var opt_game_lib_path: ?CLI.Option.Id = null;
|
|
||||||
if (build_options.code_dynamic_linking) {
|
|
||||||
opt_game_lib_path = try cli.addOption(.{
|
|
||||||
.long_name = "dynamic-lib-path",
|
|
||||||
.description = "Load game code from dynamic library",
|
|
||||||
.kind = .single_argument
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var opt_assets_path: ?CLI.Option.Id = null;
|
|
||||||
if (build_options.asset_hot_reload) {
|
|
||||||
opt_assets_path = try cli.addOption(.{
|
|
||||||
.long_name = "assets-path",
|
|
||||||
.description = "Load assets from folder at runtime",
|
|
||||||
.kind = .single_argument
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const cmd_version = try cli.addCommand(.{
|
|
||||||
.name = "version",
|
|
||||||
.description = "Show semantic version and build options"
|
|
||||||
});
|
|
||||||
|
|
||||||
const cmd_launch = try cli.addCommand(.{
|
|
||||||
.name = "launch",
|
|
||||||
});
|
|
||||||
|
|
||||||
const args = try std.process.argsAlloc(allocator);
|
|
||||||
defer std.process.argsFree(allocator, args);
|
|
||||||
|
|
||||||
var parsed = try cli.parse(allocator, args);
|
|
||||||
defer parsed.deinit();
|
|
||||||
|
|
||||||
if (parsed.isSet(opt_help)) {
|
|
||||||
cli.showUsage(args[0]);
|
|
||||||
std.process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opt_game_lib_path) |opt| {
|
|
||||||
if (parsed.getOptionArgument(opt)) |arg| {
|
|
||||||
game_library_path = try allocator.dupe(u8, arg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opt_assets_path) |opt| {
|
|
||||||
if (parsed.getOptionArgument(opt)) |arg| {
|
|
||||||
assets_path = try allocator.dupe(u8, arg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = parsed.command orelse cmd_launch;
|
|
||||||
if (command == cmd_version) {
|
|
||||||
cli.stdout.write("Version: {s}\n", .{ "TODO:" });
|
|
||||||
cli.stdout.flush();
|
|
||||||
|
|
||||||
std.process.exit(0);
|
|
||||||
|
|
||||||
} else if (command == cmd_launch) {
|
|
||||||
// Nothing to be done
|
|
||||||
|
|
||||||
} else {
|
|
||||||
unreachable;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try engine.run(.{
|
try engine.run(.{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.dynamic_library_path = game_library_path,
|
.game_library_path = game_library_path
|
||||||
.assets_path = assets_path
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
const Gfx = @import("./graphics.zig");
|
const Gfx = @import("./graphics.zig");
|
||||||
|
|
||||||
const Math = @import("engine_lib").Math;
|
const Math = @import("lib").Math;
|
||||||
const Vec2 = Math.Vec2;
|
const Vec2 = Math.Vec2;
|
||||||
const Vec4 = Math.Vec4;
|
const Vec4 = Math.Vec4;
|
||||||
const rgb = Math.rgb;
|
const rgb = Math.rgb;
|
||||||
|
|
||||||
const Lib = @import("engine_lib");
|
const Lib = @import("lib");
|
||||||
const Frame = Lib.Frame;
|
const Frame = Lib.Frame;
|
||||||
|
|
||||||
const ScreenScalar = @This();
|
const ScreenScalar = @This();
|
||||||
@ -1,4 +0,0 @@
|
|||||||
[asdf]
|
|
||||||
path = ./icon.png
|
|
||||||
|
|
||||||
# afd
|
|
||||||
77
src/game.zig
77
src/game.zig
@ -2,51 +2,51 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
const Engine = @import("engine_lib");
|
const Engine = @import("engine");
|
||||||
const FontId = Engine.Font.Id;
|
const FontId = Engine.FontId;
|
||||||
const Vec2 = Engine.Math.Vec2;
|
const Vec2 = Engine.Math.Vec2;
|
||||||
const rgb = Engine.Math.rgb;
|
const rgb = Engine.Math.rgb;
|
||||||
|
|
||||||
const Game = @This();
|
const Game = @This();
|
||||||
|
|
||||||
|
const FontName = enum {
|
||||||
|
regular,
|
||||||
|
bold,
|
||||||
|
italic,
|
||||||
|
|
||||||
|
const EnumArray = std.EnumArray(FontName, FontId);
|
||||||
|
};
|
||||||
|
|
||||||
const State = struct {
|
const State = struct {
|
||||||
gpa: Allocator,
|
gpa: Allocator,
|
||||||
player: Vec2,
|
player: Vec2,
|
||||||
};
|
|
||||||
|
|
||||||
const AssetId = enum {
|
// font_id: FontName.EnumArray,
|
||||||
icon
|
// wood01: Audio.Data.Id,
|
||||||
};
|
|
||||||
|
|
||||||
const Image = struct {
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
rgba: []const u8,
|
|
||||||
|
|
||||||
pub const nil = Image{
|
|
||||||
.width = 0,
|
|
||||||
.height = 0,
|
|
||||||
.rgba = &.{ }
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(opts: *const Engine.Init) callconv(.c) *anyopaque {
|
pub fn init(opts: *const Engine.Init) callconv(.c) *anyopaque {
|
||||||
const gpa = opts.gpa;
|
const gpa = opts.gpa;
|
||||||
|
|
||||||
|
// const font_id_array: FontName.EnumArray = .init(.{
|
||||||
|
// .regular = try Gfx.addFont("regular", @embedFile("assets/roboto-font/Roboto-Regular.ttf")),
|
||||||
|
// .bold = try Gfx.addFont("bold", @embedFile("assets/roboto-font/Roboto-Bold.ttf")),
|
||||||
|
// .italic = try Gfx.addFont("italic", @embedFile("assets/roboto-font/Roboto-Italic.ttf")),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const wood01 = try Audio.load(.{
|
||||||
|
// .format = .vorbis,
|
||||||
|
// .data = @embedFile("assets/wood01.ogg"),
|
||||||
|
// });
|
||||||
|
|
||||||
const state = gpa.create(State) catch @panic("OOM");
|
const state = gpa.create(State) catch @panic("OOM");
|
||||||
state.* = State{
|
state.* = State{
|
||||||
.gpa = gpa,
|
.gpa = gpa,
|
||||||
.player = .init(50, 50),
|
.player = .init(50, 50),
|
||||||
|
// .font_id = font_id_array,
|
||||||
|
// .wood01 = wood01
|
||||||
};
|
};
|
||||||
|
|
||||||
// var icon = opts.assets.get(ImageAsset, "icon");
|
|
||||||
// icon.id
|
|
||||||
// icon.rgba
|
|
||||||
// icon.texture_id
|
|
||||||
|
|
||||||
// std.debug.print("{}\n", .{opts.assets});
|
|
||||||
// const icon = opts.assets.get(Image, @intFromEnum(AssetId.icon));
|
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ pub fn tick(state_ptr: *anyopaque, frame: *Engine.Frame) callconv(.c) void {
|
|||||||
|
|
||||||
if (dir.x != 0 or dir.y != 0) {
|
if (dir.x != 0 or dir.y != 0) {
|
||||||
// frame.playAudio(.{
|
// frame.playAudio(.{
|
||||||
// .id = frame.assets.getSound(@intFromEnum(AssetId.sound_wood)),
|
// .id = g_state.wood01,
|
||||||
// .volume = 0.1
|
// .volume = 0.1
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
@ -92,38 +92,21 @@ pub fn tick(state_ptr: *anyopaque, frame: *Engine.Frame) callconv(.c) void {
|
|||||||
.rect = .{ .pos = .init(0, 0), .size = canvas_size },
|
.rect = .{ .pos = .init(0, 0), .size = canvas_size },
|
||||||
.color = rgb(20, 20, 20)
|
.color = rgb(20, 20, 20)
|
||||||
});
|
});
|
||||||
|
|
||||||
// const icon = frame.assets.getTexture(@intFromEnum(AssetId.texture_icon));
|
|
||||||
|
|
||||||
const size = Vec2.init(20, 20);
|
const size = Vec2.init(20, 20);
|
||||||
frame.drawRectangle(.{
|
frame.drawRectangle(.{
|
||||||
.rect = .{
|
.rect = .{
|
||||||
.pos = state.player.sub(size.divideScalar(2)),
|
.pos = state.player.sub(size.divideScalar(2)),
|
||||||
.size = size
|
.size = size
|
||||||
},
|
},
|
||||||
.color = rgb(200, 2, 200),
|
.color = rgb(200, 2, 200)
|
||||||
// .sprite = .{
|
|
||||||
// .texture = icon,
|
|
||||||
// .uv = .unit
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
if (dir.x != 0 or dir.y != 0) {
|
if (dir.x != 0 or dir.y != 0) {
|
||||||
frame.drawRectanglOutline(state.player.sub(size.divideScalar(2)), size, rgb(20, 20, 20), 3);
|
frame.drawRectanglOutline(state.player.sub(size.divideScalar(2)), size, rgb(20, 20, 20), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frame.mouse_position) |mouse_position| {
|
// const regular_font = g_state.font_id.get(.regular);
|
||||||
const mouse_size = Vec2.init(10, 10);
|
// frame.drawText(g_state.player, "Player", .{
|
||||||
frame.drawRectangle(.{
|
// .font = regular_font,
|
||||||
.rect = .{
|
|
||||||
.pos = mouse_position.sub(mouse_size.divideScalar(2)),
|
|
||||||
.size = mouse_size
|
|
||||||
},
|
|
||||||
.color = rgb(20, 2, 200)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// frame.drawText(state.player, "Player", .{
|
|
||||||
// .font = getFont(frame.assets, .regular),
|
|
||||||
// .size = 10
|
// .size = 10
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user