Refactor SDL2 interface code
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details

This commit is contained in:
kayomn 2022-10-19 23:58:56 +01:00
parent 1a28dc2404
commit 32bb049f73
4 changed files with 223 additions and 317 deletions

View File

@ -6,16 +6,16 @@ const sys = @import("./sys.zig");
/// Starts the the game engine. /// Starts the the game engine.
/// ///
pub fn main() anyerror!void { 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(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit(); 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(); 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; if ((try file_access.read(buffer)) != file_size) return error.ScriptLoadFailure;
sys.Log.debug.write(buffer); app.log(.debug, buffer);
} }
while (graphics.poll()) |_| { while (graphics.poll()) |_| {

View File

@ -1,5 +1,3 @@
const Archive = @import("./sys/Archive.zig");
const ext = @cImport({ const ext = @cImport({
@cInclude("SDL2/SDL.h"); @cInclude("SDL2/SDL.h");
}); });
@ -9,222 +7,43 @@ const ona = @import("ona");
const std = @import("std"); 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 { pub const App = struct {
/// message_chain: ?*Message = null,
/// 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,
},
},
};
///
/// Internal state of the event loop hidden from the API consumer.
///
const Implementation = struct {
user_path_prefix: [*]u8,
data_file_system: FileSystem,
user_file_system: FileSystem,
message_semaphore: *ext.SDL_sem, message_semaphore: *ext.SDL_sem,
message_mutex: *ext.SDL_mutex, message_mutex: *ext.SDL_mutex,
message_thread: ?*ext.SDL_Thread, data: FileSystem,
messages: ?*Message = null, user: FileSystem,
/// ///
/// [StartError.OutOfSemaphores] indicates that the process has no more semaphores available /// Enqueues `message` to the message chain in `app`.
/// 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 fn enqueue(app: *App, message: *Message) void {
/// 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,
};
///
/// [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. // TODO: Error check these.
_ = ext.SDL_LockMutex(implementation.message_mutex); _ = ext.SDL_LockMutex(app.message_mutex);
defer _ = ext.SDL_UnlockMutex(implementation.message_mutex); defer _ = ext.SDL_UnlockMutex(app.message_mutex);
if (implementation.messages) |messages| { if (app.message_chain) |message_chain| {
messages.next = message; message_chain.next = message;
} else { } else {
implementation.messages = message; app.message_chain = message;
} }
} }
// TODO: Error check this. // TODO: Error check this.
_ = ext.SDL_SemPost(implementation.message_semaphore); _ = ext.SDL_SemPost(app.message_semaphore);
} }
/// ///
/// Initializes a new [Implemenation] with `data_archive_file_access` as the data archive to /// Asynchronously executes `procedure` with `arguments` as an anonymous struct of its arguments
/// read from and `user_path_prefix` as the native writable user data directory. /// and `app` as its execution context.
/// ///
/// Returns the created [Implementation] value on success or [InitError] on failure. /// Once the execution frame resumes, the value returned by executing `procedure` is returned.
/// ///
fn init(allocator: std.mem.Allocator, pub fn schedule(app: *App, procedure: anytype,
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;
}
}
}
///
/// 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;
}
///
///
///
pub fn schedule(app_context: *AppContext, procedure: anytype,
arguments: anytype) ona.meta.FnReturn(@TypeOf(procedure)) { arguments: anytype) ona.meta.FnReturn(@TypeOf(procedure)) {
const Task = struct { const Task = struct {
@ -246,7 +65,7 @@ pub const AppContext = opaque {
.arguments = &arguments, .arguments = &arguments,
}; };
var message = AppContext.Message{ var message = Message{
.kind = .{.task = .{ .kind = .{.task = .{
.data = &task, .data = &task,
.action = Task.process, .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 { pub fn log(app: *App, logger: Logger, info: []const u8) void {
return &Implementation.cast(app_context).user_file_system; var message = Message{
.kind = .{.log = .{
.logger = logger,
.info = info,
}},
};
app.enqueue(&message);
} }
}; };
@ -274,17 +101,48 @@ pub const FileSystem = union(enum) {
archive: Archive, archive: Archive,
/// ///
/// With files typically being backed by a block device, they can produce a variety of /// Archive file system information.
/// errors - from physical to virtual errors - these are all encapsulated by the API as ///
/// general [OpenError.FileNotFound] errors. 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], /// When a given [FileSystem] does not support a specified [OpenMode],
/// [OpenError.ModeUnsupported] is used to inform the consuming code that another [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 /// should be tried or, if no mode other is suitable, that the resource is effectively
/// unavailable. /// unavailable.
/// ///
/// If the number of known [FileAccess] handles has been exhausted, [OpenError.OutOfFiles] /// If the number of known [FileAccess] handles has been exhausted, [OpenError.OutOfFiles] is
/// is used to communicate this. /// used to communicate this.
/// ///
pub const OpenError = error { pub const OpenError = error {
FileNotFound, FileNotFound,
@ -380,12 +238,13 @@ pub const FileSystem = union(enum) {
} }
fn skip(context: *anyopaque, offset: i64) FileAccess.Error!void { fn skip(context: *anyopaque, offset: i64) FileAccess.Error!void {
const math = std.math;
const archive_entry = archiveEntryCast(context); const archive_entry = archiveEntryCast(context);
if (archive_entry.owner == null) return error.FileInaccessible; if (archive_entry.owner == null) return error.FileInaccessible;
if (offset < 0) { if (offset < 0) {
const math = std.math;
archive_entry.cursor = math.max(0, archive_entry.cursor = math.max(0,
archive_entry.cursor - math.absCast(offset)); archive_entry.cursor - math.absCast(offset));
} else { } else {
@ -447,17 +306,18 @@ pub const FileSystem = union(enum) {
.native => |native| { .native => |native| {
if (native.len == 0) return error.FileNotFound; 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); const seperator_length = @boolToInt(native[native.len - 1] != oar.Path.seperator);
if ((native.len + seperator_length + path.length) >= if ((native.len + seperator_length + path.length) >=
path_buffer.len) return error.FileNotFound; 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; 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]); len], path.buffer[0 .. path.length]);
const FileAccess = ona.io.FileAccess; 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 { pub fn poll(graphics: *Graphics) ?*const Event {
_ = graphics_context; _ = graphics;
return null; 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; // 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 { /// [Logger.debug] logs information only when the engine is in debug mode.
return fn (*AppContext, *GraphicsContext) callconv(.Async) Errors!void;
}
/// ///
/// [Log.info] represents a log message which is purely informative and does not indicate any kind /// [Logger.warning] logs information to indicate a non-critical error has occured.
/// of issue.
/// ///
/// [Log.debug] represents a log message which is purely for debugging purposes and will only occurs pub const Logger = enum(u32) {
/// 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) {
info = ext.SDL_LOG_PRIORITY_INFO, info = ext.SDL_LOG_PRIORITY_INFO,
debug = ext.SDL_LOG_PRIORITY_DEBUG, debug = ext.SDL_LOG_PRIORITY_DEBUG,
warning = ext.SDL_LOG_PRIORITY_WARN, warning = ext.SDL_LOG_PRIORITY_WARN,
};
/// ///
/// Writes `utf8_message` as the log kind identified by `log`. /// Linked list of asynchronous messages chained together to be processed by the work processor.
/// ///
pub fn write(log: Log, utf8_message: []const u8) void { pub const Message = struct {
ext.SDL_LogMessage(ext.SDL_LOG_CATEGORY_APPLICATION, next: ?*Message = null,
@enumToInt(log), "%.*s", utf8_message.len, utf8_message.ptr);
} 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; pub const Path = oar.Path;
/// ///
/// [RunError.InitFailure] occurs if a necessary resource fails to be acquired or allocated. /// [RunError.InitFailure] occurs when the runtime fails to initialize.
///
/// [RunError.AlreadyRunning] occurs if a runner has already been started.
/// ///
pub const RunError = error { pub const RunError = error {
InitFailure, 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 /// Should an error from `run` occur, an `Error` is returned, otherwise a [RunError] is returned if
/// the underlying runtime fails and is logged. /// the underlying runtime fails and is logged.
/// ///
pub fn runGraphics(comptime Error: anytype, pub fn display(comptime Error: anytype,
comptime run: GraphicsRunner(Error)) (RunError || Error)!void { 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) { if (ext.SDL_Init(ext.SDL_INIT_EVERYTHING) != 0) {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize runtime"); 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); defer ext.SDL_DestroyRenderer(renderer);
var cwd_file_system = FileSystem{.native ="./"}; var graphics = Graphics.Implementation{
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{
.event = .{ .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;
}
}
} }

View File

@ -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,
});

View File

@ -30,7 +30,7 @@ pub const Entry = extern struct {
const origin = try file_access.queryCursor(); const origin = try file_access.queryCursor();
if (((try file_access.read(std.mem.asBytes(&entry))) != @sizeOf(Entry)) and 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); try file_access.seek(origin);