diff --git a/src/engine/main.zig b/src/engine/main.zig index 3202d11..d120a7b 100644 --- a/src/engine/main.zig +++ b/src/engine/main.zig @@ -6,16 +6,16 @@ const sys = @import("./sys.zig"); /// Starts the the game engine. /// pub fn main() anyerror!void { - return nosuspend await async sys.runGraphics(anyerror, run); + return nosuspend await async sys.display(anyerror, run); } -fn run(app: *sys.AppContext, graphics: *sys.GraphicsContext) anyerror!void { +fn run(app: *sys.App, graphics: *sys.Graphics) anyerror!void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); { - var file_access = try app.data().open(try sys.Path.joined(&.{"ona.lua"}), .readonly); + var file_access = try app.data.open(try sys.Path.joined(&.{"ona.lua"}), .readonly); defer file_access.close(); @@ -27,7 +27,7 @@ fn run(app: *sys.AppContext, graphics: *sys.GraphicsContext) anyerror!void { if ((try file_access.read(buffer)) != file_size) return error.ScriptLoadFailure; - sys.Log.debug.write(buffer); + app.log(.debug, buffer); } while (graphics.poll()) |_| { diff --git a/src/engine/sys.zig b/src/engine/sys.zig index 0c628e2..81cd0cc 100644 --- a/src/engine/sys.zig +++ b/src/engine/sys.zig @@ -1,5 +1,3 @@ -const Archive = @import("./sys/Archive.zig"); - const ext = @cImport({ @cInclude("SDL2/SDL.h"); }); @@ -9,222 +7,43 @@ const ona = @import("ona"); const std = @import("std"); /// -/// A thread-safe platform abstraction over multiplexing system I/O processing and event handling. +/// Thread-safe platform abstraction over multiplexing system I/O processing and event handling. /// -pub const AppContext = opaque { - /// - /// Linked list of asynchronous messages chained together to be processed by the work processor. - /// - const Message = struct { - next: ?*Message = null, - - kind: union(enum) { - quit, - - task: struct { - data: *anyopaque, - action: fn (*anyopaque) void, - frame: anyframe, - }, - }, - }; +pub const App = struct { + message_chain: ?*Message = null, + message_semaphore: *ext.SDL_sem, + message_mutex: *ext.SDL_mutex, + data: FileSystem, + user: FileSystem, /// - /// Internal state of the event loop hidden from the API consumer. + /// Enqueues `message` to the message chain in `app`. /// - const Implementation = struct { - user_path_prefix: [*]u8, - data_file_system: FileSystem, - user_file_system: FileSystem, - message_semaphore: *ext.SDL_sem, - message_mutex: *ext.SDL_mutex, - message_thread: ?*ext.SDL_Thread, - messages: ?*Message = null, + fn enqueue(app: *App, message: *Message) void { + { + // TODO: Error check these. + _ = ext.SDL_LockMutex(app.message_mutex); - /// - /// [StartError.OutOfSemaphores] indicates that the process has no more semaphores available - /// to it for use, meaning an [Implementation] may not be initialized at this time. - /// - /// [StartError.OutOfMutexes] indicates that the process has no more mutexes available to it - /// for use, meaning an [Implementation] may not be initialized at this time. - /// - /// [StartError.OutOfMemory] indicates that the process has no more memory available to it - /// for use, meaning an [Implementation] may not be initialized at this time. - /// - const InitError = error { - OutOfSemaphores, - OutOfMutexes, - OutOfMemory, - }; + defer _ = ext.SDL_UnlockMutex(app.message_mutex); - /// - /// [StartError.OutOfThreads] indicates that the process has no more threads available to it - /// to use, meaning that no asynchronous work may be started on an [Implementation] at this - /// time. - /// - /// [StartError.AlreadyStarted] is occurs when a request to start work processing happens on - /// an [Implementation] that is already processing work. - /// - const StartError = error { - OutOfThreads, - AlreadyStarted, - }; - - /// - /// Casts `app_context` to a [Implementation] reference. - /// - /// *Note* that if `app_context` does not have the same alignment as [Implementation], safety- - /// checked undefined behavior will occur. - /// - fn cast(app_context: *AppContext) *Implementation { - return @ptrCast(*Implementation, @alignCast(@alignOf(Implementation), app_context)); - } - - /// - /// Deinitializes the `implementation`, requesting any running asynchronous workload - /// processes quit and waits for them to do so before freeing any resources. - /// - fn deinit(implementation: *Implementation) void { - var message = Message{.kind = .quit}; - - implementation.enqueue(&message); - - { - var status = @as(c_int, 0); - - // SDL2 defines waiting on a null thread reference as a no-op. See - // https://wiki.libsdl.org/SDL_WaitThread for more information - ext.SDL_WaitThread(implementation.message_thread, &status); - - if (status != 0) { - // TODO: Error check this. - } - } - - implementation.data_file_system.archive.index_cache.deinit(); - ext.SDL_free(implementation.user_path_prefix); - ext.SDL_DestroyMutex(implementation.message_mutex); - ext.SDL_DestroySemaphore(implementation.message_semaphore); - } - - /// - /// Enqueues `message` to the message processor of `implementation` to be processed at a - /// later, non-deterministic point in time. - /// - fn enqueue(implementation: *Implementation, message: *Message) void { - { - // TODO: Error check these. - _ = ext.SDL_LockMutex(implementation.message_mutex); - - defer _ = ext.SDL_UnlockMutex(implementation.message_mutex); - - if (implementation.messages) |messages| { - messages.next = message; - } else { - implementation.messages = message; - } - } - - // TODO: Error check this. - _ = ext.SDL_SemPost(implementation.message_semaphore); - } - - /// - /// Initializes a new [Implemenation] with `data_archive_file_access` as the data archive to - /// read from and `user_path_prefix` as the native writable user data directory. - /// - /// Returns the created [Implementation] value on success or [InitError] on failure. - /// - fn init(allocator: std.mem.Allocator, - data_archive_file_access: ona.io.FileAccess) InitError!Implementation { - - const user_path_prefix = ext.SDL_GetPrefPath("ona", "ona") orelse - return error.OutOfMemory; - - return Implementation{ - .user_file_system = .{.native = - user_path_prefix[0 .. std.mem.len(user_path_prefix)]}, - - .message_semaphore = ext.SDL_CreateSemaphore(0) orelse return error.OutOfSemaphores, - .message_mutex = ext.SDL_CreateMutex() orelse return error.OutOfMutexes, - .user_path_prefix = user_path_prefix, - - .data_file_system = .{.archive = .{ - .index_cache = try Archive.IndexCache.init(allocator), - .file_access = data_archive_file_access, - }}, - - .message_thread = null, - }; - } - - /// - /// [FileSystemMessage] processing function used by a dedicated worker thread, where `data` - /// is a type-erased reference to a [EventLoop]. - /// - /// The processor returns `0` if it exited normally or any other value if an erroneous exit - /// occured. - /// - fn processTasks(userdata: ?*anyopaque) callconv(.C) c_int { - const implementation = Implementation.cast( - @ptrCast(*AppContext, userdata orelse unreachable)); - - while (true) { - // TODO: Error check these. - _ = ext.SDL_SemWait(implementation.message_semaphore); - _ = ext.SDL_LockMutex(implementation.message_mutex); - - defer _ = ext.SDL_UnlockMutex(implementation.message_mutex); - - while (implementation.messages) |messages| { - switch (messages.kind) { - .quit => { - return 0; - }, - - .task => |task| { - task.action(task.data); - - resume task.frame; - }, - } - - implementation.messages = messages.next; - } + if (app.message_chain) |message_chain| { + message_chain.next = message; + } else { + app.message_chain = message; } } - /// - /// Attempts to start the asynchronous worker thread of `implementation` if it hasn't been - /// already. - /// - /// [StartError] is returned on failure. - /// - fn start(implementation: *Implementation) StartError!void { - if (implementation.message_thread != null) return error.AlreadyStarted; - - implementation.message_thread = ext.SDL_CreateThread(processTasks, - "File System Worker", implementation) orelse { - - ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, - "Failed to create file-system work processor"); - - return error.OutOfThreads; - }; - } - }; - - /// - /// Returns a reference to the currently loaded data file-system. - /// - pub fn data(app_context: *AppContext) *FileSystem { - return &Implementation.cast(app_context).data_file_system; + // TODO: Error check this. + _ = ext.SDL_SemPost(app.message_semaphore); } /// + /// Asynchronously executes `procedure` with `arguments` as an anonymous struct of its arguments + /// and `app` as its execution context. /// + /// Once the execution frame resumes, the value returned by executing `procedure` is returned. /// - pub fn schedule(app_context: *AppContext, procedure: anytype, + pub fn schedule(app: *App, procedure: anytype, arguments: anytype) ona.meta.FnReturn(@TypeOf(procedure)) { const Task = struct { @@ -246,7 +65,7 @@ pub const AppContext = opaque { .arguments = &arguments, }; - var message = AppContext.Message{ + var message = Message{ .kind = .{.task = .{ .data = &task, .action = Task.process, @@ -254,14 +73,22 @@ pub const AppContext = opaque { }}, }; - suspend Implementation.cast(app_context).enqueue(&message); + suspend app.enqueue(&message); } /// - /// Returns a reference to the currently loaded user file-system. + /// Asynchronously logs `info` with `logger` as the logging method and `app` as the execution + /// context. /// - pub fn user(app_context: *AppContext) *FileSystem { - return &Implementation.cast(app_context).user_file_system; + pub fn log(app: *App, logger: Logger, info: []const u8) void { + var message = Message{ + .kind = .{.log = .{ + .logger = logger, + .info = info, + }}, + }; + + app.enqueue(&message); } }; @@ -274,17 +101,48 @@ pub const FileSystem = union(enum) { archive: Archive, /// - /// With files typically being backed by a block device, they can produce a variety of - /// errors - from physical to virtual errors - these are all encapsulated by the API as - /// general [OpenError.FileNotFound] errors. + /// Archive file system information. + /// + const Archive = struct { + file_access: ona.io.FileAccess, + index_cache: IndexCache, + entry_table: [max_open_entries]Entry = std.mem.zeroes([max_open_entries]Entry), + + /// + /// Hard limit on the maximum number of entries open at once. + /// + const max_open_entries = 16; + + /// + /// Stateful extension of an [oar.Entry]. + /// + const Entry = struct { + owner: ?*ona.io.FileAccess, + cursor: u64, + header: oar.Entry, + }; + + /// + /// Table cache for associating [oar.Path] values with offsets to entries in a given file. + /// + const IndexCache = ona.table.Hashed(oar.Path, u64, .{ + .equals = oar.Path.equals, + .hash = oar.Path.hash, + }); + }; + + /// + /// With files typically being backed by a block device, they can produce a variety of errors - + /// from physical to virtual errors - these are all encapsulated by the API as general + /// [OpenError.FileNotFound] errors. /// /// When a given [FileSystem] does not support a specified [OpenMode], /// [OpenError.ModeUnsupported] is used to inform the consuming code that another [OpenMode] /// should be tried or, if no mode other is suitable, that the resource is effectively /// unavailable. /// - /// If the number of known [FileAccess] handles has been exhausted, [OpenError.OutOfFiles] - /// is used to communicate this. + /// If the number of known [FileAccess] handles has been exhausted, [OpenError.OutOfFiles] is + /// used to communicate this. /// pub const OpenError = error { FileNotFound, @@ -380,12 +238,13 @@ pub const FileSystem = union(enum) { } fn skip(context: *anyopaque, offset: i64) FileAccess.Error!void { - const math = std.math; const archive_entry = archiveEntryCast(context); if (archive_entry.owner == null) return error.FileInaccessible; if (offset < 0) { + const math = std.math; + archive_entry.cursor = math.max(0, archive_entry.cursor - math.absCast(offset)); } else { @@ -447,17 +306,18 @@ pub const FileSystem = union(enum) { .native => |native| { if (native.len == 0) return error.FileNotFound; - var path_buffer = std.mem.zeroes([4096]u8); + const mem = std.mem; + var path_buffer = mem.zeroes([4096]u8); const seperator_length = @boolToInt(native[native.len - 1] != oar.Path.seperator); if ((native.len + seperator_length + path.length) >= path_buffer.len) return error.FileNotFound; - std.mem.copy(u8, path_buffer[0 ..], native); + mem.copy(u8, &path_buffer, native); if (seperator_length != 0) path_buffer[native.len] = oar.Path.seperator; - std.mem.copy(u8, path_buffer[native.len .. path_buffer. + mem.copy(u8, path_buffer[native.len .. path_buffer. len], path.buffer[0 .. path.length]); const FileAccess = ona.io.FileAccess; @@ -565,7 +425,7 @@ pub const FileSystem = union(enum) { /// /// /// -pub const GraphicsContext = opaque { +pub const Graphics = opaque { /// /// /// @@ -587,8 +447,8 @@ pub const GraphicsContext = opaque { /// /// /// - pub fn poll(graphics_context: *GraphicsContext) ?*const Event { - _ = graphics_context; + pub fn poll(graphics: *Graphics) ?*const Event { + _ = graphics; return null; } @@ -596,41 +456,46 @@ pub const GraphicsContext = opaque { /// /// /// - pub fn present(graphics_context: *GraphicsContext) void { + pub fn present(graphics: *Graphics) void { // TODO: Implement; - _ = graphics_context; + _ = graphics; } }; /// -/// Returns a graphics runner that uses `Errors` as its error set. +/// [Logger.info] logs information that isn't necessarily an error but indicates something useful to +/// be logged. /// -pub fn GraphicsRunner(comptime Errors: type) type { - return fn (*AppContext, *GraphicsContext) callconv(.Async) Errors!void; -} - +/// [Logger.debug] logs information only when the engine is in debug mode. /// -/// [Log.info] represents a log message which is purely informative and does not indicate any kind -/// of issue. +/// [Logger.warning] logs information to indicate a non-critical error has occured. /// -/// [Log.debug] represents a log message which is purely for debugging purposes and will only occurs -/// in debug builds. -/// -/// [Log.warning] represents a log message which is a warning about a issue that does not break -/// anything important but is not ideal. -/// -pub const Log = enum(u32) { +pub const Logger = enum(u32) { info = ext.SDL_LOG_PRIORITY_INFO, debug = ext.SDL_LOG_PRIORITY_DEBUG, warning = ext.SDL_LOG_PRIORITY_WARN, +}; - /// - /// Writes `utf8_message` as the log kind identified by `log`. - /// - pub fn write(log: Log, utf8_message: []const u8) void { - ext.SDL_LogMessage(ext.SDL_LOG_CATEGORY_APPLICATION, - @enumToInt(log), "%.*s", utf8_message.len, utf8_message.ptr); - } +/// +/// Linked list of asynchronous messages chained together to be processed by the work processor. +/// +pub const Message = struct { + next: ?*Message = null, + + kind: union(enum) { + quit, + + log: struct { + logger: Logger, + info: []const u8, + }, + + task: struct { + data: *anyopaque, + action: fn (*anyopaque) void, + frame: anyframe, + }, + }, }; /// @@ -639,13 +504,10 @@ pub const Log = enum(u32) { pub const Path = oar.Path; /// -/// [RunError.InitFailure] occurs if a necessary resource fails to be acquired or allocated. -/// -/// [RunError.AlreadyRunning] occurs if a runner has already been started. +/// [RunError.InitFailure] occurs when the runtime fails to initialize. /// pub const RunError = error { InitFailure, - AlreadyRunning, }; /// @@ -654,8 +516,76 @@ pub const RunError = error { /// Should an error from `run` occur, an `Error` is returned, otherwise a [RunError] is returned if /// the underlying runtime fails and is logged. /// -pub fn runGraphics(comptime Error: anytype, - comptime run: GraphicsRunner(Error)) (RunError || Error)!void { +pub fn display(comptime Error: anytype, + comptime run: fn (*App, *Graphics) callconv(.Async) Error!void) (RunError || Error)!void { + + var cwd = FileSystem{.native = "./"}; + const user_prefix = ext.SDL_GetPrefPath("ona", "ona") orelse return error.InitFailure; + + defer ext.SDL_free(user_prefix); + + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + + defer if (gpa.deinit()) + ext.SDL_LogWarn(ext.SDL_LOG_CATEGORY_APPLICATION, "Runtime allocator leaked memory"); + + var app = App{ + .user = .{.native = std.mem.sliceTo(user_prefix, 0)}, + + .message_semaphore = ext.SDL_CreateSemaphore(0) orelse { + ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create message semaphore"); + + return error.InitFailure; + }, + + .message_mutex = ext.SDL_CreateMutex() orelse { + ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create message mutex"); + + return error.InitFailure; + }, + + .data = .{.archive = .{ + .index_cache = FileSystem.Archive.IndexCache.init(gpa.allocator()) catch + return error.InitFailure, + + .file_access = cwd.open(try Path.joined(&.{"./data.oar"}), .readonly) catch { + ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to load ./data.oar"); + + return error.InitFailure; + }, + }}, + }; + + defer { + app.data.archive.file_access.close(); + app.data.archive.index_cache.deinit(); + ext.SDL_DestroySemaphore(app.message_semaphore); + ext.SDL_DestroyMutex(app.message_mutex); + } + + const message_thread = ext.SDL_CreateThread(processMessages, "Message Processor", &app) orelse { + ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create message processor"); + + return error.InitFailure; + }; + + defer { + var message = Message{.kind = .quit}; + + app.enqueue(&message); + + { + var status = std.mem.zeroes(c_int); + + // SDL2 defines waiting on a null thread reference as a no-op. See + // https://wiki.libsdl.org/SDL_WaitThread for more information + ext.SDL_WaitThread(message_thread, &status); + + if (status != 0) { + // TODO: Error check this. + } + } + } if (ext.SDL_Init(ext.SDL_INIT_EVERYTHING) != 0) { ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize runtime"); @@ -690,42 +620,47 @@ pub fn runGraphics(comptime Error: anytype, defer ext.SDL_DestroyRenderer(renderer); - var cwd_file_system = FileSystem{.native ="./"}; - var data_access = try cwd_file_system.open(try Path.joined(&.{"./data.oar"}), .readonly); - - defer data_access.close(); - - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - - defer _ = gpa.deinit(); - - var app_context = AppContext.Implementation.init(gpa.allocator(), data_access) catch |err| { - ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, switch (err) { - error.OutOfMemory => "Failed to allocate necessary memory", - error.OutOfMutexes => "Failed to create file-system work lock", - error.OutOfSemaphores => "Failed to create file-system work scheduler", - }); - - return error.InitFailure; - }; - - defer app_context.deinit(); - - app_context.start() catch |err| { - ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, switch (err) { - // Not possible for it to have already been started. - error.AlreadyStarted => unreachable, - error.OutOfThreads => "Failed to start file-system work processor", - }); - - return error.InitFailure; - }; - - var graphics_context = GraphicsContext.Implementation{ + var graphics = Graphics.Implementation{ .event = .{ }, }; - return run(@ptrCast(*AppContext, &app_context), @ptrCast(*GraphicsContext, &graphics_context)); + return run(@ptrCast(*App, &app), @ptrCast(*Graphics, &graphics)); +} + +/// +/// [FileSystemMessage] processing function used by a dedicated worker thread, where `data` +/// is a type-erased reference to a [EventLoop]. +/// +/// The processor returns `0` if it exited normally or any other value if an erroneous exit +/// occured. +/// +pub fn processMessages(userdata: ?*anyopaque) callconv(.C) c_int { + const app = @ptrCast(*App, @alignCast(@alignOf(App), userdata orelse unreachable)); + + while (true) { + // TODO: Error check these. + _ = ext.SDL_SemWait(app.message_semaphore); + _ = ext.SDL_LockMutex(app.message_mutex); + + defer _ = ext.SDL_UnlockMutex(app.message_mutex); + + while (app.message_chain) |message| { + switch (message.kind) { + .quit => return 0, + + .log => |log| ext.SDL_LogMessage(ext.SDL_LOG_CATEGORY_APPLICATION, + @enumToInt(log.logger), "%.*s", log.info.len, log.info.ptr), + + .task => |task| { + task.action(task.data); + + resume task.frame; + }, + } + + app.message_chain = message.next; + } + } } diff --git a/src/engine/sys/Archive.zig b/src/engine/sys/Archive.zig deleted file mode 100644 index 500ff4a..0000000 --- a/src/engine/sys/Archive.zig +++ /dev/null @@ -1,29 +0,0 @@ -const oar = @import("oar"); -const ona = @import("ona"); -const std = @import("std"); - -file_access: ona.io.FileAccess, -index_cache: IndexCache, -entry_table: [max_open_entries]Entry = std.mem.zeroes([max_open_entries]Entry), - -/// -/// Hard limit on the maximum number of entries open at once. -/// -const max_open_entries = 16; - -/// -/// Stateful extension of an [oar.Entry]. -/// -pub const Entry = struct { - owner: ?*ona.io.FileAccess, - cursor: u64, - header: oar.Entry, -}; - -/// -/// Table cache for associating [oar.Path] values with offsets to entries in a given file. -/// -pub const IndexCache = ona.table.Hashed(oar.Path, u64, .{ - .equals = oar.Path.equals, - .hash = oar.Path.hash, -}); diff --git a/src/oar/main.zig b/src/oar/main.zig index e11d69e..85ee82c 100644 --- a/src/oar/main.zig +++ b/src/oar/main.zig @@ -30,7 +30,7 @@ pub const Entry = extern struct { const origin = try file_access.queryCursor(); if (((try file_access.read(std.mem.asBytes(&entry))) != @sizeOf(Entry)) and - ona.io.equalsBytes(entry.signature[0 ..], signature_magic[0 ..])) { + ona.io.equalsBytes(&entry.signature, &signature_magic)) { try file_access.seek(origin);