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_png_icon = b.path("src/assets/icon.png"),
.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_tracy: ?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 {
const b = opts.dep_engine.builder;
const isWasm = opts.target.result.cpu.arch.isWasm();
const isDebug = (opts.optimize == .Debug);
var has_tracy = false;
var hot_reload = false;
var load_assets_at_runtime = false;
if (!isWasm) {
hot_reload = opts.hot_reload orelse (opts.optimize == .Debug);
has_tracy = opts.has_tracy orelse (opts.optimize == .Debug);
hot_reload = opts.hot_reload orelse isDebug;
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;
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, "hot_reload", hot_reload);
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(.{
.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 Int = @Type(.{
.int = .{
.bits = @bitSizeOf(Index) + @bitSizeOf(Generation),
.bits = @bitSizeOf(Id),
.signedness = .unsigned
}
});
@ -62,6 +62,10 @@ pub fn GenerationalArrayList(
pub fn asInt(self: Id) Int {
return @bitCast(self);
}
pub fn fromInt(self: Int) Id {
return @bitCast(self);
}
};
pub const ItemWithId = struct {
@ -227,7 +231,6 @@ pub fn GenerationalArrayList(
}
self.markUnused(unused_index, false);
self.count += 1;
const id = Id{
.index = @intCast(unused_index),
@ -324,7 +327,6 @@ pub fn GenerationalArrayList(
.generations = generations.ptr,
.unused = unused.ptr,
.len = self.len,
.count = self.count,
.capacity = self.capacity
};
}
@ -332,7 +334,6 @@ pub fn GenerationalArrayList(
pub fn getMetadata(self: *Self) Metadata {
return Metadata{
.len = self.len,
.count = self.count
};
}
@ -362,7 +363,6 @@ pub fn GenerationalArrayList(
try reader.readSliceAll(self.unused[0..divCeilGeneration(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(id2).*, array_list2.getAssumeExists(id2).*);
try expectEqual(array_list1.getAssumeExists(id3).*, array_list2.getAssumeExists(id3).*);
try expectEqual(array_list1.count, array_list2.count);
}
test "clear retaining capacity" {

View File

@ -142,9 +142,21 @@ pub const MouseButton = enum {
middle,
};
pub const TextureId = enum(u16) { _ };
pub const FontId = enum(u16) { _ };
pub const SoundId = enum(u16) { _ };
pub const TextureId = 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 {
texture: TextureId,
@ -287,7 +299,12 @@ pub const Frame = struct {
screen_size: Vec2,
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{
.arena = std.heap.ArenaAllocator.init(gpa),
.time_ns = 0,
@ -302,6 +319,7 @@ pub const Frame = struct {
.audio_commands = .empty,
.hide_cursor = false,
.screen_size = screen_size,
.assets = .empty
};
}
@ -468,7 +486,124 @@ pub const Frame = struct {
pub const Init = struct {
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 {
@ -478,10 +613,12 @@ pub const Callbacks = struct {
pub const TickFn = *const fn(state: State, frame: *Frame) 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 LoadAssetsFn = *const fn(assets: *Asset.Loader) callconv(.c) void;
init: InitFn,
tick: TickFn,
deinit: DeinitFn,
loadAssets: if (build_options.load_assets_at_runtime) LoadAssetsFn else void,
debug: if (build_options.has_imgui) DebugFn else void
};
@ -489,7 +626,8 @@ pub fn exportCallbacksIfNeeded(
comptime init: Callbacks.InitFn,
comptime tick: Callbacks.TickFn,
comptime deinit: Callbacks.DeinitFn,
comptime debug: Callbacks.DebugFn
comptime debug: Callbacks.DebugFn,
comptime assets: Callbacks.LoadAssetsFn,
) void {
const builtin = @import("builtin");
if (builtin.output_mode != .Lib) {
@ -499,6 +637,9 @@ pub fn exportCallbacksIfNeeded(
@export(init, .{ .name = "init", .linkage = .strong });
@export(tick, .{ .name = "tick", .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) {
@export(debug, .{ .name = "debug", .linkage = .strong });
}

View File

@ -31,21 +31,37 @@ const Sound = union(enum) {
return self.sample_rate;
}
pub fn streamChannel(self: Raw, opts: StreamOptions) []f32 {
assert(opts.sample_rate == self.sample_rate); // TODO:
assert(opts.channel_index < self.channels.len); // TODO:
fn streamChannel(self: Raw, buffer: []f32, cursor: u32, channel_index: usize) usize {
assert(channel_index < self.channels.len); // TODO:
const channel = self.channels[opts.channel_index];
const channel = self.channels[channel_index];
var memcpy_len: usize = 0;
if (opts.cursor + opts.buffer.len <= self.sample_count) {
memcpy_len = opts.buffer.len;
} else if (opts.cursor < opts.sample_count) {
memcpy_len = opts.sample_count - opts.cursor;
if (cursor + buffer.len <= self.sample_count) {
memcpy_len = buffer.len;
} else if (cursor < self.sample_count) {
memcpy_len = self.sample_count - cursor;
}
@memcpy(opts.buffer[0..memcpy_len], channel[opts.cursor..][0..memcpy_len]);
return opts.buffer[0..memcpy_len];
@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
}
};
@ -53,9 +69,10 @@ const Sound = union(enum) {
alloc_buffer: []u8,
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 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
// 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;
}
pub fn streamChannel(self: Vorbis, opts: StreamOptions) []f32 {
_ = self; // autofix
_ = opts; // autofix
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 {
buffer: []f32,
cursor: u32,
channel_index: u32,
sample_rate: u32
channels: []const [*]f32,
channel_size: usize,
cursor: 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) {
.nil => opts.buffer[0..0],
.raw => |raw| raw.streamChannel(opts),
.vorbis => |vorbis| vorbis.streamChannel(opts),
.nil => 0,
.raw => |raw| raw.streamChannels(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,
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: SoundsList,
sounds: SoundList,
thread_state: ThreadState,
@ -234,8 +285,9 @@ const Options = struct {
max_commands: u32 = 64,
};
pub fn init(self: *AudioSystem, opts: Options) !void {
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);
@ -246,15 +298,28 @@ pub fn init(self: *AudioSystem, opts: Options) !void {
);
errdefer thread_state.deinit(gpa);
self.* = AudioSystem{
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,
@ -262,6 +327,7 @@ pub fn init(self: *AudioSystem, opts: Options) !void {
.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());
@ -277,7 +343,9 @@ pub fn init(self: *AudioSystem, opts: Options) !void {
pub fn deinit(self: *AudioSystem) void {
self.running.store(false, .seq_cst);
saudio.shutdown();
if (self.sokol_setup) {
saudio.shutdown();
}
self.thread_state.deinit(self.gpa);
self.gpa.free(self.temp_vorbis_alloc_buffer);
@ -285,7 +353,24 @@ pub fn deinit(self: *AudioSystem) void {
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 {
stream,
decode_once,
@ -303,66 +388,64 @@ pub const LoadOptions = struct {
playback_style: ?PlaybackStyle = null,
};
pub fn load(self: *AudioSystem, opts: LoadOptions) !Lib.SoundId {
_ = self; // autofix
_ = opts; // autofix
@panic("TODO");
pub fn setSoundData(self: *AudioSystem, id: Lib.SoundId, opts: DataOptions) !void {
const sound = self.sounds.get(.fromInt(@intFromEnum(id))) orelse return error.SoundNotFound;
// const id = self.sounds.items.len;
// try self.sounds.ensureUnusedCapacity(self.gpa, 1);
//
// // 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);
}
assert(opts.format == .vorbis);
const vorbis = try Sound.Vorbis.init(self.gpa, opts.data, self.temp_vorbis_alloc_buffer);
pub fn unload(self: *AudioSystem, id: Lib.SoundId) void {
_ = self; // autofix
_ = id; // autofix
@panic("TODO");
const PlaybackStyle = DataOptions.PlaybackStyle;
var playback_style: PlaybackStyle = undefined;
if (opts.playback_style == null) {
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 {
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)) {
return;
}
@ -370,6 +453,9 @@ fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: i32, num_channel
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) {
@ -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);
@memset(buffer[0..@intCast(num_frames * num_channels)], 0);
_ = sample_rate; // autofix
for (thread_state.instances.items) |*instance| {
_ = instance; // autofix
// const sound = self.sounds
// 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);
const sound = self.sounds.get(.fromInt(@intFromEnum(instance.sound_id))) orelse 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];
// 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;
// }
// }
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, 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,
.deinit = game.deinit,
.tick = game.tick,
.loadAssets = if (build_options.load_assets_at_runtime) game.loadAssets 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 {
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 {},
.init = try ensureLookup(lib, GameCallbacks.InitFn, "init"),
.tick = try ensureLookup(lib, GameCallbacks.TickFn, "tick"),
.deinit = try ensureLookup(lib, GameCallbacks.DeinitFn, "deinit"),
.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;
// 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) {
@ -250,14 +258,13 @@ const GameState = struct {
gpa: std.mem.Allocator,
state: ?GameCallbacks.State,
frame: ?Lib.Frame,
mouse_position: ?Vec2,
frame: Lib.Frame,
started_at: std.time.Instant,
last_frame_at: Lib.Nanoseconds,
seed: u64,
const Initial = struct {
const Init = struct {
screen_size: Vec2,
seed: u64
};
@ -274,24 +281,31 @@ const GameState = struct {
cursor_hidden: bool
};
pub fn init(gpa: std.mem.Allocator, initial: Initial) GameState {
pub fn init(gpa: std.mem.Allocator) GameState {
return GameState{
.gpa = gpa,
.mouse_position = null,
.frame = .init(gpa, initial.screen_size),
.started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"),
.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 {
const frame = &self.frame;
assert(self.frame != null);
const frame = &self.frame.?;
switch (event) {
.key_pressed => |opts| {
if (!opts.repeat) {
frame.keyboard.press(opts.code, self.frame.time_ns);
frame.keyboard.press(opts.code, frame.time_ns);
}
},
.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{
.gpa = self.gpa,
.seed = self.seed,
.seed = initial.seed,
.assets = .{
.items = assets
}
};
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 {
@ -343,13 +366,9 @@ const GameState = struct {
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 {
const frame = &self.frame;
assert(self.frame != null);
const frame = &self.frame.?;
_ = frame.arena.reset(.retain_capacity);
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| {
screen_scaler.pop(frame, frame.clear_color);
@ -415,10 +435,10 @@ const GameState = struct {
frame.mouse_button.released = .initEmpty();
return TickResult{
.graphics = self.frame.graphics_commands.items,
.audio = self.frame.audio_commands.items,
.clear_color = self.frame.clear_color,
.cursor_hidden = self.frame.hide_cursor
.graphics = frame.graphics_commands.items,
.audio = frame.audio_commands.items,
.clear_color = frame.clear_color,
.cursor_hidden = frame.hide_cursor
};
}
@ -426,19 +446,15 @@ const GameState = struct {
assert(self.state != null);
debug_fn(self.state.?, imgui_ctx);
}
pub fn deinit(self: *GameState) void {
self.frame.deinit();
}
};
const Recording = struct {
arena: std.heap.ArenaAllocator,
initial: GameState.Initial,
initial: GameState.Init,
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{
.arena = .init(gpa),
.ticks = .empty,
@ -466,6 +482,7 @@ allocator: std.mem.Allocator,
graphics: Graphics,
audio: Audio,
assets: std.ArrayList(Lib.Asset),
game_linking: GameLinking,
game_state: GameState,
@ -497,18 +514,26 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
game_linking = try .initStatic();
}
const initial = GameState.Initial{
const initial = GameState.Init{
.seed = @bitCast(std.time.milliTimestamp()),
.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{
.allocator = opts.allocator,
.graphics = undefined,
.audio = undefined,
.game_state = game_state,
.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 },
}),
.game_state = .init(opts.allocator),
.game_linking = game_linking,
.show_debug = false,
.restart_game = false,
@ -518,8 +543,65 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
.play_recording = false,
.recording_tick = 0,
.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) {
self.recording = .init(opts.allocator, initial);
}
@ -537,7 +619,12 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
log.debug("Build options:", .{});
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
@ -577,19 +664,10 @@ 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 self.graphics.setupSokol();
self.audio.setupSokol();
try self.audio.init(.{
.allocator = self.allocator,
.logger = .{ .func = sokolLogCallback },
});
try self.graphics.refreshFonts();
}
fn sokolCleanup(self: *Engine) void {
@ -606,6 +684,7 @@ fn sokolCleanup(self: *Engine) void {
self.audio.deinit();
self.graphics.deinit(self.allocator);
self.game_linking.deinit(self.allocator);
self.assets.deinit(self.allocator);
}
fn sokolFrame(self: *Engine) !void {
@ -653,27 +732,25 @@ fn sokolFrame(self: *Engine) !void {
sapp.showMouse(!tick_result.cursor_hidden);
{
self.graphics.beginFrame();
try self.graphics.beginFrame();
defer self.graphics.endFrame(tick_result.clear_color);
self.graphics.drawCommands(tick_result.graphics);
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| {
// try self.audio.mixer.commands.push(command);
// }
for (tick_result.audio) |command| {
try self.audio.thread_state.commands.push(command);
}
}
if (self.restart_game) {
self.restart_game = false;
const initial = GameState.Initial{
const initial = GameState.Init{
.seed = self.seed,
.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.deinit();
self.game_state = .init(self.allocator, initial);
self.game_state.runGameInit(self.game_linking.callbacks.init);
self.game_state = .init(self.allocator);
self.game_state.runGameInit(self.game_linking.callbacks.init, initial, self.assets.items);
if (self.recording) |recording| {
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(.{
.name = "Debug",
.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.deinit();
self.game_state = .init(self.allocator, recording.initial);
self.game_state.runGameInit(self.game_linking.callbacks.init);
self.game_state = .init(self.allocator);
self.game_state.runGameInit(self.game_linking.callbacks.init, recording.initial, self.assets.items);
self.play_recording = true;
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));
imgui.textFmt("Time: {:.2}", .{ time_ms / 1000 });
imgui.textFmt("Draw commands: {}\n", .{
frame.graphics_commands.items.len,
});
// imgui.textFmt("Draw commands: {}\n", .{
// frame.graphics_commands.items.len,
// });
// TODO:
// imgui.textFmt("Audio instances: {}/{}\n", .{
// Audio.mixer.instances.items.len,

View File

@ -6,11 +6,13 @@ const sapp = sokol.app;
const simgui = sokol.imgui;
const sgl = sokol.gl;
const Math = @import("lib").Math;
const Lib = @import("lib");
const Math = Lib.Math;
const Vec2 = Math.Vec2;
const Vec4 = Math.Vec4;
const rgb = Math.rgb;
const Rect = Math.Rect;
const Command = Lib.GraphicsCommand;
const std = @import("std");
const log = std.log.scoped(.graphics);
@ -19,10 +21,6 @@ const assert = std.debug.assert;
const imgui = @import("imgui.zig");
const tracy = @import("tracy");
const fontstash = @import("./fontstash/root.zig");
pub const Font = fontstash.Font;
const Lib = @import("lib");
const Command = Lib.GraphicsCommand;
const Graphics = @This();
@ -33,26 +31,28 @@ const Graphics = @This();
// * https://github.com/libsdl-org/SDL/issues/11618
// * https://github.com/nimgl/nimgl/issues/59
const Options = struct {
const ImguiFont = struct {
ttf_data: []const u8,
size: f32 = 16
};
pub const Sprite = Lib.Sprite;
allocator: std.mem.Allocator,
logger: sg.Logger = .{},
imgui_font: ?ImguiFont = null
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,
size: f32 = 16
};
const SokolOptions = struct {
logger: sg.Logger,
imgui_font: ?ImguiFont,
};
const Texture = struct {
image: sg.Image,
view: sg.View,
info: Info,
const Info = struct {
width: u32,
height: u32,
};
const Data = struct {
width: u32,
@ -60,16 +60,26 @@ const Texture = struct {
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,
linear_sampler: sg.Sampler,
nearest_sampler: sg.Sampler,
font_context: fontstash.Context,
pub fn deinit(self: Font, gpa: std.mem.Allocator) void {
if (self.data) |data| {
gpa.free(data);
}
}
};
const FontArray = Lib.GenerationalArrayList(u8, u8, Font);
comptime {
assert(@bitSizeOf(FontArray.Id) == @bitSizeOf(Lib.FontId));
}
gpa: std.mem.Allocator,
textures: std.ArrayList(Texture) = .empty,
fonts: FontArray = .empty,
scale_stack_buffer: [32]Vec2 = undefined,
scale_stack: std.ArrayList(Vec2) = .empty,
@ -77,18 +87,38 @@ scale_stack: std.ArrayList(Vec2) = .empty,
scissor_stack_buffer: [32]Rect = undefined,
scissor_stack: std.ArrayList(Rect) = .empty,
font_id_lookup: std.AutoArrayHashMapUnmanaged(Lib.FontId, fontstash.Font.Id) = .empty,
sokol_state: ?SokolState,
sokol_options: SokolOptions,
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,
.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(.{
.logger = options.logger,
.logger = opts.logger,
.environment = sglue.environment(),
});
sgl.setup(.{
.logger = .{
.func = options.logger.func,
.user_data = options.logger.user_data
.func = opts.logger.func,
.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 = .{
.func = options.logger.func,
.user_data = options.logger.user_data
.func = opts.logger.func,
.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
// 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
});
if (options.imgui_font) |imgui_font| {
if (opts.imgui_font) |imgui_font| {
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),
});
self.* = Graphics{
self.sokol_state = SokolState{
.main_pipeline = main_pipeline,
.linear_sampler = linear_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 {
self.font_id_lookup.deinit(gpa);
if (self.sokol_state) |*sokol_state| {
imgui.shutdown();
sokol_state.font_context.deinit();
sgl.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);
imgui.shutdown();
self.font_context.deinit();
sgl.shutdown();
sg.shutdown();
}
pub fn drawCommand(self: *Graphics, command: Command) void {
assert(self.sokol_state != null);
const sokol_state = self.sokol_state.?;
switch(command) {
.push_transformation => |opts| {
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);
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", .{});
return;
};
self.font_context.setFont(font_id);
self.font_context.setSize(opts.size * font_resolution_scale.y);
self.font_context.setAlign(.{ .x = .left, .y = .top });
self.font_context.setSpacing(0);
const fontstash_id = font.fontstash_id orelse return;
const font_context = sokol_state.font_context;
font_context.setFont(fontstash_id);
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 g: u8 = @intFromFloat(opts.color.y * 255);
const b: u8 = @intFromFloat(opts.color.z * 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);
self.font_context.setColor(color);
self.font_context.drawText(
font_context.setColor(color);
font_context.drawText(
opts.pos.x * font_resolution_scale.x,
opts.pos.y * font_resolution_scale.y,
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(.{
.width = sapp.width(),
.height = sapp.height(),
@ -264,14 +321,17 @@ pub fn beginFrame(self: *Graphics) void {
self.scissor_stack = .initBuffer(&self.scissor_stack_buffer);
self.scissor_stack.appendAssumeCapacity(.init(0, 0, sapp.widthf(), sapp.heightf()));
self.font_context.clearState();
sokol_state.font_context.clearState();
sgl.defaults();
sgl.matrixModeProjection();
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 {
assert(self.sokol_state != null);
const sokol_state = self.sokol_state.?;
var pass_action: sg.PassAction = .{};
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(.{
@ -318,13 +378,16 @@ const Vertex = struct {
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();
defer sgl.disableTexture();
const view = self.textures.items[@intFromEnum(texture_id)].view;
// TODO: Make sampler configurable
sgl.texture(view, self.nearest_sampler);
sgl.texture(view, sokol_state.nearest_sampler);
sgl.beginQuads();
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 {
const image_view = sg.makeView(.{
.texture = .{ .image = image }
@ -448,7 +507,7 @@ fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.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);
errdefer sg.deallocImage(image);
@ -469,7 +528,26 @@ pub fn addTexture(self: *Graphics, gpa: std.mem.Allocator, mipmaps: []const Text
return @enumFromInt(index);
}
pub fn getTextureInfo(self: *Graphics, id: TextureId) TextureInfo {
const texture = self.textures.items[@intFromEnum(id)];
return texture.info;
pub fn addFont(self: *Graphics) !Lib.FontId {
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.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,
bold,
italic,
};
const EnumArray = std.EnumArray(FontName, FontId);
const AssetId = enum {
font_regular,
font_bold,
font_italic,
sound_wood,
};
const State = struct {
gpa: Allocator,
player: Vec2,
// font_id: FontName.EnumArray,
// wood01: Audio.Data.Id,
};
pub fn init(opts: *const Engine.Init) callconv(.c) *anyopaque {
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");
state.* = State{
.gpa = gpa,
.player = .init(50, 50),
// .font_id = font_id_array,
// .wood01 = wood01
};
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 {
const state: *State = @alignCast(@ptrCast(state_ptr));
const gpa = state.gpa;
@ -80,10 +86,10 @@ pub fn tick(state_ptr: *anyopaque, frame: *Engine.Frame) callconv(.c) void {
dir = dir.normalized();
if (dir.x != 0 or dir.y != 0) {
// frame.playAudio(.{
// .id = g_state.wood01,
// .volume = 0.1
// });
frame.playAudio(.{
.id = frame.assets.getSound(@intFromEnum(AssetId.sound_wood)),
.volume = 0.1
});
}
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);
}
// const regular_font = g_state.font_id.get(.regular);
// frame.drawText(g_state.player, "Player", .{
// .font = regular_font,
// .size = 10
// });
if (frame.mouse_position) |mouse_position| {
const mouse_size = Vec2.init(10, 10);
frame.drawRectangle(.{
.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 {
@ -118,5 +134,5 @@ pub fn debug(state_ptr: *anyopaque, imgui: *Engine.ImGui) callconv(.c) void {
}
comptime {
Engine.exportCallbacksIfNeeded(init, tick, deinit, debug);
Engine.exportCallbacksIfNeeded(init, tick, deinit, debug, loadAssets);
}