Use a CircularBufferQueue for lighting instead of a PriorityQueue.

A CircularBufferQueue means that we might revisit some points, for example when there is a dim and a bright light source.
However in practice the performance impact of using a PriorityQueue is huge, so this edge case doesn't matter really.

This makes light propagation about 20 times faster and should help with #223, but it doesn't seem to actually make a difference. There is likely something else going on here.
This commit is contained in:
IntegratedQuantum 2023-12-07 16:58:50 +01:00
parent 048b48e5c7
commit 451cac2923
2 changed files with 61 additions and 14 deletions

View File

@ -40,15 +40,9 @@ pub const ChannelChunk = struct {
y: u5, y: u5,
z: u5, z: u5,
value: u8, value: u8,
fn compare(_: void, a: @This(), b: @This()) std.math.Order {
if (a.value > b.value) return .gt;
if (a.value < b.value) return .lt;
return .eq;
}
}; };
fn propagateDirect(self: *ChannelChunk, lightQueue: *std.PriorityQueue(Entry, void, Entry.compare)) std.mem.Allocator.Error!void { noinline fn propagateDirect(self: *ChannelChunk, lightQueue: *main.utils.CircularBufferQueue(Entry)) std.mem.Allocator.Error!void {
var neighborLists: [6]std.ArrayListUnmanaged(Entry) = .{.{}} ** 6; var neighborLists: [6]std.ArrayListUnmanaged(Entry) = .{.{}} ** 6;
defer { defer {
for(&neighborLists) |*list| { for(&neighborLists) |*list| {
@ -58,7 +52,7 @@ pub const ChannelChunk = struct {
self.mutex.lock(); self.mutex.lock();
errdefer self.mutex.unlock(); errdefer self.mutex.unlock();
while(lightQueue.removeOrNull()) |entry| { while(lightQueue.dequeue()) |entry| {
const index = chunk.getIndex(entry.x, entry.y, entry.z); const index = chunk.getIndex(entry.x, entry.y, entry.z);
if(entry.value <= self.data[index].load(.Unordered)) continue; if(entry.value <= self.data[index].load(.Unordered)) continue;
self.data[index].store(entry.value, .Unordered); self.data[index].store(entry.value, .Unordered);
@ -77,7 +71,7 @@ pub const ChannelChunk = struct {
var absorption: u8 = @intCast(self.ch.blocks[neighborIndex].absorption() >> self.channel.shift() & 255); var absorption: u8 = @intCast(self.ch.blocks[neighborIndex].absorption() >> self.channel.shift() & 255);
absorption *|= @intCast(self.ch.pos.voxelSize); absorption *|= @intCast(self.ch.pos.voxelSize);
result.value -|= absorption; result.value -|= absorption;
if(result.value != 0) try lightQueue.add(result); if(result.value != 0) try lightQueue.enqueue(result);
} }
} }
self.mutex.unlock(); self.mutex.unlock();
@ -97,7 +91,7 @@ pub const ChannelChunk = struct {
} }
fn propagateFromNeighbor(self: *ChannelChunk, lights: []const Entry) std.mem.Allocator.Error!void { fn propagateFromNeighbor(self: *ChannelChunk, lights: []const Entry) std.mem.Allocator.Error!void {
var lightQueue = std.PriorityQueue(Entry, void, Entry.compare).init(main.globalAllocator, {}); var lightQueue = try main.utils.CircularBufferQueue(Entry).init(main.globalAllocator, 1 << 8);
defer lightQueue.deinit(); defer lightQueue.deinit();
for(lights) |entry| { for(lights) |entry| {
const index = chunk.getIndex(entry.x, entry.y, entry.z); const index = chunk.getIndex(entry.x, entry.y, entry.z);
@ -105,17 +99,17 @@ pub const ChannelChunk = struct {
var absorption: u8 = @intCast(self.ch.blocks[index].absorption() >> self.channel.shift() & 255); var absorption: u8 = @intCast(self.ch.blocks[index].absorption() >> self.channel.shift() & 255);
absorption *|= @intCast(self.ch.pos.voxelSize); absorption *|= @intCast(self.ch.pos.voxelSize);
result.value -|= absorption; result.value -|= absorption;
if(result.value != 0) try lightQueue.add(result); if(result.value != 0) try lightQueue.enqueue(result);
} }
try self.propagateDirect(&lightQueue); try self.propagateDirect(&lightQueue);
} }
pub fn propagateLights(self: *ChannelChunk, lights: []const [3]u8, comptime checkNeighbors: bool) std.mem.Allocator.Error!void { pub fn propagateLights(self: *ChannelChunk, lights: []const [3]u8, comptime checkNeighbors: bool) std.mem.Allocator.Error!void {
var lightQueue = std.PriorityQueue(Entry, void, Entry.compare).init(main.globalAllocator, {}); var lightQueue = try main.utils.CircularBufferQueue(Entry).init(main.globalAllocator, 1 << 8);
defer lightQueue.deinit(); defer lightQueue.deinit();
for(lights) |pos| { for(lights) |pos| {
const index = chunk.getIndex(pos[0], pos[1], pos[2]); const index = chunk.getIndex(pos[0], pos[1], pos[2]);
try lightQueue.add(.{.x = @intCast(pos[0]), .y = @intCast(pos[1]), .z = @intCast(pos[2]), .value = @intCast(self.ch.blocks[index].light() >> self.channel.shift() & 255)}); try lightQueue.enqueue(.{.x = @intCast(pos[0]), .y = @intCast(pos[1]), .z = @intCast(pos[2]), .value = @intCast(self.ch.blocks[index].light() >> self.channel.shift() & 255)});
} }
if(checkNeighbors) { if(checkNeighbors) {
for(0..6) |neighbor| { for(0..6) |neighbor| {
@ -154,7 +148,7 @@ pub const ChannelChunk = struct {
var absorption: u8 = @intCast(self.ch.blocks[index].absorption() >> self.channel.shift() & 255); var absorption: u8 = @intCast(self.ch.blocks[index].absorption() >> self.channel.shift() & 255);
absorption *|= @intCast(self.ch.pos.voxelSize); absorption *|= @intCast(self.ch.pos.voxelSize);
value -|= absorption; value -|= absorption;
if(value != 0) try lightQueue.add(.{.x = @intCast(x), .y = @intCast(y), .z = @intCast(z), .value = value}); if(value != 0) try lightQueue.enqueue(.{.x = @intCast(x), .y = @intCast(y), .z = @intCast(z), .value = value});
} }
} }
} }

View File

@ -317,6 +317,59 @@ pub fn Array3D(comptime T: type) type {
}; };
} }
pub fn CircularBufferQueue(comptime T: type) type {
return struct {
const Self = @This();
mem: []T,
mask: usize,
startIndex: usize,
endIndex: usize,
allocator: Allocator,
pub fn init(allocator: Allocator, initialCapacity: usize) !Self {
comptime std.debug.assert(@sizeOf(Self) <= 64);
std.debug.assert(initialCapacity-1 & initialCapacity == 0 and initialCapacity > 0);
return .{
.mem = try allocator.alloc(T, initialCapacity),
.mask = initialCapacity-1,
.startIndex = 0,
.endIndex = 0,
.allocator = allocator,
};
}
pub fn deinit(self: Self) void {
self.allocator.free(self.mem);
}
fn increaseCapacity(self: *Self) !void {
const newMem = try self.allocator.alloc(T, self.mem.len*2);
@memcpy(newMem[0..(self.mem.len - self.startIndex)], self.mem[self.startIndex..]);
@memcpy(newMem[(self.mem.len - self.startIndex)..][0..self.endIndex], self.mem[0..self.endIndex]);
self.startIndex = 0;
self.endIndex = self.mem.len;
self.allocator.free(self.mem);
self.mem = newMem;
self.mask = self.mem.len - 1;
}
pub fn enqueue(self: *Self, elem: T) !void {
self.mem[self.endIndex] = elem;
self.endIndex = (self.endIndex + 1) & self.mask;
if(self.endIndex == self.startIndex) {
try self.increaseCapacity();
}
}
pub fn dequeue(self: *Self) ?T {
if(self.startIndex == self.endIndex) return null;
const result = self.mem[self.startIndex];
self.startIndex = (self.startIndex + 1) & self.mask;
return result;
}
};
}
/// Allows for stack-like allocations in a fast and safe way. /// 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. /// It is safe in the sense that a regular allocator will be used when the buffer is full.
pub const StackAllocator = struct { pub const StackAllocator = struct {