Add the ability to insert ranges of equal blocks into the palette compressed data structure.

The index is then only calculated once. Individual insertions are not optimized though.

fixes #600

Also it seems that in my previous performance commit I had made a mistake in my measurement, I must have measured a buggy version which turned out to be super fast. In actuality the performance difference was much smaller, so I decided to optimize that section as well.

Now it's at 1.7 ms per chunk. Still needs more improvements, I guess.
This commit is contained in:
IntegratedQuantum 2024-08-01 17:30:44 +02:00
parent c3711868c5
commit 4fa08bec95
4 changed files with 137 additions and 68 deletions

View File

@ -385,6 +385,20 @@ pub const ServerChunk = struct { // MARK: ServerChunk
self.super.data.setValue(index, newBlock);
}
/// Updates a block if it is inside this chunk. Should be used in generation to prevent accidently storing these as changes.
/// Does not do any bound checks. They are expected to be done with the `liesInChunk` function.
pub fn updateBlockColumnInGeneration(self: *ServerChunk, _x: i32, _y: i32, _zStartInclusive: i32, _zEndInclusive: i32, newBlock: Block) void {
std.debug.assert(_zStartInclusive <= _zEndInclusive);
main.utils.assertLocked(&self.mutex);
const x = _x >> self.super.voxelSizeShift;
const y = _y >> self.super.voxelSizeShift;
const zStartInclusive = _zStartInclusive >> self.super.voxelSizeShift;
const zEndInclusive = _zEndInclusive >> self.super.voxelSizeShift;
const indexStart = getIndex(x, y, zStartInclusive);
const indexEnd = getIndex(x, y, zEndInclusive) + 1;
self.super.data.setValueInColumn(indexStart, indexEnd, newBlock);
}
pub fn updateFromLowerResolution(self: *ServerChunk, other: *ServerChunk) void {
const xOffset = if(other.super.pos.wx != self.super.pos.wx) chunkSize/2 else 0; // Offsets of the lower resolution chunk in this chunk.
const yOffset = if(other.super.pos.wy != self.super.pos.wy) chunkSize/2 else 0;

View File

@ -46,7 +46,7 @@ pub const CaveBiomeMapFragment = struct { // MARK: caveBiomeMapFragment
@Vector(3, i64){-12*fac, 15*fac, 16*fac},
}; // divide result by shift to do a proper rotation
const inverseRotationMatrix = .{
const transposeRotationMatrix = .{
@Vector(3, i64){20*fac, 9*fac, -12*fac},
@Vector(3, i64){ 0*fac, 20*fac, 15*fac},
@Vector(3, i64){15*fac, -12*fac, 16*fac},
@ -62,9 +62,9 @@ pub const CaveBiomeMapFragment = struct { // MARK: caveBiomeMapFragment
pub fn rotateInverse(in: Vec3i) Vec3i {
return @truncate(@Vector(3, i64){
vec.dot(inverseRotationMatrix[0], in) >> rotationMatrixShift,
vec.dot(inverseRotationMatrix[1], in) >> rotationMatrixShift,
vec.dot(inverseRotationMatrix[2], in) >> rotationMatrixShift,
vec.dot(transposeRotationMatrix[0], in) >> rotationMatrixShift,
vec.dot(transposeRotationMatrix[1], in) >> rotationMatrixShift,
vec.dot(transposeRotationMatrix[2], in) >> rotationMatrixShift,
});
}
@ -402,15 +402,14 @@ pub const InterpolatableCaveBiomeMapView = struct { // MARK: InterpolatableCaveB
return frag.biomeMap[indexInArray][map];
}
fn getGridPoint(pos: Vec3i, map: *u1) Vec3i {
const rotatedPos = CaveBiomeMapFragment.rotate(pos);
fn getGridPointFromPrerotated(rotatedPos: Vec3i, map: *u1) Vec3i {
var gridPoint = rotatedPos +% @as(Vec3i, @splat(CaveBiomeMapFragment.caveBiomeSize/2)) & @as(Vec3i, @splat(~@as(i32, CaveBiomeMapFragment.caveBiomeMask)));
const distance = rotatedPos -% gridPoint;
const totalDistance = @reduce(.Add, @abs(distance));
if(totalDistance > CaveBiomeMapFragment.caveBiomeSize*3/4) {
// Or with 1 to prevent errors if the value is 0.
gridPoint +%= (std.math.sign(distance | @as(Vec3i, @splat(1)) - @as(Vec3i, @splat(1))))*@as(Vec3i, @splat(CaveBiomeMapFragment.caveBiomeSize/2));
gridPoint +%= std.math.sign(distance)*@as(Vec3i, @splat(CaveBiomeMapFragment.caveBiomeSize/2));
map.* = 1;
} else {
map.* = 0;
@ -418,6 +417,42 @@ pub const InterpolatableCaveBiomeMapView = struct { // MARK: InterpolatableCaveB
return gridPoint;
}
fn getGridPoint(pos: Vec3i, map: *u1) Vec3i {
const rotatedPos = CaveBiomeMapFragment.rotate(pos);
return getGridPointFromPrerotated(rotatedPos, map);
}
fn getGridPointAndHeight(pos: Vec3i, map: *u1, returnHeight: *i32, voxelSize: u31) Vec3i {
const preRotatedPos = @Vector(3, i64){
vec.dot(CaveBiomeMapFragment.rotationMatrix[0], pos),
vec.dot(CaveBiomeMapFragment.rotationMatrix[1], pos),
vec.dot(CaveBiomeMapFragment.rotationMatrix[2], pos),
};
var startMap: u1 = undefined;
const gridPoint = getGridPointFromPrerotated(@truncate(preRotatedPos >> @splat(CaveBiomeMapFragment.rotationMatrixShift)), &startMap);
var start: i32 = 0;
var end = @min(returnHeight.*, @as(comptime_int, @intFromFloat(@ceil(CaveBiomeMapFragment.caveBiomeSize*@sqrt(5.0)/2.0)))) & ~@as(i32, voxelSize-1);
{
var otherMap: u1 = undefined;
const nextGridPoint = getGridPointFromPrerotated(@truncate(preRotatedPos +% CaveBiomeMapFragment.transposeRotationMatrix[2]*@as(Vec3i, @splat(end)) >> @splat(CaveBiomeMapFragment.rotationMatrixShift)), &otherMap);
if(@reduce(.And, nextGridPoint == gridPoint) and otherMap == startMap) start = end;
}
while(start + voxelSize < end) {
const mid = start +% @divTrunc(end -% start, 2) & ~@as(i32, voxelSize-1);
var otherMap: u1 = undefined;
const nextGridPoint = getGridPointFromPrerotated(@truncate(preRotatedPos +% CaveBiomeMapFragment.transposeRotationMatrix[2]*@as(Vec3i, @splat(mid)) >> @splat(CaveBiomeMapFragment.rotationMatrixShift)), &otherMap);
if(@reduce(.Or, nextGridPoint != gridPoint) or otherMap != startMap) {
end = mid;
} else {
start = mid;
}
}
returnHeight.* = end;
map.* = startMap;
return gridPoint;
}
/// Useful when the rough biome location is enough, for example for music.
pub noinline fn getRoughBiome(self: InterpolatableCaveBiomeMapView, wx: i32, wy: i32, wz: i32, comptime getSeed: bool, seed: *u64, comptime _checkSurfaceBiome: bool) *const Biome {
if(_checkSurfaceBiome) {
@ -444,25 +479,7 @@ pub const InterpolatableCaveBiomeMapView = struct { // MARK: InterpolatableCaveB
}
}
var map: u1 = undefined;
const gridPoint = getGridPoint(.{wx, wy, wz}, &map);
var start = wz;
var end = start +% returnHeight.*;
{
var otherMap: u1 = undefined;
const nextGridPoint = getGridPoint(.{wx, wy, end}, &otherMap);
if(@reduce(.And, nextGridPoint == gridPoint) and otherMap == map) start = end;
}
while(start + 1 < end) {
const mid = start +% @divTrunc(end -% start, 2);
var otherMap: u1 = undefined;
const nextGridPoint = getGridPoint(.{wx, wy, mid}, &otherMap);
if(@reduce(.Or, nextGridPoint != gridPoint) or otherMap != map) {
end = mid;
} else {
start = mid;
}
}
returnHeight.* = end -% wz;
const gridPoint = getGridPointAndHeight(.{wx, wy, wz}, &map, returnHeight, self.pos.voxelSize);
if(getSeed) {
// A good old "I don't know what I'm doing" hash (TODO: Use some standard hash maybe):

View File

@ -42,57 +42,75 @@ pub fn generate(worldSeed: u64, chunk: *main.chunk.ServerChunk, caveMap: CaveMap
var baseSeed: u64 = undefined;
const biome = biomeMap.getBiomeColumnAndSeed(x, y, zBiome, true, &baseSeed, &biomeHeight);
defer zBiome = chunk.startIndex(zBiome + biomeHeight - 1 + chunk.super.pos.voxelSize);
var makeSurfaceStructure = true;
var z: i32 = @min(chunk.super.width - chunk.super.pos.voxelSize, chunk.startIndex(zBiome + biomeHeight - 1));
while(z >= zBiome) : (z -= chunk.super.pos.voxelSize) {
const mask = @as(u64, 1) << @intCast(z >> voxelSizeShift);
if(heightData & mask != 0) {
if(makeSurfaceStructure) {
const surfaceBlock = caveMap.findTerrainChangeAbove(x, y, z) - chunk.super.pos.voxelSize;
var bseed: u64 = random.initSeed3D(worldSeed, .{chunk.super.pos.wx + x, chunk.super.pos.wy + y, chunk.super.pos.wz + z});
// Add the biomes surface structure:
z = @min(z + chunk.super.pos.voxelSize, biome.structure.addSubTerranian(chunk, surfaceBlock, @max(caveMap.findTerrainChangeBelow(x, y, z), zBiome - 1), x, y, &bseed));
makeSurfaceStructure = false;
} else {
var typ = biome.stoneBlockType;
var seed = baseSeed;
for (biome.stripes) |stripe| {
const pos: Vec3d = .{
@as(f64, @floatFromInt(x + chunk.super.pos.wx)),
@as(f64, @floatFromInt(y + chunk.super.pos.wy)),
@as(f64, @floatFromInt(z + chunk.super.pos.wz))
};
var d: f64 = 0;
if (stripe.direction) |direction| {
d = vec.dot(direction, pos);
} else {
const dx = main.random.nextDoubleSigned(&seed);
const dy = main.random.nextDoubleSigned(&seed);
const dz = main.random.nextDoubleSigned(&seed);
const dir: Vec3d = .{dx, dy, dz};
d = vec.dot(vec.normalize(dir), pos);
}
const distance = (stripe.maxDistance - stripe.minDistance) * main.random.nextDouble(&seed) + stripe.minDistance;
const offset = (stripe.maxOffset - stripe.minOffset) * main.random.nextDouble(&seed) + stripe.minOffset;
const width = (stripe.maxWidth - stripe.minWidth) * main.random.nextDouble(&seed) + stripe.minWidth;
if (@mod(d + offset, distance) < width) {
typ = stripe.block;
break;
const surfaceBlock = caveMap.findTerrainChangeAbove(x, y, z) - chunk.super.pos.voxelSize;
var bseed: u64 = random.initSeed3D(worldSeed, .{chunk.super.pos.wx + x, chunk.super.pos.wy + y, chunk.super.pos.wz + z});
const airBlockBelow = caveMap.findTerrainChangeBelow(x, y, z);
// Add the biomes surface structure:
z = @min(z + chunk.super.pos.voxelSize, biome.structure.addSubTerranian(chunk, surfaceBlock, @max(airBlockBelow, zBiome - 1), x, y, &bseed));
z -= chunk.super.pos.voxelSize;
if(z < zBiome) break;
if(z > airBlockBelow) {
const zMin = @max(airBlockBelow + 1, zBiome);
if(biome.stripes.len == 0) {
const typ = biome.stoneBlockType;
chunk.updateBlockColumnInGeneration(x, y, zMin, z, .{.typ = typ, .data = 0}); // TODO: Natural standard.
z = zMin;
} else {
while(z >= zMin) : (z -= chunk.super.pos.voxelSize) {
var typ = biome.stoneBlockType;
var seed = baseSeed;
for (biome.stripes) |stripe| {
const pos: Vec3d = .{
@as(f64, @floatFromInt(x + chunk.super.pos.wx)),
@as(f64, @floatFromInt(y + chunk.super.pos.wy)),
@as(f64, @floatFromInt(z + chunk.super.pos.wz))
};
var d: f64 = 0;
if (stripe.direction) |direction| {
d = vec.dot(direction, pos);
} else {
const dx = main.random.nextDoubleSigned(&seed);
const dy = main.random.nextDoubleSigned(&seed);
const dz = main.random.nextDoubleSigned(&seed);
const dir: Vec3d = .{dx, dy, dz};
d = vec.dot(vec.normalize(dir), pos);
}
const distance = (stripe.maxDistance - stripe.minDistance) * main.random.nextDouble(&seed) + stripe.minDistance;
const offset = (stripe.maxOffset - stripe.minOffset) * main.random.nextDouble(&seed) + stripe.minOffset;
const width = (stripe.maxWidth - stripe.minWidth) * main.random.nextDouble(&seed) + stripe.minWidth;
if (@mod(d + offset, distance) < width) {
typ = stripe.block;
break;
}
}
chunk.updateBlockInGeneration(x, y, z, .{.typ = typ, .data = 0}); // TODO: Natural standard.
}
z += chunk.super.pos.voxelSize;
}
chunk.updateBlockInGeneration(x, y, z, .{.typ = typ, .data = 0}); // TODO: Natural standard.
}
} else {
if(z + chunk.super.pos.wz < 0 and z + chunk.super.pos.wz >= biomeMap.getSurfaceHeight(x + chunk.super.pos.wx, y + chunk.super.pos.wy) - (chunk.super.pos.voxelSize - 1)) {
chunk.updateBlockInGeneration(x, y, z, .{.typ = water, .data = 0}); // TODO: Natural standard.
const surface = biomeMap.getSurfaceHeight(x + chunk.super.pos.wx, y + chunk.super.pos.wy) - (chunk.super.pos.voxelSize - 1) -% chunk.super.pos.wz;
const oceanHeight = 0 -% chunk.super.pos.wz;
const airVolumeStart = caveMap.findTerrainChangeBelow(x, y, z) + chunk.super.pos.voxelSize;
const zStart = @max(airVolumeStart, zBiome);
if(z < surface or zStart >= oceanHeight) {
chunk.updateBlockColumnInGeneration(x, y, zStart, z, .{.typ = 0, .data = 0});
} else {
chunk.updateBlockInGeneration(x, y, z, .{.typ = 0, .data = 0});
if(z >= oceanHeight) {
chunk.updateBlockColumnInGeneration(x, y, oceanHeight, z, .{.typ = 0, .data = 0});
z = oceanHeight - chunk.super.pos.voxelSize;
}
chunk.updateBlockColumnInGeneration(x, y, zStart, z, .{.typ = water, .data = 0}); // TODO: Natural standard.
}
makeSurfaceStructure = true;
z = zStart;
}
}
}

View File

@ -1252,7 +1252,7 @@ pub fn PaletteCompressedRegion(T: type, size: comptime_int) type { // MARK: Pale
return self.palette[self.data.getValue(i)];
}
pub fn setValue(noalias self: *Self, i: usize, val: T) void {
fn getOrInsertPaletteIndex(noalias self: *Self, val: T) u32 {
std.debug.assert(self.paletteLength <= self.palette.len);
var paletteIndex: u32 = 0;
while(paletteIndex < self.paletteLength) : (paletteIndex += 1) { // TODO: There got to be a faster way to do this. Either using SIMD or using a cache or hashmap.
@ -1272,7 +1272,11 @@ pub fn PaletteCompressedRegion(T: type, size: comptime_int) type { // MARK: Pale
self.paletteLength += 1;
std.debug.assert(self.paletteLength <= self.palette.len);
}
return paletteIndex;
}
pub fn setValue(noalias self: *Self, i: usize, val: T) void {
const paletteIndex = self.getOrInsertPaletteIndex(val);
const previousPaletteIndex = self.data.setAndGetValue(i, paletteIndex);
if(previousPaletteIndex != paletteIndex) {
if(self.paletteOccupancy[paletteIndex] == 0) {
@ -1286,6 +1290,22 @@ pub fn PaletteCompressedRegion(T: type, size: comptime_int) type { // MARK: Pale
}
}
pub fn setValueInColumn(noalias self: *Self, startIndex: usize, endIndex: usize, val: T) void {
std.debug.assert(startIndex < endIndex);
const paletteIndex = self.getOrInsertPaletteIndex(val);
for(startIndex..endIndex) |i| {
const previousPaletteIndex = self.data.setAndGetValue(i, paletteIndex);
self.paletteOccupancy[previousPaletteIndex] -= 1;
if(self.paletteOccupancy[previousPaletteIndex] == 0) {
self.activePaletteEntries -= 1;
}
}
if(self.paletteOccupancy[paletteIndex] == 0) {
self.activePaletteEntries += 1;
}
self.paletteOccupancy[paletteIndex] += @intCast(endIndex - startIndex);
}
pub fn optimizeLayout(self: *Self) void {
if(std.math.log2_int_ceil(usize, self.palette.len) == std.math.log2_int_ceil(usize, self.activePaletteEntries)) return;