Add entry caching to archive file systems
This commit is contained in:
parent
b8517d3b22
commit
26f342e518
|
@ -11,7 +11,9 @@ const sys = @import("./sys.zig");
|
|||
/// Entry point.
|
||||
///
|
||||
pub fn main() anyerror!void {
|
||||
return nosuspend await async sys.runGraphics(anyerror, run);
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
|
||||
return nosuspend await async sys.runGraphics(gpa.allocator(), anyerror, run);
|
||||
}
|
||||
|
||||
test {
|
||||
|
|
172
src/sys.zig
172
src/sys.zig
|
@ -35,6 +35,7 @@ pub const AppContext = opaque {
|
|||
/// 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,
|
||||
|
@ -104,6 +105,7 @@ pub const AppContext = opaque {
|
|||
|
||||
ext.SDL_DestroyMutex(implementation.message_mutex);
|
||||
ext.SDL_DestroySemaphore(implementation.message_semaphore);
|
||||
ext.SDL_free(implementation.user_path_prefix);
|
||||
}
|
||||
|
||||
///
|
||||
|
@ -134,14 +136,25 @@ pub const AppContext = opaque {
|
|||
///
|
||||
/// Returns the created [Implementation] value on success or [InitError] on failure.
|
||||
///
|
||||
fn init(data_archive_file_access: FileAccess,
|
||||
user_path_prefix: []const u8) InitError!Implementation {
|
||||
fn init(allocator: std.mem.Allocator,
|
||||
data_archive_file_access: FileAccess) InitError!Implementation {
|
||||
|
||||
const user_path_prefix = ext.SDL_GetPrefPath("ona", "ona") orelse
|
||||
return error.OutOfMemory;
|
||||
|
||||
return Implementation{
|
||||
.user_file_system = .{.native = .{.path_prefix =
|
||||
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,
|
||||
.data_file_system = .{.archive = .{.file_access = data_archive_file_access}},
|
||||
.user_file_system = .{.native = .{.path_prefix = user_path_prefix}},
|
||||
.user_path_prefix = user_path_prefix,
|
||||
|
||||
.data_file_system = .{.archive = .{
|
||||
.file_access = data_archive_file_access,
|
||||
.index_cache = try FileSystem.ArchiveIndexCache.init(allocator),
|
||||
}},
|
||||
|
||||
.message_thread = null,
|
||||
};
|
||||
}
|
||||
|
@ -351,7 +364,7 @@ pub const FileSystem = union(enum) {
|
|||
|
||||
archive: struct {
|
||||
file_access: FileAccess,
|
||||
index_cache: *table.Dynamic([]const u8, ArchiveEntry, table.string_context),
|
||||
index_cache: ArchiveIndexCache,
|
||||
|
||||
entry_table: [max_open_entries]ArchiveEntry =
|
||||
std.mem.zeroes([max_open_entries]ArchiveEntry),
|
||||
|
@ -368,6 +381,8 @@ pub const FileSystem = union(enum) {
|
|||
cursor: u64,
|
||||
};
|
||||
|
||||
const ArchiveIndexCache = table.Hashed([]const u8, oar.Entry, table.string_context);
|
||||
|
||||
///
|
||||
/// Platform-agnostic mechanism for referencing files and directories on a [FileSystem].
|
||||
///
|
||||
|
@ -447,99 +462,98 @@ pub const FileSystem = union(enum) {
|
|||
.archive => |*archive| {
|
||||
if (mode != .readonly) return error.ModeUnsupported;
|
||||
|
||||
for (archive.entry_table) |_, index| {
|
||||
if (archive.entry_table[index].using == null) {
|
||||
const archive_path = path.buffer[0 .. path.length];
|
||||
for (archive.entry_table) |*entry| if (entry.using == null) {
|
||||
const entry_path = path.buffer[0 .. path.length];
|
||||
|
||||
entry.* = .{
|
||||
.header = find_header: {
|
||||
if (archive.index_cache.lookup(entry_path)) |header|
|
||||
break: find_header header.*;
|
||||
|
||||
const entry_header = archive.index_cache.lookup(archive_path) orelse {
|
||||
const header = oar.Entry.find(archive.file_access,
|
||||
archive_path) catch return error.FileNotFound;
|
||||
entry_path) catch return error.FileNotFound;
|
||||
|
||||
archive.index_cache.insert(archive_path, header) catch {
|
||||
// If caching fails... oh well...
|
||||
};
|
||||
// If caching fails... oh well...
|
||||
archive.index_cache.insert(entry_path, header) catch {};
|
||||
|
||||
break header;
|
||||
};
|
||||
break: find_header header;
|
||||
},
|
||||
|
||||
archive.entry_table[index] = .{
|
||||
.header = entry_header,
|
||||
.using = &archive.file_access,
|
||||
.cursor = 0,
|
||||
};
|
||||
.using = &archive.file_access,
|
||||
.cursor = 0,
|
||||
};
|
||||
|
||||
const Implementation = struct {
|
||||
fn archiveEntryCast(context: *anyopaque) *ArchiveEntry {
|
||||
return @ptrCast(*ArchiveEntry, @alignCast(
|
||||
@alignOf(ArchiveEntry), context));
|
||||
}
|
||||
const Implementation = struct {
|
||||
fn archiveEntryCast(context: *anyopaque) *ArchiveEntry {
|
||||
return @ptrCast(*ArchiveEntry, @alignCast(
|
||||
@alignOf(ArchiveEntry), context));
|
||||
}
|
||||
|
||||
fn close(context: *anyopaque) void {
|
||||
archiveEntryCast(context).using = null;
|
||||
}
|
||||
fn close(context: *anyopaque) void {
|
||||
archiveEntryCast(context).using = null;
|
||||
}
|
||||
|
||||
fn queryCursor(context: *anyopaque) FileAccess.Error!u64 {
|
||||
const archive_entry = archiveEntryCast(context);
|
||||
fn queryCursor(context: *anyopaque) FileAccess.Error!u64 {
|
||||
const archive_entry = archiveEntryCast(context);
|
||||
|
||||
if (archive_entry.using == null) return error.FileInaccessible;
|
||||
if (archive_entry.using == null) return error.FileInaccessible;
|
||||
|
||||
return archive_entry.cursor;
|
||||
}
|
||||
return archive_entry.cursor;
|
||||
}
|
||||
|
||||
fn queryLength(context: *anyopaque) FileAccess.Error!u64 {
|
||||
const archive_entry = archiveEntryCast(context);
|
||||
fn queryLength(context: *anyopaque) FileAccess.Error!u64 {
|
||||
const archive_entry = archiveEntryCast(context);
|
||||
|
||||
if (archive_entry.using == null) return error.FileInaccessible;
|
||||
if (archive_entry.using == null) return error.FileInaccessible;
|
||||
|
||||
return archive_entry.header.file_size;
|
||||
}
|
||||
return archive_entry.header.file_size;
|
||||
}
|
||||
|
||||
fn read(context: *anyopaque, buffer: []u8) FileAccess.Error!usize {
|
||||
const archive_entry = archiveEntryCast(context);
|
||||
fn read(context: *anyopaque, buffer: []u8) FileAccess.Error!usize {
|
||||
const archive_entry = archiveEntryCast(context);
|
||||
|
||||
const file_access = archive_entry.using orelse
|
||||
return error.FileInaccessible;
|
||||
const file_access = archive_entry.using orelse
|
||||
return error.FileInaccessible;
|
||||
|
||||
if (archive_entry.cursor >= archive_entry.header.file_size)
|
||||
return error.FileInaccessible;
|
||||
if (archive_entry.cursor >= archive_entry.header.file_size)
|
||||
return error.FileInaccessible;
|
||||
|
||||
try file_access.seek(archive_entry.header.file_offset);
|
||||
try file_access.seek(archive_entry.header.file_offset);
|
||||
|
||||
return file_access.read(buffer[0 .. std.math.min(
|
||||
buffer.len, archive_entry.header.file_size)]);
|
||||
}
|
||||
return file_access.read(buffer[0 .. std.math.min(
|
||||
buffer.len, archive_entry.header.file_size)]);
|
||||
}
|
||||
|
||||
fn seek(context: *anyopaque, cursor: usize) FileAccess.Error!void {
|
||||
const archive_entry = archiveEntryCast(context);
|
||||
fn seek(context: *anyopaque, cursor: usize) FileAccess.Error!void {
|
||||
const archive_entry = archiveEntryCast(context);
|
||||
|
||||
if (archive_entry.using == null) return error.FileInaccessible;
|
||||
if (archive_entry.using == null) return error.FileInaccessible;
|
||||
|
||||
archive_entry.cursor = cursor;
|
||||
}
|
||||
archive_entry.cursor = cursor;
|
||||
}
|
||||
|
||||
fn seekToEnd(context: *anyopaque) FileAccess.Error!void {
|
||||
const archive_entry = archiveEntryCast(context);
|
||||
fn seekToEnd(context: *anyopaque) FileAccess.Error!void {
|
||||
const archive_entry = archiveEntryCast(context);
|
||||
|
||||
if (archive_entry.using == null) return error.FileInaccessible;
|
||||
if (archive_entry.using == null) return error.FileInaccessible;
|
||||
|
||||
archive_entry.cursor = archive_entry.header.file_size;
|
||||
}
|
||||
};
|
||||
archive_entry.cursor = archive_entry.header.file_size;
|
||||
}
|
||||
};
|
||||
|
||||
return FileAccess{
|
||||
.context = &archive.entry_table[index],
|
||||
return FileAccess{
|
||||
.context = entry,
|
||||
|
||||
.implementation = &.{
|
||||
.close = Implementation.close,
|
||||
.queryCursor = Implementation.queryCursor,
|
||||
.queryLength = Implementation.queryLength,
|
||||
.read = Implementation.read,
|
||||
.seek = Implementation.seek,
|
||||
.seekToEnd = Implementation.seekToEnd,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
.implementation = &.{
|
||||
.close = Implementation.close,
|
||||
.queryCursor = Implementation.queryCursor,
|
||||
.queryLength = Implementation.queryLength,
|
||||
.read = Implementation.read,
|
||||
.seek = Implementation.seek,
|
||||
.seekToEnd = Implementation.seekToEnd,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return error.OutOfFiles;
|
||||
},
|
||||
|
@ -805,7 +819,7 @@ 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,
|
||||
pub fn runGraphics(allocator: std.mem.Allocator, comptime Error: anytype,
|
||||
comptime run: GraphicsRunner(Error)) (RunError || Error)!void {
|
||||
|
||||
if (ext.SDL_Init(ext.SDL_INIT_EVERYTHING) != 0) {
|
||||
|
@ -841,14 +855,6 @@ pub fn runGraphics(comptime Error: anytype,
|
|||
|
||||
defer ext.SDL_DestroyRenderer(renderer);
|
||||
|
||||
const user_path_prefix = ext.SDL_GetPrefPath("ona", "ona") orelse {
|
||||
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to load user path");
|
||||
|
||||
return error.InitFailure;
|
||||
};
|
||||
|
||||
defer ext.SDL_free(user_path_prefix);
|
||||
|
||||
var cwd_file_system = FileSystem{.native =.{.path_prefix = "./"}};
|
||||
|
||||
var data_archive_file_access = try (try cwd_file_system.
|
||||
|
@ -856,9 +862,7 @@ pub fn runGraphics(comptime Error: anytype,
|
|||
|
||||
defer data_archive_file_access.close();
|
||||
|
||||
var app_context = AppContext.Implementation.init(data_archive_file_access,
|
||||
user_path_prefix[0 .. std.mem.len(user_path_prefix)]) catch |err| {
|
||||
|
||||
var app_context = AppContext.Implementation.init(allocator, data_archive_file_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",
|
||||
|
|
|
@ -1,61 +1,119 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub fn Dynamic(comptime Key: type, comptime Value: type, comptime key_context: KeyContext(Key)) type {
|
||||
///
|
||||
/// Returns a hash-backed table type of `Value`s indexed by `Key` and using `key_context` as the key
|
||||
/// context.
|
||||
///
|
||||
pub fn Hashed(comptime Key: type, comptime Value: type,
|
||||
comptime key_context: KeyContext(Key)) type {
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
return struct {
|
||||
load_maximum: f32,
|
||||
buckets_used: usize,
|
||||
allocator: Allocator,
|
||||
load_limit: f32,
|
||||
buckets: []Bucket,
|
||||
filled: usize,
|
||||
|
||||
///
|
||||
///
|
||||
/// A slot in the hash table.
|
||||
///
|
||||
const Bucket = struct {
|
||||
maybe_entry: ?struct {
|
||||
key: Key,
|
||||
value: Value,
|
||||
},
|
||||
} = null,
|
||||
|
||||
maybe_next_index: ?usize,
|
||||
maybe_next_index: ?usize = null,
|
||||
};
|
||||
|
||||
///
|
||||
///
|
||||
/// Hash table type.
|
||||
///
|
||||
const Self = @This();
|
||||
|
||||
///
|
||||
/// Searches for `key` to delete it, returning the deleted value or `null` if no matching
|
||||
/// key was found.
|
||||
///
|
||||
///
|
||||
pub fn delete(self: Self, key: Key) bool {
|
||||
_ = key;
|
||||
_ = self;
|
||||
pub fn remove(self: Self, key: Key) ?Value {
|
||||
var bucket = &(self.buckets[@mod(key_context.hash(key), self.buckets.len)]);
|
||||
|
||||
if (bucket.maybe_entry) |*entry| if (key_context.equals(entry.key, key)) {
|
||||
defer entry.value = null;
|
||||
|
||||
self.filled -= 1;
|
||||
|
||||
return entry.value;
|
||||
};
|
||||
|
||||
while (bucket.maybe_next_index) |index| {
|
||||
bucket = &(self.buckets[index]);
|
||||
|
||||
if (bucket.maybe_entry) |*entry| if (key_context.equals(entry.key, key)) {
|
||||
defer entry.value = null;
|
||||
|
||||
self.filled -= 1;
|
||||
|
||||
return entry.value;
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator) Allocator.Error!Self {
|
||||
return Self{
|
||||
.buckets = try allocator.alloc(Bucket, 4),
|
||||
.filled = 0,
|
||||
.allocator = allocator,
|
||||
.load_limit = 0.75,
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to insert the value at `key` to be `value` in `self`, returning an
|
||||
/// [InsertError] if it fails.
|
||||
///
|
||||
///
|
||||
pub fn insert(self: Self, key: Key, value: Value) InsertError!void {
|
||||
if ((@intToFloat(f32, self.buckets_used) / @intToFloat(
|
||||
f32, self.buckets.len)) >= self.load_maximum) try self.rehash();
|
||||
pub fn insert(self: *Self, key: Key, value: Value) InsertError!void {
|
||||
if (self.loadFactor() >= self.load_limit) {
|
||||
const old_buckets = self.buckets;
|
||||
|
||||
defer self.allocator.free(old_buckets);
|
||||
|
||||
self.buckets = try self.allocator.alloc(Bucket, old_buckets.len * 2);
|
||||
|
||||
for (old_buckets) |bucket, index| self.buckets[index] = bucket;
|
||||
}
|
||||
|
||||
var hash = @mod(key_context.hash(key), self.buckets.len);
|
||||
|
||||
while (true) {
|
||||
const bucket = &(self.buckets[hash]);
|
||||
|
||||
const entry = &(bucket.maybe_entry orelse {
|
||||
if (key_context.equals((bucket.maybe_entry orelse {
|
||||
bucket.maybe_entry = .{
|
||||
.key = key,
|
||||
.value = value
|
||||
};
|
||||
});
|
||||
|
||||
if (key_context.equals(entry.key, key)) return error.KeyExists;
|
||||
self.filled += 1;
|
||||
|
||||
hash = @mod(hashHash(hash), self.buckets.len);
|
||||
break;
|
||||
}).key, key)) return error.KeyExists;
|
||||
|
||||
hash = @mod(hash + 1, self.buckets.len);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns the current load factor of `self`, which is derived from the number of capacity
|
||||
/// that has been filled.
|
||||
///
|
||||
pub fn loadFactor(self: Self) f32 {
|
||||
return @intToFloat(f32, self.filled) / @intToFloat(f32, self.buckets.len);
|
||||
}
|
||||
|
||||
///
|
||||
/// Searches for a value indexed with `key` in `self`, returning it or `null` if no matching
|
||||
/// entry was found.
|
||||
|
@ -81,7 +139,7 @@ pub fn Dynamic(comptime Key: type, comptime Value: type, comptime key_context: K
|
|||
///
|
||||
///
|
||||
///
|
||||
pub const InsertError = std.mem.Allocator || error {
|
||||
pub const InsertError = std.mem.Allocator.Error || error {
|
||||
KeyExists,
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue