basic asset loading

This commit is contained in:
Rokas Puzonas 2026-02-15 23:03:08 +02:00
parent f80859877c
commit 6b48af7ea9
8 changed files with 693 additions and 276 deletions

View File

@ -22,6 +22,7 @@ pub fn build(b: *std.Build) !void {
.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"),
.hot_reload = b.option(bool, "hot-reload", "Dynamically load game code at runtime"), .hot_reload = b.option(bool, "hot-reload", "Dynamically load game code at runtime"),
.asset_dir = b.path("src/assets/"),
}); });
{ {

View File

@ -19,22 +19,27 @@ const InitOptions = struct {
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,
load_assets_at_runtime: ?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 hot_reload = false; var hot_reload = false;
var load_assets_at_runtime = false;
if (!isWasm) { if (!isWasm) {
hot_reload = opts.hot_reload orelse (opts.optimize == .Debug); hot_reload = opts.hot_reload orelse isDebug;
has_tracy = opts.has_tracy orelse (opts.optimize == .Debug); has_tracy = opts.has_tracy orelse isDebug;
load_assets_at_runtime = opts.load_assets_at_runtime orelse isDebug;
} }
const has_imgui = opts.has_imgui orelse (opts.optimize == .Debug); const has_imgui = opts.has_imgui orelse isDebug;
const statically_linked = opts.statically_linked orelse true; const statically_linked = opts.statically_linked orelse true;
var build_options = b.addOptions(); var build_options = b.addOptions();
@ -42,6 +47,10 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
build_options.addOption(bool, "has_tracy", has_tracy); build_options.addOption(bool, "has_tracy", has_tracy);
build_options.addOption(bool, "hot_reload", hot_reload); build_options.addOption(bool, "hot_reload", hot_reload);
build_options.addOption(bool, "statically_linked", statically_linked); build_options.addOption(bool, "statically_linked", statically_linked);
build_options.addOption(bool, "load_assets_at_runtime", load_assets_at_runtime);
if (load_assets_at_runtime) {
build_options.addOptionPath("asset_dir", opts.asset_dir);
}
const engine_lib = b.createModule(.{ const engine_lib = b.createModule(.{
.root_source_file = b.path("src/lib/root.zig") .root_source_file = b.path("src/lib/root.zig")

View File

@ -37,7 +37,7 @@ pub fn GenerationalArrayList(
pub const Id = packed struct { pub const Id = packed struct {
pub const Int = @Type(.{ pub const Int = @Type(.{
.int = .{ .int = .{
.bits = @bitSizeOf(Index) + @bitSizeOf(Generation), .bits = @bitSizeOf(Id),
.signedness = .unsigned .signedness = .unsigned
} }
}); });
@ -62,6 +62,10 @@ pub fn GenerationalArrayList(
pub fn asInt(self: Id) Int { pub fn asInt(self: Id) Int {
return @bitCast(self); return @bitCast(self);
} }
pub fn fromInt(self: Int) Id {
return @bitCast(self);
}
}; };
pub const ItemWithId = struct { pub const ItemWithId = struct {
@ -227,7 +231,6 @@ pub fn GenerationalArrayList(
} }
self.markUnused(unused_index, false); self.markUnused(unused_index, false);
self.count += 1;
const id = Id{ const id = Id{
.index = @intCast(unused_index), .index = @intCast(unused_index),
@ -324,7 +327,6 @@ pub fn GenerationalArrayList(
.generations = generations.ptr, .generations = generations.ptr,
.unused = unused.ptr, .unused = unused.ptr,
.len = self.len, .len = self.len,
.count = self.count,
.capacity = self.capacity .capacity = self.capacity
}; };
} }
@ -332,7 +334,6 @@ pub fn GenerationalArrayList(
pub fn getMetadata(self: *Self) Metadata { pub fn getMetadata(self: *Self) Metadata {
return Metadata{ return Metadata{
.len = self.len, .len = self.len,
.count = self.count
}; };
} }
@ -362,7 +363,6 @@ pub fn GenerationalArrayList(
try reader.readSliceAll(self.unused[0..divCeilGeneration(metadata.len)]); try reader.readSliceAll(self.unused[0..divCeilGeneration(metadata.len)]);
self.len = metadata.len; self.len = metadata.len;
self.count = metadata.count;
} }
}; };
} }
@ -467,7 +467,6 @@ test "read & write" {
try expectEqual(array_list1.getAssumeExists(id1).*, array_list2.getAssumeExists(id1).*); 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(id2).*, array_list2.getAssumeExists(id2).*);
try expectEqual(array_list1.getAssumeExists(id3).*, array_list2.getAssumeExists(id3).*); try expectEqual(array_list1.getAssumeExists(id3).*, array_list2.getAssumeExists(id3).*);
try expectEqual(array_list1.count, array_list2.count);
} }
test "clear retaining capacity" { test "clear retaining capacity" {

View File

@ -142,9 +142,21 @@ pub const MouseButton = enum {
middle, middle,
}; };
pub const TextureId = enum(u16) { _ }; pub const TextureId = enum(u16) {
pub const FontId = enum(u16) { _ }; _,
pub const SoundId = enum(u16) { _ }; };
pub const FontId = enum(u16) {
_,
pub const nil: FontId = @enumFromInt(std.math.maxInt(@typeInfo(FontId).@"enum".tag_type));
};
pub const SoundId = enum(u16) {
_,
pub const nil: SoundId = @enumFromInt(std.math.maxInt(@typeInfo(SoundId).@"enum".tag_type));
};
pub const Sprite = struct { pub const Sprite = struct {
texture: TextureId, texture: TextureId,
@ -287,7 +299,12 @@ pub const Frame = struct {
screen_size: Vec2, screen_size: Vec2,
clear_color: Vec4, clear_color: Vec4,
pub fn init(gpa: std.mem.Allocator, screen_size: Vec2) Frame { assets: Asset.Registry,
pub fn init(
gpa: std.mem.Allocator,
screen_size: Vec2
) Frame {
return Frame{ return Frame{
.arena = std.heap.ArenaAllocator.init(gpa), .arena = std.heap.ArenaAllocator.init(gpa),
.time_ns = 0, .time_ns = 0,
@ -302,6 +319,7 @@ pub const Frame = struct {
.audio_commands = .empty, .audio_commands = .empty,
.hide_cursor = false, .hide_cursor = false,
.screen_size = screen_size, .screen_size = screen_size,
.assets = .empty
}; };
} }
@ -468,7 +486,124 @@ 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.Registry,
};
pub const Asset = struct {
id: Id,
kind: union(Type) {
font: FontId,
sound: SoundId,
texture: TextureId,
},
pub const Id = u32;
pub const Type = enum {
font,
sound,
texture
};
pub const Loader = struct {
const Descriptor = struct {
id: Id,
kind: union(Type) {
font: []const u8,
sound: []const u8,
texture: []const u8,
}
};
arena: std.heap.ArenaAllocator,
dir: std.fs.Dir,
assets: std.ArrayList(Descriptor),
pub fn init(gpa: std.mem.Allocator, dir: std.fs.Dir) Loader {
return Loader{
.arena = .init(gpa),
.dir = dir,
.assets = .empty,
};
}
pub fn deinit(self: *Loader) void {
self.arena.deinit();
self.* = undefined;
}
pub fn addFont(self: *Loader, id: Asset.Id, path: []const u8) void {
const allocator = self.arena.allocator();
const path_dupe = allocator.dupe(u8, path) catch |e| {
log.err("Failed to duplicate path: {}", .{e});
return;
};
self.assets.append(allocator, Descriptor{
.id = id,
.kind = .{ .font = path_dupe }
}) catch |e| {
log.err("Failed add sound: {}", .{e});
};
}
pub fn addSound(self: *Loader, id: Asset.Id, path: []const u8) void {
const allocator = self.arena.allocator();
const path_dupe = allocator.dupe(u8, path) catch |e| {
log.err("Failed to duplicate path: {}", .{e});
return;
};
self.assets.append(allocator, Descriptor{
.id = id,
.kind = .{ .sound = path_dupe }
}) catch |e| {
log.err("Failed add sound: {}", .{e});
};
}
};
pub const Registry = struct {
items: []const Asset,
pub const empty = Registry{
.items = &.{}
};
pub fn get(self: Registry, id: Id) ?Asset {
for (self.items) |asset| {
if (asset.id == id) {
return asset;
}
}
return null;
}
pub fn getSound(self: Registry, id: Id) SoundId {
const asset = self.get(id) orelse return SoundId.nil;
if (asset.kind != Type.sound) {
return SoundId.nil;
}
return asset.kind.sound;
}
pub fn getFont(self: Registry, id: Id) FontId {
const asset = self.get(id) orelse return FontId.nil;
if (asset.kind != Type.font) {
return FontId.nil;
}
return asset.kind.font;
}
pub fn getTexture(self: Registry, id: Id) TextureId {
const asset = self.get(id) orelse return TextureId.nil;
if (asset.kind != Type.font) {
return TextureId.nil;
}
return asset.kind.font;
}
};
}; };
pub const Callbacks = struct { pub const Callbacks = struct {
@ -478,10 +613,12 @@ pub const Callbacks = struct {
pub const TickFn = *const fn(state: State, frame: *Frame) callconv(.c) void; pub const TickFn = *const fn(state: State, frame: *Frame) callconv(.c) void;
pub const DeinitFn = *const fn(state: State) callconv(.c) void; pub const DeinitFn = *const fn(state: State) callconv(.c) void;
pub const DebugFn = *const fn(state: State, imgui: *ImGui) callconv(.c) void; pub const DebugFn = *const fn(state: State, imgui: *ImGui) callconv(.c) void;
pub const LoadAssetsFn = *const fn(assets: *Asset.Loader) callconv(.c) void;
init: InitFn, init: InitFn,
tick: TickFn, tick: TickFn,
deinit: DeinitFn, deinit: DeinitFn,
loadAssets: if (build_options.load_assets_at_runtime) LoadAssetsFn else void,
debug: if (build_options.has_imgui) DebugFn else void debug: if (build_options.has_imgui) DebugFn else void
}; };
@ -489,7 +626,8 @@ pub fn exportCallbacksIfNeeded(
comptime init: Callbacks.InitFn, comptime init: Callbacks.InitFn,
comptime tick: Callbacks.TickFn, comptime tick: Callbacks.TickFn,
comptime deinit: Callbacks.DeinitFn, comptime deinit: Callbacks.DeinitFn,
comptime debug: Callbacks.DebugFn comptime debug: Callbacks.DebugFn,
comptime assets: Callbacks.LoadAssetsFn,
) void { ) void {
const builtin = @import("builtin"); const builtin = @import("builtin");
if (builtin.output_mode != .Lib) { if (builtin.output_mode != .Lib) {
@ -499,6 +637,9 @@ pub fn exportCallbacksIfNeeded(
@export(init, .{ .name = "init", .linkage = .strong }); @export(init, .{ .name = "init", .linkage = .strong });
@export(tick, .{ .name = "tick", .linkage = .strong }); @export(tick, .{ .name = "tick", .linkage = .strong });
@export(deinit, .{ .name = "deinit", .linkage = .strong }); @export(deinit, .{ .name = "deinit", .linkage = .strong });
if (build_options.load_assets_at_runtime) {
@export(assets, .{ .name = "loadAssets", .linkage = .strong });
}
if (build_options.has_imgui) { if (build_options.has_imgui) {
@export(debug, .{ .name = "debug", .linkage = .strong }); @export(debug, .{ .name = "debug", .linkage = .strong });
} }

View File

@ -31,21 +31,37 @@ const Sound = union(enum) {
return self.sample_rate; return self.sample_rate;
} }
pub fn streamChannel(self: Raw, opts: StreamOptions) []f32 { fn streamChannel(self: Raw, buffer: []f32, cursor: u32, channel_index: usize) usize {
assert(opts.sample_rate == self.sample_rate); // TODO: assert(channel_index < self.channels.len); // TODO:
assert(opts.channel_index < self.channels.len); // TODO:
const channel = self.channels[opts.channel_index]; const channel = self.channels[channel_index];
var memcpy_len: usize = 0; var memcpy_len: usize = 0;
if (opts.cursor + opts.buffer.len <= self.sample_count) { if (cursor + buffer.len <= self.sample_count) {
memcpy_len = opts.buffer.len; memcpy_len = buffer.len;
} else if (opts.cursor < opts.sample_count) { } else if (cursor < self.sample_count) {
memcpy_len = opts.sample_count - opts.cursor; memcpy_len = self.sample_count - cursor;
} }
@memcpy(opts.buffer[0..memcpy_len], channel[opts.cursor..][0..memcpy_len]); @memcpy(buffer[0..memcpy_len], channel[cursor..][0..memcpy_len]);
return opts.buffer[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
} }
}; };
@ -53,9 +69,10 @@ const Sound = union(enum) {
alloc_buffer: []u8, alloc_buffer: []u8,
stb_vorbis: STBVorbis, stb_vorbis: STBVorbis,
fn init(arena: std.mem.Allocator, data: []const u8, temp_vorbis_alloc_buffer: []u8) Vorbis { 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 temp_stb_vorbis = try STBVorbis.init(data, temp_vorbis_alloc_buffer);
const alloc_buffer = try arena.alloc(u8, temp_stb_vorbis.getMinimumAllocBufferSize()); 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 // 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 // And there can't be a decode error because `temp_stb_vorbis` was successfully initialized
@ -76,25 +93,45 @@ const Sound = union(enum) {
return info.sample_rate; return info.sample_rate;
} }
pub fn streamChannel(self: Vorbis, opts: StreamOptions) []f32 { fn getChannels(self: Vorbis) u32 {
_ = self; // autofix const info = self.stb_vorbis.getInfo();
_ = opts; // autofix 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"); @panic("TODO");
} }
fn deinit(self: Vorbis, gpa: std.mem.Allocator) void {
gpa.free(self.alloc_buffer);
}
}; };
const StreamOptions = struct { const StreamOptions = struct {
buffer: []f32, channels: []const [*]f32,
cursor: u32, channel_size: usize,
channel_index: u32, cursor: u32
sample_rate: u32
}; };
pub fn streamChannel(self: Sound, opts: StreamOptions) []f32 { 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) { return switch (self) {
.nil => opts.buffer[0..0], .nil => 0,
.raw => |raw| raw.streamChannel(opts), .raw => |raw| raw.streamChannels(opts),
.vorbis => |vorbis| vorbis.streamChannel(opts), .vorbis => |vorbis| vorbis.streamChannels(opts),
}; };
} }
@ -212,14 +249,28 @@ const ThreadState = struct {
} }
}; };
const SoundsList = Lib.GenerationalArrayList(u8, u8, Sound); const SokolSetupOptions = struct {
logger: saudio.Logger,
channels: u32,
buffer_frames: u32
};
const SoundList = Lib.GenerationalArrayList(u8, u8, Sound);
comptime {
assert(@bitSizeOf(SoundList.Id) == @bitSizeOf(Lib.SoundId));
}
gpa: std.mem.Allocator, gpa: std.mem.Allocator,
running: std.atomic.Value(bool), running: std.atomic.Value(bool),
temp_vorbis_alloc_buffer: []u8, 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_arena: std.heap.ArenaAllocator,
sounds: SoundsList, sounds: SoundList,
thread_state: ThreadState, thread_state: ThreadState,
@ -234,8 +285,9 @@ const Options = struct {
max_commands: u32 = 64, max_commands: u32 = 64,
}; };
pub fn init(self: *AudioSystem, opts: Options) !void { pub fn init(opts: Options) !AudioSystem {
const gpa = opts.allocator; const gpa = opts.allocator;
const temp_vorbis_alloc_buffer = try gpa.alloc(u8, opts.max_vorbis_alloc_buffer_size); const temp_vorbis_alloc_buffer = try gpa.alloc(u8, opts.max_vorbis_alloc_buffer_size);
errdefer gpa.free(temp_vorbis_alloc_buffer); errdefer gpa.free(temp_vorbis_alloc_buffer);
@ -246,15 +298,28 @@ pub fn init(self: *AudioSystem, opts: Options) !void {
); );
errdefer thread_state.deinit(gpa); errdefer thread_state.deinit(gpa);
self.* = AudioSystem{ return AudioSystem{
.gpa = gpa, .gpa = gpa,
.running = .init(false), .running = .init(false),
.temp_vorbis_alloc_buffer = temp_vorbis_alloc_buffer, .temp_vorbis_alloc_buffer = temp_vorbis_alloc_buffer,
.sounds = .empty, .sounds = .empty,
.sounds_arena = std.heap.ArenaAllocator.init(gpa), .sounds_arena = std.heap.ArenaAllocator.init(gpa),
.thread_state = thread_state, .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(.{ saudio.setup(.{
.stream_userdata_cb = sokolStreamCallback, .stream_userdata_cb = sokolStreamCallback,
.user_data = self, .user_data = self,
@ -262,6 +327,7 @@ pub fn init(self: *AudioSystem, opts: Options) !void {
.num_channels = @intCast(opts.channels), .num_channels = @intCast(opts.channels),
.buffer_frames = @intCast(opts.buffer_frames), .buffer_frames = @intCast(opts.buffer_frames),
}); });
self.sokol_setup = true;
self.running.store(true, .seq_cst); self.running.store(true, .seq_cst);
const sample_rate: f32 = @floatFromInt(saudio.sampleRate()); const sample_rate: f32 = @floatFromInt(saudio.sampleRate());
@ -277,7 +343,9 @@ pub fn init(self: *AudioSystem, opts: Options) !void {
pub fn deinit(self: *AudioSystem) void { pub fn deinit(self: *AudioSystem) void {
self.running.store(false, .seq_cst); self.running.store(false, .seq_cst);
if (self.sokol_setup) {
saudio.shutdown(); saudio.shutdown();
}
self.thread_state.deinit(self.gpa); self.thread_state.deinit(self.gpa);
self.gpa.free(self.temp_vorbis_alloc_buffer); self.gpa.free(self.temp_vorbis_alloc_buffer);
@ -285,7 +353,24 @@ pub fn deinit(self: *AudioSystem) void {
self.sounds.deinit(self.gpa); self.sounds.deinit(self.gpa);
} }
pub const LoadOptions = struct { pub fn addSound(self: *AudioSystem) !Lib.SoundId {
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.SoundId) void {
self.sounds_mutex.lock();
defer self.sounds_mutex.unlock();
_ = self.sounds.remove(.fromInt(@intFromEnum(id)));
}
pub const DataOptions = struct {
const PlaybackStyle = enum { const PlaybackStyle = enum {
stream, stream,
decode_once, decode_once,
@ -303,66 +388,64 @@ pub const LoadOptions = struct {
playback_style: ?PlaybackStyle = null, playback_style: ?PlaybackStyle = null,
}; };
pub fn load(self: *AudioSystem, opts: LoadOptions) !Lib.SoundId { pub fn setSoundData(self: *AudioSystem, id: Lib.SoundId, opts: DataOptions) !void {
_ = self; // autofix const sound = self.sounds.get(.fromInt(@intFromEnum(id))) orelse return error.SoundNotFound;
_ = opts; // autofix
@panic("TODO");
// const id = self.sounds.items.len; assert(opts.format == .vorbis);
// try self.sounds.ensureUnusedCapacity(self.gpa, 1); const vorbis = try Sound.Vorbis.init(self.gpa, opts.data, self.temp_vorbis_alloc_buffer);
//
// // const list = std.ArrayList(u32).initBuffer(&.{});
// // list.unused
//
// const arena_allocator = self.sounds_arena.allocator();
//
// assert(opts.format == .vorbis);
// // const vorbis = try Sound.Vorbis.init(arena_allocator, opts.data, self.temp_vorbis_alloc_buffer);
//
// 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 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.sounds.appendAssumeCapacity(Sound{
// .raw = .{
// .channels = channels,
// .sample_count = duration_in_samples,
// .sample_rate = info.sample_rate
// }
// });
// } else {
// self.sounds.appendAssumeCapacity(Sound{
// .vorbis = try Sound.Vorbis.init(arena_allocator, opts.data, self.temp_vorbis_alloc_buffer)
// });
// }
//
// return @enumFromInt(id);
}
pub fn unload(self: *AudioSystem, id: Lib.SoundId) void { const PlaybackStyle = DataOptions.PlaybackStyle;
_ = self; // autofix var playback_style: PlaybackStyle = undefined;
_ = id; // autofix if (opts.playback_style == null) {
@panic("TODO"); const stream_threshold = PlaybackStyle.stream_threshold;
const decoded_size = vorbis.getChannels() * vorbis.getSampleCount() * @sizeOf(f32);
const default_syle: PlaybackStyle = if (decoded_size < 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;
}
} }
pub fn get(self: *AudioSystem, id: Lib.SoundId) Sound { pub fn get(self: *AudioSystem, id: Lib.SoundId) Sound {
return self.sounds.items[@intFromEnum(id)]; return self.sounds.items[@intFromEnum(id)];
} }
fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: i32, num_channels: i32) void { fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: u32, num_channels: u32) void {
if (!self.running.load(.seq_cst)) { if (!self.running.load(.seq_cst)) {
return; return;
} }
@ -370,6 +453,9 @@ fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: i32, num_channel
const zone = tracy.initZone(@src(), .{ }); const zone = tracy.initZone(@src(), .{ });
defer zone.deinit(); defer zone.deinit();
self.sounds_mutex.lock();
defer self.sounds_mutex.unlock();
const thread_state = &self.thread_state; const thread_state = &self.thread_state;
while (thread_state.commands.pop()) |command| { while (thread_state.commands.pop()) |command| {
switch (command) { switch (command) {
@ -394,36 +480,46 @@ fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: i32, num_channel
assert(thread_state.temp_channel_buffer.len >= num_frames); assert(thread_state.temp_channel_buffer.len >= num_frames);
@memset(buffer[0..@intCast(num_frames * num_channels)], 0); @memset(buffer[0..@intCast(num_frames * num_channels)], 0);
_ = sample_rate; // autofix
for (thread_state.instances.items) |*instance| { for (thread_state.instances.items) |*instance| {
_ = instance; // autofix const sound = self.sounds.get(.fromInt(@intFromEnum(instance.sound_id))) orelse continue;
// const sound = self.sounds assert(sound.getSampleRate() == sample_rate); // TODO:
// const audio_data = store.get(instance.data_id);
// const samples = audio_data.streamChannel(self.working_buffer[0..num_frames], instance.cursor, 0, sample_rate); const sample_count = sound.streamChannels(.{
// for (0.., samples) |i, sample| { .channels = &.{
// buffer[i] += sample * instance.volume; thread_state.temp_channel_buffer.ptr,
// } },
// instance.cursor += @intCast(samples.len); .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; var i: usize = 0;
// while (i < thread_state.instances.items.len) { while (i < thread_state.instances.items.len) {
// const instance = thread_state.instances.items[i]; const instance = thread_state.instances.items[i];
// const audio_data = store.get(instance.data_id);
// const is_complete = instance.cursor == audio_data.getSampleCount(); var is_complete = true;
// if (self.sounds.get(.fromInt(@intFromEnum(instance.sound_id)))) |sound| {
// if (is_complete) { is_complete = instance.cursor >= sound.getSampleCount();
// _ = self.instances.swapRemove(i); }
// } else {
// i += 1; 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 { fn sokolStreamCallback(buffer: [*c]f32, num_frames: i32, num_channels: i32, user_data: ?*anyopaque) callconv(.c) void {
const audio: *AudioSystem = @alignCast(@ptrCast(user_data)); const audio: *AudioSystem = @alignCast(@ptrCast(user_data));
audio.sokolStream(buffer, num_frames, num_channels); audio.sokolStream(buffer, @intCast(num_frames), @intCast(num_channels));
} }

View File

@ -115,6 +115,7 @@ const GameLinking = struct {
.init = game.init, .init = game.init,
.deinit = game.deinit, .deinit = game.deinit,
.tick = game.tick, .tick = game.tick,
.loadAssets = if (build_options.load_assets_at_runtime) game.loadAssets else {},
.debug = if (build_options.has_imgui) game.debug else {}, .debug = if (build_options.has_imgui) game.debug else {},
} }
}; };
@ -129,12 +130,17 @@ const GameLinking = struct {
} }
} }
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 { fn getCallbacksFromLibrary(lib: *std.DynLib) !GameCallbacks {
return GameCallbacks{ return GameCallbacks{
.init = lib.lookup(GameCallbacks.InitFn, "init") orelse return error.MissingFunction, .init = try ensureLookup(lib, GameCallbacks.InitFn, "init"),
.tick = lib.lookup(GameCallbacks.TickFn, "tick") orelse return error.MissingFunction, .tick = try ensureLookup(lib, GameCallbacks.TickFn, "tick"),
.deinit = lib.lookup(GameCallbacks.DeinitFn, "deinit") orelse return error.MissingFunction, .deinit = try ensureLookup(lib, GameCallbacks.DeinitFn, "deinit"),
.debug = if (build_options.has_imgui) lib.lookup(GameCallbacks.DebugFn, "debug") orelse return error.MissingFunction else {}, .loadAssets = if (build_options.load_assets_at_runtime) try ensureLookup(lib, GameCallbacks.LoadAssetsFn, "loadAssets") else {},
.debug = if (build_options.has_imgui) try ensureLookup(lib, GameCallbacks.DebugFn, "debug") else {},
}; };
} }
@ -153,6 +159,8 @@ const GameLinking = struct {
var need_to_reload = false; var need_to_reload = false;
// TODO: clean this up.
// Reuse the same watching logic for assets
var events_buf: [256 + 4096]u8 = undefined; var events_buf: [256 + 4096]u8 = undefined;
while (true) { while (true) {
var len = std.posix.read(fan_fd, &events_buf) catch |err| switch (err) { var len = std.posix.read(fan_fd, &events_buf) catch |err| switch (err) {
@ -250,14 +258,13 @@ const GameState = struct {
gpa: std.mem.Allocator, gpa: std.mem.Allocator,
state: ?GameCallbacks.State, state: ?GameCallbacks.State,
frame: ?Lib.Frame,
mouse_position: ?Vec2, mouse_position: ?Vec2,
frame: Lib.Frame,
started_at: std.time.Instant, started_at: std.time.Instant,
last_frame_at: Lib.Nanoseconds, last_frame_at: Lib.Nanoseconds,
seed: u64,
const Initial = struct { const Init = struct {
screen_size: Vec2, screen_size: Vec2,
seed: u64 seed: u64
}; };
@ -274,24 +281,31 @@ const GameState = struct {
cursor_hidden: bool cursor_hidden: bool
}; };
pub fn init(gpa: std.mem.Allocator, initial: Initial) GameState { pub fn init(gpa: std.mem.Allocator) GameState {
return GameState{ return GameState{
.gpa = gpa, .gpa = gpa,
.mouse_position = null, .mouse_position = null,
.frame = .init(gpa, initial.screen_size),
.started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"), .started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"),
.last_frame_at = 0, .last_frame_at = 0,
.seed = initial.seed, .state = null,
.state = null .frame = null,
}; };
} }
pub fn deinit(self: *GameState) void {
if (self.frame) |*frame| {
frame.deinit();
}
}
fn processEvent(self: *GameState, event: Input.Event) void { fn processEvent(self: *GameState, event: Input.Event) void {
const frame = &self.frame; assert(self.frame != null);
const frame = &self.frame.?;
switch (event) { switch (event) {
.key_pressed => |opts| { .key_pressed => |opts| {
if (!opts.repeat) { if (!opts.repeat) {
frame.keyboard.press(opts.code, self.frame.time_ns); frame.keyboard.press(opts.code, frame.time_ns);
} }
}, },
.key_released => |key_code| { .key_released => |key_code| {
@ -329,12 +343,21 @@ const GameState = struct {
} }
} }
fn runGameInit(self: *GameState, init_fn: GameCallbacks.InitFn) void { fn runGameInit(self: *GameState, init_fn: GameCallbacks.InitFn, initial: Init, assets: []const Lib.Asset) void {
assert(self.frame == null);
const opts = Lib.Init{ const opts = Lib.Init{
.gpa = self.gpa, .gpa = self.gpa,
.seed = self.seed, .seed = initial.seed,
.assets = .{
.items = assets
}
}; };
self.state = init_fn(&opts); self.state = init_fn(&opts);
self.frame = .init(self.gpa, initial.screen_size);
self.frame.?.assets = .{
.items = assets
};
} }
fn runGameDeinit(self: *GameState, deinit_fn: GameCallbacks.DeinitFn) void { fn runGameDeinit(self: *GameState, deinit_fn: GameCallbacks.DeinitFn) void {
@ -343,13 +366,9 @@ const GameState = struct {
self.state = null; self.state = null;
} }
fn runGameTick(self: *GameState, tick_fn: GameCallbacks.TickFn) void {
assert(self.state != null);
tick_fn(self.state.?, &self.frame);
}
fn tick(self: *GameState, tick_fn: GameCallbacks.TickFn, opts: GameState.Tick) !TickResult { fn tick(self: *GameState, tick_fn: GameCallbacks.TickFn, opts: GameState.Tick) !TickResult {
const frame = &self.frame; assert(self.frame != null);
const frame = &self.frame.?;
_ = frame.arena.reset(.retain_capacity); _ = frame.arena.reset(.retain_capacity);
const arena = frame.arena.allocator(); const arena = frame.arena.allocator();
@ -403,7 +422,8 @@ const GameState = struct {
} }
} }
self.runGameTick(tick_fn); assert(self.state != null);
tick_fn(self.state.?, frame);
if (maybe_screen_scaler) |screen_scaler| { if (maybe_screen_scaler) |screen_scaler| {
screen_scaler.pop(frame, frame.clear_color); screen_scaler.pop(frame, frame.clear_color);
@ -415,10 +435,10 @@ const GameState = struct {
frame.mouse_button.released = .initEmpty(); frame.mouse_button.released = .initEmpty();
return TickResult{ return TickResult{
.graphics = self.frame.graphics_commands.items, .graphics = frame.graphics_commands.items,
.audio = self.frame.audio_commands.items, .audio = frame.audio_commands.items,
.clear_color = self.frame.clear_color, .clear_color = frame.clear_color,
.cursor_hidden = self.frame.hide_cursor .cursor_hidden = frame.hide_cursor
}; };
} }
@ -426,19 +446,15 @@ const GameState = struct {
assert(self.state != null); assert(self.state != null);
debug_fn(self.state.?, imgui_ctx); debug_fn(self.state.?, imgui_ctx);
} }
pub fn deinit(self: *GameState) void {
self.frame.deinit();
}
}; };
const Recording = struct { const Recording = struct {
arena: std.heap.ArenaAllocator, arena: std.heap.ArenaAllocator,
initial: GameState.Initial, initial: GameState.Init,
ticks: std.ArrayList(GameState.Tick), ticks: std.ArrayList(GameState.Tick),
pub fn init(gpa: std.mem.Allocator, initial: GameState.Initial) Recording { pub fn init(gpa: std.mem.Allocator, initial: GameState.Init) Recording {
return Recording{ return Recording{
.arena = .init(gpa), .arena = .init(gpa),
.ticks = .empty, .ticks = .empty,
@ -466,6 +482,7 @@ allocator: std.mem.Allocator,
graphics: Graphics, graphics: Graphics,
audio: Audio, audio: Audio,
assets: std.ArrayList(Lib.Asset),
game_linking: GameLinking, game_linking: GameLinking,
game_state: GameState, game_state: GameState,
@ -497,18 +514,26 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
game_linking = try .initStatic(); game_linking = try .initStatic();
} }
const initial = GameState.Initial{ const initial = GameState.Init{
.seed = @bitCast(std.time.milliTimestamp()), .seed = @bitCast(std.time.milliTimestamp()),
.screen_size = .initFromInt(u32, opts.screen_width, opts.screen_height) .screen_size = .initFromInt(u32, opts.screen_width, opts.screen_height)
}; };
var game_state: GameState = .init(opts.allocator, initial);
game_state.runGameInit(game_linking.callbacks.init);
self.* = Engine{ self.* = Engine{
.allocator = opts.allocator, .allocator = opts.allocator,
.graphics = undefined, .graphics = try .init(.{
.audio = undefined, .allocator = opts.allocator,
.game_state = game_state, .logger = .{ .func = sokolLogCallback },
// TODO:
// .imgui_font = .{
// .ttf_data = @embedFile("../assets/roboto-font/Roboto-Regular.ttf"),
// }
}),
.audio = try .init(.{
.allocator = opts.allocator,
.logger = .{ .func = sokolLogCallback },
}),
.game_state = .init(opts.allocator),
.game_linking = game_linking, .game_linking = game_linking,
.show_debug = false, .show_debug = false,
.restart_game = false, .restart_game = false,
@ -518,8 +543,65 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
.play_recording = false, .play_recording = false,
.recording_tick = 0, .recording_tick = 0,
.seed = initial.seed, .seed = initial.seed,
.assets = .empty
}; };
if (build_options.load_assets_at_runtime) {
var dir = try std.fs.cwd().openDir(build_options.asset_dir, .{ .iterate = true });
defer dir.close();
var loader: Lib.Asset.Loader = .init(opts.allocator, dir);
defer loader.deinit();
game_linking.callbacks.loadAssets(&loader);
for (loader.assets.items) |asset_desc| {
switch (asset_desc.kind) {
.font => |path| {
const f = try dir.openFile(path, .{});
defer f.close();
var wa: std.Io.Writer.Allocating = .init(self.allocator);
defer wa.deinit();
var buffer: [4 * 4096]u8 = undefined;
var reader = f.reader(&buffer);
_ = try reader.interface.stream(&wa.writer, .unlimited);
const font_id = try self.graphics.addFont();
try self.graphics.setFontData(font_id, wa.written());
try self.assets.append(self.allocator, .{
.id = asset_desc.id,
.kind = .{ .font = font_id }
});
},
.texture => |path| {
_ = path; // autofix
},
.sound => |path| {
const f = try dir.openFile(path, .{});
defer f.close();
var wa: std.Io.Writer.Allocating = .init(self.allocator);
defer wa.deinit();
var buffer: [4 * 4096]u8 = undefined;
var reader = f.reader(&buffer);
_ = try reader.interface.stream(&wa.writer, .unlimited);
const sound_id = try self.audio.addSound();
try self.audio.setSoundData(sound_id, .{
.data = wa.written(),
.format = .vorbis
});
try self.assets.append(self.allocator, .{
.id = asset_desc.id,
.kind = .{ .sound = sound_id }
});
},
}
}
}
self.game_state.runGameInit(game_linking.callbacks.init, initial, self.assets.items);
if (opts.do_recording) { if (opts.do_recording) {
self.recording = .init(opts.allocator, initial); self.recording = .init(opts.allocator, initial);
} }
@ -537,7 +619,12 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
log.debug("Build options:", .{}); log.debug("Build options:", .{});
inline for (@typeInfo(build_options).@"struct".decls) |decl| { inline for (@typeInfo(build_options).@"struct".decls) |decl| {
log.debug("- {s}: {}", .{decl.name, @field(build_options, decl.name)}); 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 // TODO: Don't hard code icon path, allow changing through options
@ -577,19 +664,10 @@ fn sokolInit(self: *Engine) !void {
const zone = tracy.initZone(@src(), .{ }); const zone = tracy.initZone(@src(), .{ });
defer zone.deinit(); defer zone.deinit();
try self.graphics.init(.{ try self.graphics.setupSokol();
.allocator = self.allocator, self.audio.setupSokol();
.logger = .{ .func = sokolLogCallback },
// TODO:
// .imgui_font = .{
// .ttf_data = @embedFile("../assets/roboto-font/Roboto-Regular.ttf"),
// }
});
try self.audio.init(.{ try self.graphics.refreshFonts();
.allocator = self.allocator,
.logger = .{ .func = sokolLogCallback },
});
} }
fn sokolCleanup(self: *Engine) void { fn sokolCleanup(self: *Engine) void {
@ -606,6 +684,7 @@ fn sokolCleanup(self: *Engine) void {
self.audio.deinit(); self.audio.deinit();
self.graphics.deinit(self.allocator); self.graphics.deinit(self.allocator);
self.game_linking.deinit(self.allocator); self.game_linking.deinit(self.allocator);
self.assets.deinit(self.allocator);
} }
fn sokolFrame(self: *Engine) !void { fn sokolFrame(self: *Engine) !void {
@ -653,27 +732,25 @@ fn sokolFrame(self: *Engine) !void {
sapp.showMouse(!tick_result.cursor_hidden); sapp.showMouse(!tick_result.cursor_hidden);
{ {
self.graphics.beginFrame(); try self.graphics.beginFrame();
defer self.graphics.endFrame(tick_result.clear_color); defer self.graphics.endFrame(tick_result.clear_color);
self.graphics.drawCommands(tick_result.graphics); self.graphics.drawCommands(tick_result.graphics);
if (self.show_debug and build_options.has_imgui) { if (self.show_debug and build_options.has_imgui) {
try self.showDebugWindow(&self.game_state.frame); try self.showDebugWindow();
} }
} }
// TODO: for (tick_result.audio) |command| {
// for (tick_result.audio) |command| { try self.audio.thread_state.commands.push(command);
// try self.audio.mixer.commands.push(command); }
// }
} }
if (self.restart_game) { if (self.restart_game) {
self.restart_game = false; self.restart_game = false;
const initial = GameState.Init{
const initial = GameState.Initial{
.seed = self.seed, .seed = self.seed,
.screen_size = .init(sapp.widthf(), sapp.heightf()) .screen_size = .init(sapp.widthf(), sapp.heightf())
}; };
@ -681,8 +758,8 @@ fn sokolFrame(self: *Engine) !void {
self.game_state.runGameDeinit(self.game_linking.callbacks.deinit); self.game_state.runGameDeinit(self.game_linking.callbacks.deinit);
self.game_state.deinit(); self.game_state.deinit();
self.game_state = .init(self.allocator, initial); self.game_state = .init(self.allocator);
self.game_state.runGameInit(self.game_linking.callbacks.init); self.game_state.runGameInit(self.game_linking.callbacks.init, initial, self.assets.items);
if (self.recording) |recording| { if (self.recording) |recording| {
recording.deinit(); recording.deinit();
@ -691,7 +768,7 @@ fn sokolFrame(self: *Engine) !void {
} }
} }
fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void { fn showDebugWindow(self: *Engine) !void {
if (!imgui.beginWindow(.{ if (!imgui.beginWindow(.{
.name = "Debug", .name = "Debug",
.pos = Vec2.init(20, 20), .pos = Vec2.init(20, 20),
@ -733,8 +810,8 @@ fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void {
self.game_state.runGameDeinit(self.game_linking.callbacks.deinit); self.game_state.runGameDeinit(self.game_linking.callbacks.deinit);
self.game_state.deinit(); self.game_state.deinit();
self.game_state = .init(self.allocator, recording.initial); self.game_state = .init(self.allocator);
self.game_state.runGameInit(self.game_linking.callbacks.init); self.game_state.runGameInit(self.game_linking.callbacks.init, recording.initial, self.assets.items);
self.play_recording = true; self.play_recording = true;
self.recording_tick = 0; self.recording_tick = 0;
@ -749,9 +826,9 @@ fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void {
const time_ms: f64 = @floatFromInt(@divFloor(self.game_state.last_frame_at, std.time.ns_per_ms)); 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("Time: {:.2}", .{ time_ms / 1000 });
imgui.textFmt("Draw commands: {}\n", .{ // imgui.textFmt("Draw commands: {}\n", .{
frame.graphics_commands.items.len, // frame.graphics_commands.items.len,
}); // });
// TODO: // TODO:
// imgui.textFmt("Audio instances: {}/{}\n", .{ // imgui.textFmt("Audio instances: {}/{}\n", .{
// Audio.mixer.instances.items.len, // Audio.mixer.instances.items.len,

View File

@ -6,11 +6,13 @@ const sapp = sokol.app;
const simgui = sokol.imgui; const simgui = sokol.imgui;
const sgl = sokol.gl; const sgl = sokol.gl;
const Math = @import("lib").Math; const Lib = @import("lib");
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);
@ -19,10 +21,6 @@ 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();
@ -33,26 +31,28 @@ 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
const Options = struct { pub const Sprite = Lib.Sprite;
const ImguiFont = struct {
const SokolState = struct {
main_pipeline: sgl.Pipeline,
linear_sampler: sg.Sampler,
nearest_sampler: sg.Sampler,
font_context: fontstash.Context,
};
const ImguiFont = struct {
ttf_data: []const u8, ttf_data: []const u8,
size: f32 = 16 size: f32 = 16
}; };
allocator: std.mem.Allocator, const SokolOptions = struct {
logger: sg.Logger = .{}, logger: sg.Logger,
imgui_font: ?ImguiFont = null 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,
@ -60,16 +60,26 @@ const Texture = struct {
rgba: [*]u8 rgba: [*]u8
}; };
}; };
pub const TextureId = Lib.TextureId;
pub const TextureInfo = Texture.Info;
pub const Sprite = Lib.Sprite; const Font = struct {
fontstash_id: ?fontstash.Font.Id,
data: ?[]const u8,
main_pipeline: sgl.Pipeline, pub fn deinit(self: Font, gpa: std.mem.Allocator) void {
linear_sampler: sg.Sampler, if (self.data) |data| {
nearest_sampler: sg.Sampler, gpa.free(data);
font_context: fontstash.Context, }
}
};
const FontArray = Lib.GenerationalArrayList(u8, u8, Font);
comptime {
assert(@bitSizeOf(FontArray.Id) == @bitSizeOf(Lib.FontId));
}
gpa: std.mem.Allocator,
textures: std.ArrayList(Texture) = .empty, textures: std.ArrayList(Texture) = .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,
@ -77,18 +87,38 @@ 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,
font_id_lookup: std.AutoArrayHashMapUnmanaged(Lib.FontId, fontstash.Font.Id) = .empty, sokol_state: ?SokolState,
sokol_options: SokolOptions,
pub fn init(self: *Graphics, options: Options) !void { const Options = struct {
sg.setup(.{ 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,
.sokol_options = .{
.logger = options.logger, .logger = options.logger,
.imgui_font = options.imgui_font,
}
};
}
pub fn setupSokol(self: *Graphics) !void {
const opts = self.sokol_options;
sg.setup(.{
.logger = opts.logger,
.environment = sglue.environment(), .environment = sglue.environment(),
}); });
sgl.setup(.{ sgl.setup(.{
.logger = .{ .logger = .{
.func = options.logger.func, .func = opts.logger.func,
.user_data = options.logger.user_data .user_data = opts.logger.user_data
} }
}); });
@ -110,12 +140,12 @@ pub fn init(self: *Graphics, options: Options) !void {
}, },
}); });
imgui.setup(options.allocator, .{ imgui.setup(self.gpa, .{
.logger = .{ .logger = .{
.func = options.logger.func, .func = opts.logger.func,
.user_data = options.logger.user_data .user_data = opts.logger.user_data
}, },
.no_default_font = options.imgui_font != null, .no_default_font = opts.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.
@ -123,7 +153,7 @@ pub fn init(self: *Graphics, options: Options) !void {
.disable_set_mouse_cursor = true .disable_set_mouse_cursor = true
}); });
if (options.imgui_font) |imgui_font| { if (opts.imgui_font) |imgui_font| {
imgui.addFont(imgui_font.ttf_data, imgui_font.size); imgui.addFont(imgui_font.ttf_data, imgui_font.size);
} }
@ -149,7 +179,7 @@ pub fn init(self: *Graphics, options: Options) !void {
.height = @intCast(atlas_dim), .height = @intCast(atlas_dim),
}); });
self.* = Graphics{ self.sokol_state = SokolState{
.main_pipeline = main_pipeline, .main_pipeline = main_pipeline,
.linear_sampler = linear_sampler, .linear_sampler = linear_sampler,
.nearest_sampler = nearest_sampler, .nearest_sampler = nearest_sampler,
@ -158,15 +188,26 @@ pub fn init(self: *Graphics, options: Options) !void {
} }
pub fn deinit(self: *Graphics, gpa: std.mem.Allocator) void { pub fn deinit(self: *Graphics, gpa: std.mem.Allocator) void {
self.font_id_lookup.deinit(gpa); if (self.sokol_state) |*sokol_state| {
self.textures.deinit(gpa);
imgui.shutdown(); imgui.shutdown();
self.font_context.deinit(); sokol_state.font_context.deinit();
sgl.shutdown(); sgl.shutdown();
sg.shutdown(); sg.shutdown();
}
var fonts_iter = self.fonts.iterator();
while (fonts_iter.nextItem()) |font| {
font.deinit(self.gpa);
}
self.fonts.deinit(gpa);
self.textures.deinit(gpa);
} }
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);
@ -220,22 +261,24 @@ 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_id = self.font_id_lookup.get(opts.font) orelse { const font = self.fonts.get(.fromInt(@intFromEnum(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;
}; };
self.font_context.setFont(font_id); const fontstash_id = font.fontstash_id orelse return;
self.font_context.setSize(opts.size * font_resolution_scale.y); const font_context = sokol_state.font_context;
self.font_context.setAlign(.{ .x = .left, .y = .top }); font_context.setFont(fontstash_id);
self.font_context.setSpacing(0); font_context.setSize(opts.size * font_resolution_scale.y);
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);
self.font_context.setColor(color); font_context.setColor(color);
self.font_context.drawText( 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
@ -250,7 +293,21 @@ pub fn drawCommands(self: *Graphics, commands: []const Command) void {
} }
} }
pub fn beginFrame(self: *Graphics) void { pub fn refreshFonts(self: *Graphics) !void {
const sokol_state = self.sokol_state orelse return;
var fonts_iter = self.fonts.iterator();
while (fonts_iter.nextItem()) |font| {
if (font.fontstash_id == null and font.data != null) {
font.fontstash_id = try sokol_state.font_context.addFont("", font.data.?);
}
}
}
pub fn beginFrame(self: *Graphics) !void {
assert(self.sokol_state != null);
const sokol_state = self.sokol_state.?;
imgui.newFrame(.{ imgui.newFrame(.{
.width = sapp.width(), .width = sapp.width(),
.height = sapp.height(), .height = sapp.height(),
@ -264,14 +321,17 @@ 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()));
self.font_context.clearState(); sokol_state.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(self.main_pipeline); sgl.loadPipeline(sokol_state.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 sokol_state = self.sokol_state.?;
var pass_action: sg.PassAction = .{}; var pass_action: sg.PassAction = .{};
pass_action.colors[0] = sg.ColorAttachmentAction{ pass_action.colors[0] = sg.ColorAttachmentAction{
@ -284,7 +344,7 @@ pub fn endFrame(self: *Graphics, clear_color: Vec4) void {
} }
}; };
self.font_context.flush(); sokol_state.font_context.flush();
{ {
sg.beginPass(.{ sg.beginPass(.{
@ -318,13 +378,16 @@ const Vertex = struct {
uv: Vec2 uv: Vec2
}; };
fn drawQuad(self: *Graphics, quad: [4]Vertex, color: Vec4, texture_id: TextureId) void { fn drawQuad(self: *Graphics, quad: [4]Vertex, color: Vec4, texture_id: Lib.TextureId) void {
assert(self.sokol_state != null);
const sokol_state = self.sokol_state.?;
sgl.enableTexture(); sgl.enableTexture();
defer sgl.disableTexture(); defer sgl.disableTexture();
const view = self.textures.items[@intFromEnum(texture_id)].view; const view = self.textures.items[@intFromEnum(texture_id)].view;
// TODO: Make sampler configurable // TODO: Make sampler configurable
sgl.texture(view, self.nearest_sampler); sgl.texture(view, sokol_state.nearest_sampler);
sgl.beginQuads(); sgl.beginQuads();
defer sgl.end(); defer sgl.end();
@ -402,10 +465,6 @@ fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
); );
} }
fn addFont(self: *Graphics, name: [*c]const u8, data: []const u8) !Font.Id {
return try self.font_context.addFont(name, data);
}
fn makeView(image: sg.Image) !sg.View { fn makeView(image: sg.Image) !sg.View {
const image_view = sg.makeView(.{ const image_view = sg.makeView(.{
.texture = .{ .image = image } .texture = .{ .image = image }
@ -448,7 +507,7 @@ fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image {
return image; return image;
} }
pub fn addTexture(self: *Graphics, gpa: std.mem.Allocator, mipmaps: []const Texture.Data) !TextureId { pub fn loadTexture(self: *Graphics, gpa: std.mem.Allocator, mipmaps: []const Texture.Data) !Lib.TextureId {
const image = try makeImageWithMipMaps(mipmaps); const image = try makeImageWithMipMaps(mipmaps);
errdefer sg.deallocImage(image); errdefer sg.deallocImage(image);
@ -469,7 +528,26 @@ pub fn addTexture(self: *Graphics, gpa: std.mem.Allocator, mipmaps: []const Text
return @enumFromInt(index); return @enumFromInt(index);
} }
pub fn getTextureInfo(self: *Graphics, id: TextureId) TextureInfo { pub fn addFont(self: *Graphics) !Lib.FontId {
const texture = self.textures.items[@intFromEnum(id)]; const font_id = try self.fonts.insert(self.gpa, .{
return texture.info; .fontstash_id = null,
.data = null
});
return @enumFromInt(font_id.asInt());
}
pub fn removeFont(self: *Graphics, id: Lib.FontId) void {
_ = self.fonts.remove(.fromInt(@intFromEnum(id)));
}
pub fn setFontData(self: *Graphics, id: Lib.FontId, data: []const u8) !void {
if (self.fonts.get(.fromInt(@intFromEnum(id)))) |font| {
const new_data = try self.gpa.dupe(u8, data);
if (font.data) |old_data| {
self.gpa.free(old_data);
}
font.data = new_data;
font.fontstash_id = null;
}
} }

View File

@ -13,43 +13,49 @@ const FontName = enum {
regular, regular,
bold, bold,
italic, italic,
};
const EnumArray = std.EnumArray(FontName, FontId); const AssetId = enum {
font_regular,
font_bold,
font_italic,
sound_wood,
}; };
const State = struct { const State = struct {
gpa: Allocator, gpa: Allocator,
player: Vec2, player: Vec2,
// font_id: FontName.EnumArray,
// wood01: Audio.Data.Id,
}; };
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
}; };
return state; return state;
} }
fn getFont(assets: Engine.Asset.Registry, font_name: FontName) FontId {
const asset_id = switch (font_name) {
.regular => AssetId.font_regular,
.bold => AssetId.font_bold,
.italic => AssetId.font_italic,
};
return assets.getFont(@intFromEnum(asset_id));
}
pub fn loadAssets(assets: *Engine.Asset.Loader) callconv(.c) void {
assets.addFont(@intFromEnum(AssetId.font_regular), "roboto-font/Roboto-Regular.ttf");
assets.addFont(@intFromEnum(AssetId.font_bold), "roboto-font/Roboto-Bold.ttf");
assets.addFont(@intFromEnum(AssetId.font_italic), "roboto-font/Roboto-Italic.ttf");
assets.addSound(@intFromEnum(AssetId.sound_wood), "wood01.ogg");
}
pub fn deinit(state_ptr: *anyopaque) callconv(.c) void { pub fn deinit(state_ptr: *anyopaque) callconv(.c) void {
const state: *State = @alignCast(@ptrCast(state_ptr)); const state: *State = @alignCast(@ptrCast(state_ptr));
const gpa = state.gpa; const gpa = state.gpa;
@ -80,10 +86,10 @@ pub fn tick(state_ptr: *anyopaque, frame: *Engine.Frame) callconv(.c) void {
dir = dir.normalized(); dir = dir.normalized();
if (dir.x != 0 or dir.y != 0) { if (dir.x != 0 or dir.y != 0) {
// frame.playAudio(.{ frame.playAudio(.{
// .id = g_state.wood01, .id = frame.assets.getSound(@intFromEnum(AssetId.sound_wood)),
// .volume = 0.1 .volume = 0.1
// }); });
} }
state.player = state.player.add(dir.multiplyScalar(50 * dt)); state.player = state.player.add(dir.multiplyScalar(50 * dt));
@ -104,11 +110,21 @@ pub fn tick(state_ptr: *anyopaque, frame: *Engine.Frame) callconv(.c) void {
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);
} }
// const regular_font = g_state.font_id.get(.regular); if (frame.mouse_position) |mouse_position| {
// frame.drawText(g_state.player, "Player", .{ const mouse_size = Vec2.init(10, 10);
// .font = regular_font, frame.drawRectangle(.{
// .size = 10 .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
});
} }
pub fn debug(state_ptr: *anyopaque, imgui: *Engine.ImGui) callconv(.c) void { pub fn debug(state_ptr: *anyopaque, imgui: *Engine.ImGui) callconv(.c) void {
@ -118,5 +134,5 @@ pub fn debug(state_ptr: *anyopaque, imgui: *Engine.ImGui) callconv(.c) void {
} }
comptime { comptime {
Engine.exportCallbacksIfNeeded(init, tick, deinit, debug); Engine.exportCallbacksIfNeeded(init, tick, deinit, debug, loadAssets);
} }