Cubyz/src/utils.zig
2023-11-21 16:23:24 +01:00

1007 lines
32 KiB
Zig

const std = @import("std");
const Allocator = std.mem.Allocator;
const Atomic = std.atomic.Atomic;
const builtin = @import("builtin");
const main = @import("main.zig");
pub const Compression = struct {
pub fn deflate(allocator: Allocator, data: []const u8) ![]u8 {
var result = std.ArrayList(u8).init(allocator);
var comp = try std.compress.deflate.compressor(main.globalAllocator, result.writer(), .{.level = .default_compression});
_ = try comp.write(data);
try comp.close();
comp.deinit();
return result.toOwnedSlice();
}
pub fn inflateTo(buf: []u8, data: []const u8) !usize {
var stream = std.io.fixedBufferStream(data);
var decomp = try std.compress.deflate.decompressor(main.globalAllocator, stream.reader(), null);
defer decomp.deinit();
return try decomp.reader().readAll(buf);
}
pub fn pack(sourceDir: std.fs.IterableDir, writer: anytype) !void {
var comp = try std.compress.deflate.compressor(main.globalAllocator, writer, .{.level = .default_compression});
defer comp.deinit();
var walker = try sourceDir.walk(main.globalAllocator);
defer walker.deinit();
while(try walker.next()) |entry| {
if(entry.kind == .file) {
var relPath = entry.path;
if(builtin.os.tag == .windows) { // I hate you
const copy = try main.stackAllocator.dupe(u8, relPath);
std.mem.replaceScalar(u8, copy, '\\', '/');
relPath = copy;
}
defer if(builtin.os.tag == .windows) {
main.stackAllocator.free(relPath);
};
var len: [4]u8 = undefined;
std.mem.writeInt(u32, &len, @as(u32, @intCast(relPath.len)), .big);
_ = try comp.write(&len);
_ = try comp.write(relPath);
const file = try sourceDir.dir.openFile(relPath, .{});
defer file.close();
const fileData = try file.readToEndAlloc(main.stackAllocator, std.math.maxInt(u32));
defer main.stackAllocator.free(fileData);
std.mem.writeInt(u32, &len, @as(u32, @intCast(fileData.len)), .big);
_ = try comp.write(&len);
_ = try comp.write(fileData);
}
}
try comp.close();
}
pub fn unpack(outDir: std.fs.Dir, input: []const u8) !void {
var stream = std.io.fixedBufferStream(input);
var decomp = try std.compress.deflate.decompressor(main.globalAllocator, stream.reader(), null);
defer decomp.deinit();
const reader = decomp.reader();
const _data = try reader.readAllAlloc(main.stackAllocator, std.math.maxInt(usize));
defer main.stackAllocator.free(_data);
var data = _data;
while(data.len != 0) {
var len = std.mem.readInt(u32, data[0..4], .big);
data = data[4..];
const path = data[0..len];
data = data[len..];
len = std.mem.readInt(u32, data[0..4], .big);
data = data[4..];
const fileData = data[0..len];
data = data[len..];
var splitter = std.mem.splitBackwards(u8, path, "/");
_ = splitter.first();
try outDir.makePath(splitter.rest());
const file = try outDir.createFile(path, .{});
defer file.close();
try file.writeAll(fileData);
}
}
};
/// Implementation of https://en.wikipedia.org/wiki/Alias_method
pub fn AliasTable(comptime T: type) type {
return struct {
const AliasData = struct {
chance: u16,
alias: u16,
};
items: []T,
aliasData: []AliasData,
ownsSlice: bool = false,
fn initAliasData(self: *@This(), totalChance: f32, currentChances: []f32) void {
const desiredChance = totalChance/@as(f32, @floatFromInt(self.aliasData.len));
var lastOverfullIndex: u16 = 0;
var lastUnderfullIndex: u16 = 0;
outer: while(true) {
while(currentChances[lastOverfullIndex] <= desiredChance) {
lastOverfullIndex += 1;
if(lastOverfullIndex == self.items.len)
break :outer;
}
while(currentChances[lastUnderfullIndex] >= desiredChance) {
lastUnderfullIndex += 1;
if(lastUnderfullIndex == self.items.len)
break :outer;
}
const delta = desiredChance - currentChances[lastUnderfullIndex];
currentChances[lastUnderfullIndex] = desiredChance;
currentChances[lastOverfullIndex] -= delta;
self.aliasData[lastUnderfullIndex] = .{
.alias = lastOverfullIndex,
.chance = @intFromFloat(delta/desiredChance*std.math.maxInt(u16)),
};
if (currentChances[lastOverfullIndex] < desiredChance) {
lastUnderfullIndex = @min(lastUnderfullIndex, lastOverfullIndex);
}
}
}
pub fn init(allocator: Allocator, items: []T) !@This() {
var self: @This() = .{
.items = items,
.aliasData = try allocator.alloc(AliasData, items.len),
};
if(items.len == 0) return self;
@memset(self.aliasData, AliasData{.chance = 0, .alias = 0});
const currentChances = try main.stackAllocator.alloc(f32, items.len);
defer main.stackAllocator.free(currentChances);
var totalChance: f32 = 0;
for(items, 0..) |*item, i| {
totalChance += item.chance;
currentChances[i] = item.chance;
}
self.initAliasData(totalChance, currentChances);
return self;
}
pub fn initFromContext(allocator: Allocator, slice: anytype) !@This() {
const items = try allocator.alloc(T, slice.len);
for(slice, items) |context, *result| {
result.* = context.getItem();
}
var self: @This() = .{
.items = items,
.aliasData = try allocator.alloc(AliasData, items.len),
.ownsSlice = true,
};
if(items.len == 0) return self;
@memset(self.aliasData, AliasData{.chance = 0, .alias = 0});
const currentChances = try main.stackAllocator.alloc(f32, items.len);
defer main.stackAllocator.free(currentChances);
var totalChance: f32 = 0;
for(slice, 0..) |context, i| {
totalChance += context.chance;
currentChances[i] = context.chance;
}
self.initAliasData(totalChance, currentChances);
return self;
}
pub fn deinit(self: *const @This(), allocator: Allocator) void {
allocator.free(self.aliasData);
if(self.ownsSlice) {
allocator.free(self.items);
}
}
pub fn sample(self: *const @This(), seed: *u64) *T {
const initialIndex = main.random.nextIntBounded(u16, seed, @as(u16, @intCast(self.items.len)));
if(main.random.nextInt(u16, seed) < self.aliasData[initialIndex].chance) {
return &self.items[self.aliasData[initialIndex].alias];
}
return &self.items[initialIndex];
}
};
}
/// A list that is always sorted in ascending order based on T.lessThan(lhs, rhs).
pub fn SortedList(comptime T: type) type {
return struct {
const Self = @This();
ptr: [*]T = undefined,
len: u32 = 0,
capacity: u32 = 0,
pub fn deinit(self: Self, allocator: Allocator) void {
allocator.free(self.ptr[0..self.capacity]);
}
pub fn items(self: Self) []T {
return self.ptr[0..self.len];
}
fn increaseCapacity(self: *Self, allocator: Allocator) !void {
const newSize = 8 + self.capacity*3/2;
const newSlice = try allocator.realloc(self.ptr[0..self.capacity], newSize);
self.capacity = @intCast(newSlice.len);
self.ptr = newSlice.ptr;
}
pub fn insertSorted(self: *Self, allocator: Allocator, object: T) !void {
if(self.len == self.capacity) {
try self.increaseCapacity(allocator);
}
var i = self.len;
while(i != 0) { // Find the point to insert and move the rest out of the way.
if(object.lessThan(self.ptr[i - 1])) {
self.ptr[i] = self.ptr[i - 1];
} else {
break;
}
i -= 1;
}
self.len += 1;
self.ptr[i] = object;
}
pub fn toOwnedSlice(self: *Self, allocator: Allocator) ![]T {
const output = try allocator.realloc(self.ptr[0..self.capacity], self.len);
self.* = .{};
return output;
}
};
}
pub fn Array2D(comptime T: type) type {
return struct {
const Self = @This();
mem: []T,
width: u32,
height: u32,
pub fn init(allocator: Allocator, width: u32, height: u32) !Self {
return .{
.mem = try allocator.alloc(T, width*height),
.width = width,
.height = height,
};
}
pub fn deinit(self: Self, allocator: Allocator) void {
allocator.free(self.mem);
}
pub fn get(self: Self, x: usize, y: usize) T {
std.debug.assert(x < self.width and y < self.height);
return self.mem[x*self.height + y];
}
pub fn getRow(self: Self, x: usize) []T {
std.debug.assert(x < self.width);
return self.mem[x*self.height..][0..self.height];
}
pub fn set(self: Self, x: usize, y: usize, t: T) void {
std.debug.assert(x < self.width and y < self.height);
self.mem[x*self.height + y] = t;
}
pub fn ptr(self: Self, x: usize, y: usize) *T {
std.debug.assert(x < self.width and y < self.height);
return &self.mem[x*self.height + y];
}
};
}
pub fn Array3D(comptime T: type) type {
return struct {
const Self = @This();
mem: []T,
width: u32,
height: u32,
depth: u32,
pub fn init(allocator: Allocator, width: u32, height: u32, depth: u32) !Self {
return .{
.mem = try allocator.alloc(T, width*height*depth),
.width = width,
.height = height,
.depth = depth,
};
}
pub fn deinit(self: Self, allocator: Allocator) void {
allocator.free(self.mem);
}
pub fn get(self: Self, x: usize, y: usize, z: usize) T {
std.debug.assert(x < self.width and y < self.height and z < self.depth);
return self.mem[(x*self.height + y)*self.depth + z];
}
pub fn set(self: Self, x: usize, y: usize, z: usize, t: T) void {
std.debug.assert(x < self.width and y < self.height and z < self.depth);
self.mem[(x*self.height + y)*self.depth + z] = t;
}
pub fn ptr(self: Self, x: usize, y: usize, z: usize) *T {
std.debug.assert(x < self.width and y < self.height and z < self.depth);
return &self.mem[(x*self.height + y)*self.depth + z];
}
};
}
/// Allows for stack-like allocations in a fast and safe way.
/// It is safe in the sense that a regular allocator will be used when the buffer is full.
pub const StackAllocator = struct {
const Allocation = struct{start: u32, len: u32};
backingAllocator: Allocator,
buffer: []align(4096) u8,
allocationList: std.ArrayList(Allocation),
index: usize,
pub fn init(backingAllocator: Allocator, size: u32) !StackAllocator {
return .{
.backingAllocator = backingAllocator,
.buffer = try backingAllocator.alignedAlloc(u8, 4096, size),
.allocationList = std.ArrayList(Allocation).init(backingAllocator),
.index = 0,
};
}
pub fn deinit(self: StackAllocator) void {
if(self.allocationList.items.len != 0) {
std.log.err("Memory leak in Stack Allocator", .{});
}
self.allocationList.deinit();
self.backingAllocator.free(self.buffer);
}
pub fn allocator(self: *StackAllocator) Allocator {
return .{
.vtable = &.{
.alloc = &alloc,
.resize = &resize,
.free = &free,
},
.ptr = self,
};
}
fn isInsideBuffer(self: *StackAllocator, buf: []u8) bool {
const bufferStart = @intFromPtr(self.buffer.ptr);
const bufferEnd = bufferStart + self.buffer.len;
const compare = @intFromPtr(buf.ptr);
return compare >= bufferStart and compare < bufferEnd;
}
fn indexInBuffer(self: *StackAllocator, buf: []u8) usize {
const bufferStart = @intFromPtr(self.buffer.ptr);
const compare = @intFromPtr(buf.ptr);
return compare - bufferStart;
}
/// Attempt to allocate exactly `len` bytes aligned to `1 << ptr_align`.
///
/// `ret_addr` is optionally provided as the first return address of the
/// allocation call stack. If the value is `0` it means no return address
/// has been provided.
fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8 {
const self: *StackAllocator = @ptrCast(@alignCast(ctx));
if(len >= self.buffer.len) return self.backingAllocator.rawAlloc(len, ptr_align, ret_addr);
const start = std.mem.alignForward(usize, self.index, @as(usize, 1) << @intCast(ptr_align));
if(start + len >= self.buffer.len) return self.backingAllocator.rawAlloc(len, ptr_align, ret_addr);
self.allocationList.append(.{.start = @intCast(start), .len = @intCast(len)}) catch return null;
self.index = start + len;
return self.buffer.ptr + start;
}
/// Attempt to expand or shrink memory in place. `buf.len` must equal the
/// length requested from the most recent successful call to `alloc` or
/// `resize`. `buf_align` must equal the same value that was passed as the
/// `ptr_align` parameter to the original `alloc` call.
///
/// A result of `true` indicates the resize was successful and the
/// allocation now has the same address but a size of `new_len`. `false`
/// indicates the resize could not be completed without moving the
/// allocation to a different address.
///
/// `new_len` must be greater than zero.
///
/// `ret_addr` is optionally provided as the first return address of the
/// allocation call stack. If the value is `0` it means no return address
/// has been provided.
fn resize(ctx: *anyopaque, buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool {
const self: *StackAllocator = @ptrCast(@alignCast(ctx));
if(self.isInsideBuffer(buf)) {
const top = &self.allocationList.items[self.allocationList.items.len - 1];
std.debug.assert(top.start == self.indexInBuffer(buf)); // Can only resize the top element.
std.debug.assert(top.len == buf.len);
std.debug.assert(self.index >= top.start + top.len);
if(top.start + new_len >= self.buffer.len) {
return false;
}
self.index -= top.len;
self.index += new_len;
top.len = @intCast(new_len);
return true;
} else {
return self.backingAllocator.rawResize(buf, buf_align, new_len, ret_addr);
}
}
/// Free and invalidate a buffer.
///
/// `buf.len` must equal the most recent length returned by `alloc` or
/// given to a successful `resize` call.
///
/// `buf_align` must equal the same value that was passed as the
/// `ptr_align` parameter to the original `alloc` call.
///
/// `ret_addr` is optionally provided as the first return address of the
/// allocation call stack. If the value is `0` it means no return address
/// has been provided.
fn free(ctx: *anyopaque, buf: []u8, buf_align: u8, ret_addr: usize) void {
const self: *StackAllocator = @ptrCast(@alignCast(ctx));
if(self.isInsideBuffer(buf)) {
const top = self.allocationList.pop();
std.debug.assert(top.start == self.indexInBuffer(buf)); // Can only free the top element.
std.debug.assert(top.len == buf.len);
std.debug.assert(self.index >= top.start + top.len);
self.index = top.start;
} else {
self.backingAllocator.rawFree(buf, buf_align, ret_addr);
}
}
};
/// A simple binary heap.
/// Thread safe and blocking.
/// Expects T to have a `biggerThan(T) bool` function
pub fn BlockingMaxHeap(comptime T: type) type {
return struct {
const initialSize = 16;
size: usize,
array: []T,
waitingThreads: std.Thread.Condition,
waitingThreadCount: u32 = 0,
mutex: std.Thread.Mutex,
allocator: Allocator,
closed: bool = false,
pub fn init(allocator: Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = @This() {
.size = 0,
.array = try allocator.alloc(T, initialSize),
.waitingThreads = .{},
.mutex = .{},
.allocator = allocator,
};
return self;
}
pub fn deinit(self: *@This()) void {
self.mutex.lock();
self.closed = true;
// Wait for all waiting threads to leave before cleaning memory.
self.waitingThreads.broadcast();
while(self.waitingThreadCount != 0) {
self.mutex.unlock();
std.time.sleep(1000000);
self.mutex.lock();
}
self.mutex.unlock();
self.allocator.free(self.array);
self.allocator.destroy(self);
}
/// Moves an element from a given index down the heap, such that all children are always smaller than their parents.
fn siftDown(self: *@This(), _i: usize) void {
std.debug.assert(!self.mutex.tryLock()); // The mutex should be locked when calling this function.
var i = _i;
while(2*i + 1 < self.size) {
const biggest = if(2*i + 2 < self.size and self.array[2*i + 2].biggerThan(self.array[2*i + 1])) 2*i + 2 else 2*i + 1;
// Break if all childs are smaller.
if(self.array[i].biggerThan(self.array[biggest])) return;
// Swap it:
const local = self.array[biggest];
self.array[biggest] = self.array[i];
self.array[i] = local;
// goto the next node:
i = biggest;
}
}
/// Moves an element from a given index up the heap, such that all children are always smaller than their parents.
fn siftUp(self: *@This(), _i: usize) void {
std.debug.assert(!self.mutex.tryLock()); // The mutex should be locked when calling this function.
var i = _i;
while(i > 0) {
const parentIndex = (i - 1)/2;
if(!self.array[i].biggerThan(self.array[parentIndex])) break;
const local = self.array[parentIndex];
self.array[parentIndex] = self.array[i];
self.array[i] = local;
i = parentIndex;
}
}
/// Needs to be called after updating the priority of all elements.
pub fn updatePriority(self: *@This()) void {
self.mutex.lock();
defer self.mutex.unlock();
for(0..self.size) |i| {
self.siftUp(i);
}
}
/// Returns the i-th element in the heap. Useless for most applications.
pub fn get(self: *@This(), i: usize) ?T {
std.debug.assert(!self.mutex.tryLock()); // The mutex should be locked when calling this function.
if(i >= self.size) return null;
return self.array[i];
}
/// Adds a new element to the heap.
pub fn add(self: *@This(), elem: T) !void {
self.mutex.lock();
defer self.mutex.unlock();
if(self.size == self.array.len) {
try self.increaseCapacity(self.size*2);
}
self.array[self.size] = elem;
self.siftUp(self.size);
self.size += 1;
self.waitingThreads.signal();
}
fn removeIndex(self: *@This(), i: usize) void {
std.debug.assert(!self.mutex.tryLock()); // The mutex should be locked when calling this function.
self.size -= 1;
self.array[i] = self.array[self.size];
self.siftDown(i);
}
/// Returns the biggest element and removes it from the heap.
/// If empty blocks until a new object is added or the datastructure is closed.
pub fn extractMax(self: *@This()) !T {
self.mutex.lock();
defer self.mutex.unlock();
while(true) {
if(self.size == 0) {
self.waitingThreadCount += 1;
self.waitingThreads.wait(&self.mutex);
self.waitingThreadCount -= 1;
} else {
const ret = self.array[0];
self.removeIndex(0);
return ret;
}
if(self.closed) {
return error.Closed;
}
}
}
fn increaseCapacity(self: *@This(), newCapacity: usize) !void {
self.array = try self.allocator.realloc(self.array, newCapacity);
}
};
}
pub const ThreadPool = struct {
const Task = struct {
cachedPriority: f32,
self: *anyopaque,
vtable: *const VTable,
fn biggerThan(self: Task, other: Task) bool {
return self.cachedPriority > other.cachedPriority;
}
};
pub const VTable = struct {
getPriority: *const fn(*anyopaque) f32,
isStillNeeded: *const fn(*anyopaque) bool,
run: *const fn(*anyopaque) Allocator.Error!void,
clean: *const fn(*anyopaque) void,
};
const refreshTime: u32 = 100; // The time after which all priorities get refreshed in milliseconds.
threads: []std.Thread,
currentTasks: []std.atomic.Atomic(?*const VTable),
loadList: *BlockingMaxHeap(Task),
allocator: Allocator,
pub fn init(allocator: Allocator, threadCount: usize) !ThreadPool {
const self = ThreadPool {
.threads = try allocator.alloc(std.Thread, threadCount),
.currentTasks = try allocator.alloc(std.atomic.Atomic(?*const VTable), threadCount),
.loadList = try BlockingMaxHeap(Task).init(allocator),
.allocator = allocator,
};
for(self.threads, 0..) |*thread, i| {
thread.* = try std.Thread.spawn(.{}, run, .{self, i});
var buf: [64]u8 = undefined;
thread.setName(try std.fmt.bufPrint(&buf, "Worker {}", .{i+1})) catch |err| std.log.err("Couldn't rename thread: {s}", .{@errorName(err)});
}
return self;
}
pub fn deinit(self: ThreadPool) void {
// Clear the remaining tasks:
self.loadList.mutex.lock();
for(self.loadList.array[0..self.loadList.size]) |task| {
task.vtable.clean(task.self);
}
self.loadList.mutex.unlock();
self.loadList.deinit();
for(self.threads) |thread| {
thread.join();
}
self.allocator.free(self.currentTasks);
self.allocator.free(self.threads);
}
pub fn closeAllTasksOfType(self: ThreadPool, vtable: *const VTable) void {
self.loadList.mutex.lock();
defer self.loadList.mutex.unlock();
var i: u32 = 0;
while(i < self.loadList.size) {
const task = &self.loadList.array[i];
if(task.vtable == vtable) {
task.vtable.clean(task.self);
self.loadList.removeIndex(i);
} else {
i += 1;
}
}
// Wait for active tasks:
for(self.currentTasks) |*task| {
while(task.load(.Monotonic) == vtable) {
std.time.sleep(1e6);
}
}
}
fn run(self: ThreadPool, id: usize) !void {
// In case any of the tasks wants to allocate memory:
var sta = try StackAllocator.init(main.globalAllocator, 1 << 23);
defer sta.deinit();
main.stackAllocator = sta.allocator();
var lastUpdate = std.time.milliTimestamp();
while(true) {
{
const task = self.loadList.extractMax() catch break;
self.currentTasks[id].store(task.vtable, .Monotonic);
try task.vtable.run(task.self);
self.currentTasks[id].store(null, .Monotonic);
}
if(std.time.milliTimestamp() -% lastUpdate > refreshTime) {
if(self.loadList.mutex.tryLock()) {
lastUpdate = std.time.milliTimestamp();
{
defer self.loadList.mutex.unlock();
var i: u32 = 0;
while(i < self.loadList.size) {
const task = &self.loadList.array[i];
if(!task.vtable.isStillNeeded(task.self)) {
task.vtable.clean(task.self);
self.loadList.removeIndex(i);
} else {
task.cachedPriority = task.vtable.getPriority(task.self);
i += 1;
}
}
}
self.loadList.updatePriority();
}
}
}
}
pub fn addTask(self: ThreadPool, task: *anyopaque, vtable: *const VTable) !void {
try self.loadList.add(Task {
.cachedPriority = vtable.getPriority(task),
.vtable = vtable,
.self = task,
});
}
pub fn clear(self: ThreadPool) void {
// Clear the remaining tasks:
self.loadList.mutex.lock();
for(self.loadList.array[0..self.loadList.size]) |task| {
task.vtable.clean(task.self);
}
self.loadList.size = 0;
self.loadList.mutex.unlock();
// Wait for the in-progress tasks to finish:
while(true) {
if(self.loadList.mutex.tryLock()) {
defer self.loadList.mutex.unlock();
if(self.loadList.waitingThreadCount == self.threads.len) {
break;
}
}
std.time.sleep(1000000);
}
}
pub fn queueSize(self: ThreadPool) usize {
self.loadList.mutex.lock();
defer self.loadList.mutex.unlock();
return self.loadList.size;
}
};
/// Implements a simple set associative cache with LRU replacement strategy.
pub fn Cache(comptime T: type, comptime numberOfBuckets: u32, comptime bucketSize: u32, comptime deinitFunction: fn(*T) void) type {
const hashMask = numberOfBuckets-1;
if(numberOfBuckets & hashMask != 0) @compileError("The number of buckets should be a power of 2!");
const Bucket = struct {
mutex: std.Thread.Mutex = .{},
items: [bucketSize]?*T = [_]?*T {null} ** bucketSize,
fn find(self: *@This(), compare: anytype) ?*T {
std.debug.assert(!self.mutex.tryLock()); // The mutex must be locked.
for(self.items, 0..) |item, i| {
if(compare.equals(item)) {
if(i != 0) {
std.mem.copyBackwards(?*T, self.items[1..], self.items[0..i]);
self.items[0] = item;
}
return item;
}
}
return null;
}
/// Returns the object that got kicked out of the cache. This must be deinited by the user.
fn add(self: *@This(), item: *T) ?*T {
std.debug.assert(!self.mutex.tryLock()); // The mutex must be locked.
const previous = self.items[bucketSize - 1];
std.mem.copyBackwards(?*T, self.items[1..], self.items[0..bucketSize - 1]);
self.items[0] = item;
return previous;
}
fn findOrCreate(self: *@This(), compare: anytype, comptime initFunction: fn(@TypeOf(compare)) Allocator.Error!*T) Allocator.Error!*T {
std.debug.assert(!self.mutex.tryLock()); // The mutex must be locked.
if(self.find(compare)) |item| {
return item;
}
const new = try initFunction(compare);
if(self.add(new)) |toRemove| {
deinitFunction(toRemove);
}
return new;
}
fn clear(self: *@This()) void {
self.mutex.lock();
defer self.mutex.unlock();
for(&self.items) |*nullItem| {
if(nullItem.*) |item| {
deinitFunction(item);
nullItem.* = null;
}
}
}
fn foreach(self: @This(), comptime function: fn(*T) void) void {
self.mutex.lock();
defer self.mutex.unlock();
for(self.items) |*nullItem| {
if(nullItem) |item| {
function(item);
}
}
}
};
return struct {
buckets: [numberOfBuckets]Bucket = [_]Bucket {Bucket{}} ** numberOfBuckets,
cacheRequests: Atomic(usize) = Atomic(usize).init(0),
cacheMisses: Atomic(usize) = Atomic(usize).init(0),
/// Tries to find the entry that fits to the supplied hashable.
pub fn find(self: *@This(), compareAndHash: anytype) ?*T {
const index: u32 = compareAndHash.hashCode() & hashMask;
_ = @atomicRmw(usize, &self.cacheRequests.value, .Add, 1, .Monotonic);
self.buckets[index].mutex.lock();
defer self.buckets[index].mutex.unlock();
if(self.buckets[index].find(compareAndHash)) |item| {
return item;
}
_ = @atomicRmw(usize, &self.cacheMisses.value, .Add, 1, .Monotonic);
return null;
}
/// Clears all elements calling the deinitFunction for each element.
pub fn clear(self: *@This()) void {
for(&self.buckets) |*bucket| {
bucket.clear();
}
}
pub fn foreach(self: *@This(), comptime function: fn(*T) void) void {
for(&self.buckets) |*bucket| {
bucket.foreach(function);
}
}
/// Returns the object that got kicked out of the cache. This must be deinited by the user.
pub fn addToCache(self: *@This(), item: *T, hash: u32) ?*T {
const index = hash & hashMask;
self.buckets[index].mutex.lock();
defer self.buckets[index].mutex.unlock();
return self.buckets[index].add(item);
}
pub fn findOrCreate(self: *@This(), compareAndHash: anytype, comptime initFunction: fn(@TypeOf(compareAndHash)) Allocator.Error!*T) Allocator.Error!*T {
const index: u32 = compareAndHash.hashCode() & hashMask;
self.buckets[index].mutex.lock();
defer self.buckets[index].mutex.unlock();
return try self.buckets[index].findOrCreate(compareAndHash, initFunction);
}
};
}
/// https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Unit_interval_(0,_1)
pub fn unitIntervalSpline(comptime Float: type, p0: Float, m0: Float, p1: Float, m1: Float) [4]Float {
return .{
p0,
m0,
-3*p0 - 2*m0 + 3*p1 - m1,
2*p0 + m0 - 2*p1 + m1,
};
}
pub fn GenericInterpolation(comptime elements: comptime_int) type {
const frames: u32 = 8;
return struct {
lastPos: [frames][elements]f64,
lastVel: [frames][elements]f64,
lastTimes: [frames]i16,
frontIndex: u32,
currentPoint: ?u31,
outPos: *[elements]f64,
outVel: *[elements]f64,
pub fn init(self: *@This(), initialPosition: *[elements]f64, initialVelocity: *[elements]f64) void {
self.outPos = initialPosition;
self.outVel = initialVelocity;
@memset(&self.lastPos, self.outPos.*);
@memset(&self.lastVel, self.outVel.*);
self.frontIndex = 0;
self.currentPoint = null;
}
pub fn updatePosition(self: *@This(), pos: *const [elements]f64, vel: *const [elements]f64, time: i16) void {
self.frontIndex = (self.frontIndex + 1)%frames;
@memcpy(&self.lastPos[self.frontIndex], pos);
@memcpy(&self.lastVel[self.frontIndex], vel);
self.lastTimes[self.frontIndex] = time;
}
fn evaluateSplineAt(_t: f64, tScale: f64, p0: f64, _m0: f64, p1: f64, _m1: f64) [2]f64 {
// https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Unit_interval_(0,_1)
const t = _t/tScale;
const m0 = _m0*tScale;
const m1 = _m1*tScale;
const t2 = t*t;
const t3 = t2*t;
const a = unitIntervalSpline(f64, p0, m0, p1, m1);
return [_]f64 {
a[0] + a[1]*t + a[2]*t2 + a[3]*t3, // value
(a[1] + 2*a[2]*t + 3*a[3]*t2)/tScale, // first derivative
};
}
fn interpolateCoordinate(self: *@This(), i: usize, t: f64, tScale: f64) void {
if(self.outVel[i] == 0 and self.lastVel[self.currentPoint.?][i] == 0) {
self.outPos[i] += (self.lastPos[self.currentPoint.?][i] - self.outPos[i])*t/tScale;
} else {
// Use cubic interpolation to interpolate the velocity as well.
const newValue = evaluateSplineAt(t, tScale, self.outPos[i], self.outVel[i], self.lastPos[self.currentPoint.?][i], self.lastVel[self.currentPoint.?][i]);
self.outPos[i] = newValue[0];
self.outVel[i] = newValue[1];
}
}
fn determineNextDataPoint(self: *@This(), time: i16, lastTime: *i16) void {
if(self.currentPoint != null and self.lastTimes[self.currentPoint.?] -% time <= 0) {
// Jump to the last used value and adjust the time to start at that point.
lastTime.* = self.lastTimes[self.currentPoint.?];
@memcpy(self.outPos, &self.lastPos[self.currentPoint.?]);
@memcpy(self.outVel, &self.lastVel[self.currentPoint.?]);
self.currentPoint = null;
}
if(self.currentPoint == null) {
// Need a new point:
var smallestTime: i16 = std.math.maxInt(i16);
var smallestIndex: ?u31 = null;
for(self.lastTimes, 0..) |lastTimeI, i| {
// ↓ Only using a future time value that is far enough away to prevent jumping.
if(lastTimeI -% time >= 50 and lastTimeI -% time < smallestTime) {
smallestTime = lastTimeI -% time;
smallestIndex = @intCast(i);
}
}
self.currentPoint = smallestIndex;
}
}
pub fn update(self: *@This(), time: i16, _lastTime: i16) void {
var lastTime = _lastTime;
self.determineNextDataPoint(time, &lastTime);
var deltaTime = @as(f64, @floatFromInt(time -% lastTime))/1000;
if(deltaTime < 0) {
std.log.err("Experienced time travel. Current time: {} Last time: {}", .{time, lastTime});
deltaTime = 0;
}
if(self.currentPoint == null) {
const drag = std.math.pow(f64, 0.5, deltaTime);
for(self.outPos, self.outVel) |*pos, *vel| {
// Just move on with the current velocity.
pos.* += (vel.*)*deltaTime;
// Add some drag to prevent moving far away on short connection loss.
vel.* *= drag;
}
} else {
const tScale = @as(f64, @floatFromInt(self.lastTimes[self.currentPoint.?] -% lastTime))/1000;
const t = deltaTime;
for(self.outPos, 0..) |_, i| {
self.interpolateCoordinate(i, t, tScale);
}
}
}
pub fn updateIndexed(self: *@This(), time: i16, _lastTime: i16, indices: []u16, comptime coordinatesPerIndex: comptime_int) void {
var lastTime = _lastTime;
self.determineNextDataPoint(time, &lastTime);
var deltaTime = @as(f64, @floatFromInt(time -% lastTime))/1000;
if(deltaTime < 0) {
std.log.err("Experienced time travel. Current time: {} Last time: {}", .{time, lastTime});
deltaTime = 0;
}
if(self.currentPoint == null) {
const drag = std.math.pow(f64, 0.5, deltaTime);
for(indices) |i| {
const index = @as(usize, i)*coordinatesPerIndex;
for(0..coordinatesPerIndex) |j| {
// Just move on with the current velocity.
self.outPos[index + j] += self.outVel[index + j]*deltaTime;
// Add some drag to prevent moving far away on short connection loss.
self.outVel[index + j] *= drag;
}
}
} else {
const tScale = @as(f64, @floatFromInt(self.lastTimes[self.currentPoint.?] -% lastTime))/1000;
const t = deltaTime;
for(indices) |i| {
const index = @as(usize, i)*coordinatesPerIndex;
for(0..coordinatesPerIndex) |j| {
self.interpolateCoordinate(index + j, t, tScale);
}
}
}
}
};
}
pub const TimeDifference = struct {
difference: Atomic(i16) = Atomic(i16).init(0),
firstValue: bool = true,
pub fn addDataPoint(self: *TimeDifference, time: i16) void {
const currentTime: i16 = @truncate(std.time.milliTimestamp());
const timeDifference = currentTime -% time;
if(self.firstValue) {
self.difference.store(timeDifference, .Monotonic);
self.firstValue = false;
}
if(timeDifference -% self.difference.load(.Monotonic) > 0) {
_ = @atomicRmw(i16, &self.difference.value, .Add, 1, .Monotonic);
} else if(timeDifference -% self.difference.load(.Monotonic) < 0) {
_ = @atomicRmw(i16, &self.difference.value, .Add, -1, .Monotonic);
}
}
};