basic asset loading
This commit is contained in:
parent
f80859877c
commit
6b48af7ea9
@ -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/"),
|
||||
});
|
||||
|
||||
{
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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" {
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
70
src/game.zig
70
src/game.zig
@ -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);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user