give up on this repo, rewrite on v2 without hot code reload

This commit is contained in:
Rokas Puzonas 2026-07-12 20:03:52 +03:00
parent 39c311e225
commit 5e9b4b43a9
9 changed files with 308 additions and 24 deletions

1
.gitignore vendored
View File

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

View File

@ -48,9 +48,6 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
build_options.addOption(bool, "code_dynamic_linking", code_dynamic_linking);
build_options.addOption(bool, "code_static_linking", code_static_linking);
build_options.addOption(bool, "asset_hot_reload", asset_hot_reload);
if (asset_hot_reload) {
build_options.addOptionPath("asset_dir", opts.asset_dir);
}
const engine_lib = b.createModule(.{
.root_source_file = b.path("src/game_lib/root.zig")
@ -140,13 +137,17 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
});
const install_game_lib = b.addInstallArtifact(game_lib, .{});
run_cmd.addArg("--game-lib-path");
run_cmd.addArg("--dynamic-lib-path");
run_cmd.addArg(b.getInstallPath(.lib, game_lib.out_lib_filename));
run_cmd.step.dependOn(&install_game_lib.step);
var game_lib_step = outer_builder.step("game-lib", "Build game dynamic library");
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;
}
}

View File

@ -106,15 +106,16 @@ fn processEvent(self: *Environment, event: Input.Event) void {
}
}
pub fn stateInit(self: *Environment, initial: Init) void {
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);
self.frame = .init(self.gpa, initial.screen_size, assets);
}
pub fn stateDeinit(self: *Environment) void {

View File

@ -58,6 +58,18 @@ const Texture = struct {
width: u32,
height: u32,
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 {
@ -103,6 +115,9 @@ scissor_stack: std.ArrayList(Rect) = .empty,
sokol_state: ?SokolState,
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 = .{},
@ -113,6 +128,7 @@ 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,
@ -198,6 +214,13 @@ pub fn setupSokol(self: *Graphics) !void {
.nearest_sampler = nearest_sampler,
.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 {
@ -525,7 +548,9 @@ pub fn removeTexture(self: *Graphics, id: Lib.Texture.Id) void {
pub fn setTextureData(self: *Graphics, id: Lib.Texture.Id, data: Texture.Data) !void {
if (self.sokol_state == null) {
return error.SokolNotSetup;
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;

View File

@ -36,14 +36,126 @@ 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,
// game_linking: GameLinking,
environment: Environment,
assets_arena: std.heap.ArenaAllocator,
assets: std.ArrayList(Lib.Asset),
queued_events: std.ArrayList(Input.Event),
is_mouse_inside: bool,
@ -51,28 +163,23 @@ debug: DebugSystem,
pub const RunOptions = struct {
allocator: std.mem.Allocator,
game_library_path: ?[]const u8 = null,
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 game_linking: GameLinking = undefined;
// if (opts.game_library_path) |game_library_path| {
// game_linking = try .initDynamic(opts.allocator, game_library_path);
// } else {
// game_linking = try .initStatic();
// }
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.game_library_path != null) {
var dynamic = try GameLinking.Dynamic.init(opts.allocator, opts.game_library_path.?);
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;
}
@ -105,10 +212,62 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
.queued_events = .empty,
.seed = initial.seed,
.is_mouse_inside = false,
.debug = debug_system
.debug = debug_system,
.assets = .empty,
.assets_arena = std.heap.ArenaAllocator.init(opts.allocator)
};
self.environment.stateInit(initial);
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) {
@ -169,6 +328,30 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
});
}
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();
@ -206,6 +389,7 @@ fn sokolCleanup(self: *Engine) void {
self.audio.deinit();
self.graphics.deinit(self.allocator);
self.debug.deinit();
self.assets_arena.deinit();
// TODO:
// self.game_linking.deinit(self.allocator);
}

View File

@ -270,6 +270,27 @@ 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 {
arena: std.heap.ArenaAllocator,
time_ns: Nanoseconds,
@ -288,9 +309,12 @@ pub const Frame = struct {
screen_size: Vec2,
clear_color: Vec4,
assets: Asset.List,
pub fn init(
gpa: std.mem.Allocator,
screen_size: Vec2
screen_size: Vec2,
assets: Asset.List,
) Frame {
return Frame{
.arena = std.heap.ArenaAllocator.init(gpa),
@ -306,6 +330,7 @@ pub const Frame = struct {
.audio_commands = .empty,
.hide_cursor = false,
.screen_size = screen_size,
.assets = assets
};
}
@ -473,6 +498,7 @@ pub const Frame = struct {
pub const Init = struct {
gpa: std.mem.Allocator,
seed: u64,
assets: Asset.List,
};
pub const Callbacks = struct {

View File

@ -29,6 +29,9 @@ pub fn main() !void {
var game_library_path: ?[]const u8 = null;
defer if (game_library_path) |str| allocator.free(str);
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 = .{
@ -53,12 +56,21 @@ pub fn main() !void {
var opt_game_lib_path: ?CLI.Option.Id = null;
if (build_options.code_dynamic_linking) {
opt_game_lib_path = try cli.addOption(.{
.long_name = "game-lib-path",
.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"
@ -85,6 +97,12 @@ pub fn main() !void {
}
}
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:" });
@ -102,8 +120,8 @@ pub fn main() !void {
try engine.run(.{
.allocator = allocator,
.game_library_path = game_library_path,
// .asset_loading = asset_loading
.dynamic_library_path = game_library_path,
.assets_path = assets_path
});
}

View File

@ -0,0 +1,4 @@
[asdf]
path = ./icon.png
# afd

View File

@ -14,6 +14,22 @@ const State = struct {
player: Vec2,
};
const AssetId = enum {
icon
};
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 {
const gpa = opts.gpa;
@ -23,6 +39,14 @@ pub fn init(opts: *const Engine.Init) callconv(.c) *anyopaque {
.player = .init(50, 50),
};
// 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;
}