Refactor SDL2 interface code
This commit is contained in:
parent
1a28dc2404
commit
32bb049f73
|
@ -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()) |_| {
|
||||||
|
|
|
@ -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.
|
message_semaphore: *ext.SDL_sem,
|
||||||
///
|
message_mutex: *ext.SDL_mutex,
|
||||||
const Message = struct {
|
data: FileSystem,
|
||||||
next: ?*Message = null,
|
user: FileSystem,
|
||||||
|
|
||||||
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.
|
/// Enqueues `message` to the message chain in `app`.
|
||||||
///
|
///
|
||||||
const Implementation = struct {
|
fn enqueue(app: *App, message: *Message) void {
|
||||||
user_path_prefix: [*]u8,
|
{
|
||||||
data_file_system: FileSystem,
|
// TODO: Error check these.
|
||||||
user_file_system: FileSystem,
|
_ = ext.SDL_LockMutex(app.message_mutex);
|
||||||
message_semaphore: *ext.SDL_sem,
|
|
||||||
message_mutex: *ext.SDL_mutex,
|
|
||||||
message_thread: ?*ext.SDL_Thread,
|
|
||||||
messages: ?*Message = null,
|
|
||||||
|
|
||||||
///
|
defer _ = ext.SDL_UnlockMutex(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,
|
|
||||||
};
|
|
||||||
|
|
||||||
///
|
if (app.message_chain) |message_chain| {
|
||||||
/// [StartError.OutOfThreads] indicates that the process has no more threads available to it
|
message_chain.next = message;
|
||||||
/// to use, meaning that no asynchronous work may be started on an [Implementation] at this
|
} else {
|
||||||
/// time.
|
app.message_chain = message;
|
||||||
///
|
|
||||||
/// [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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
// TODO: Error check this.
|
||||||
/// Attempts to start the asynchronous worker thread of `implementation` if it hasn't been
|
_ = ext.SDL_SemPost(app.message_semaphore);
|
||||||
/// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
/// 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)) {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
});
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue