giga optimized floodfill

This commit is contained in:
TechnoDraconic
2026-01-29 21:50:07 -08:00
parent 351e1a806a
commit a1a24b8506
2 changed files with 105 additions and 22 deletions

View File

@@ -1,12 +1,19 @@
package net.xevianlight.aphelion.block.entity.custom; package net.xevianlight.aphelion.block.entity.custom;
import it.unimi.dsi.fastutil.longs.Long2ObjectArrayMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.core.SectionPos;
import net.minecraft.core.Vec3i; import net.minecraft.core.Vec3i;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import static net.xevianlight.aphelion.Aphelion.LOGGER; import static net.xevianlight.aphelion.Aphelion.LOGGER;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunk;
import net.xevianlight.aphelion.core.init.ModBlockEntities; import net.xevianlight.aphelion.core.init.ModBlockEntities;
import org.openjdk.nashorn.internal.runtime.regexp.joni.exception.ValueException;
import java.util.*; import java.util.*;
@@ -17,13 +24,86 @@ public class OxygenTestBlockEntity extends BlockEntity {
super(ModBlockEntities.OXYGEN_TEST_BLOCK_ENTITY.get(), pos, blockState); super(ModBlockEntities.OXYGEN_TEST_BLOCK_ENTITY.get(), pos, blockState);
} }
public boolean canSpreadTo(BlockPos pos) { private LevelChunk lastChunk;
return level != null && level.getBlockState(pos).isAir(); private int lastChunkX = Integer.MIN_VALUE, lastChunkZ = Integer.MIN_VALUE;
public BlockState fastBlockState(BlockPos pos) {
int cx = pos.getX() >> 4;
int cz = pos.getZ() >> 4;
if (cx != lastChunkX || cz != lastChunkZ) {
lastChunk = level.getChunk(cx, cz);
lastChunkX = cx;
lastChunkZ = cz;
}
return lastChunk.getBlockState(pos);
} }
public static final int MAX_RANGE = 8; public boolean canSpreadTo(BlockPos pos) {
public boolean isInRange(BlockPos pos, int radius) { return level != null && fastBlockState(pos).isAir();
return radius <= MAX_RANGE; }
public static final int MAX_RANGE = 100;
public boolean isInRange(BlockPos pos1, BlockPos pos2) {
return Math.abs(pos1.getX() - pos2.getX()) + Math.abs(pos1.getY() - pos2.getY()) + Math.abs(pos1.getZ() - pos2.getZ()) <= MAX_RANGE;
}
/// 256*256*256 grid of booleans
private class BigBoolGrid {
int bitsSize;
int size;
private int[] _grid;
int xOff, yOff, zOff;
/// Note that the ACTUAL size is 2^bitsSize, so 128 for example would be 7
/// max bitsSize is 12(?)
BigBoolGrid (int bitsSize, int xOrigin, int yOrigin, int zOrigin) {
if (Integer.bitCount(bitsSize) != 1) throw new ValueException("Grid size " + bitsSize + " is not a power of two!");
this.bitsSize = bitsSize;
this.size = (1 << bitsSize);
if (this.size < 8) throw new ValueException("Grid size is too small!");
_grid = new int[size * size * size / 8];
xOff = -xOrigin;
yOff = -yOrigin;
zOff = -zOrigin;
}
public int getArrayPos(int x, int y, int z) {
return 0;
}
public int getBit(int x) {
return 0;
}
// Returns the previous value at this location
public boolean add(int x, int y, int z) {
final int inX, inY, inZ;
inX = x + xOff + size / 2;
inY = y + yOff + size / 2;
inZ = z + zOff + size / 2;
if (inX >= size || inX < 0) throw new ValueException("Coordinate X out of range");
if (inY >= size || inY < 0) throw new ValueException("Coordinate Y out of range");
if (inZ >= size || inZ < 0) throw new ValueException("Coordinate Z out of range");
// the bytes are really just packed groups of 8 booleans,
// "laid across" the X direction.
// wrapping order is X->Z->Y, so we go along the X axis,
// wrap to the Z axis to make a square, and once the square is full
// we step up once along the Y axis.
int bitPos = inX & 7; // bottom 4 bits of X is bit pos
int bit = (1 << bitPos);
// First (bitsSize-4) bits are for X, next (bitsSize) bits are for Z, next (bitsSize) bits are for Y
int arrayPos = (inX >>> 4) + (inZ << (bitsSize-4)) + (inY << (bitsSize*2-4));
boolean out = !((_grid[arrayPos] & bit) > 0);
_grid[arrayPos] = _grid[arrayPos] | bit;
return out;
}
} }
private List<BlockPos> enclosedCache; private List<BlockPos> enclosedCache;
@@ -31,16 +111,17 @@ public class OxygenTestBlockEntity extends BlockEntity {
if (level == null) return List.of(); if (level == null) return List.of();
if (enclosedCache != null) return enclosedCache; if (enclosedCache != null) return enclosedCache;
long start = System.nanoTime();
List<BlockPos> enclosedBlocks = new ArrayList<>(); List<BlockPos> enclosedBlocks = new ArrayList<>();
Set<BlockPos> seen = new HashSet<>(); // make this bitch BIIIIIG
BigBoolGrid seen = new BigBoolGrid(8, this.getBlockPos().getX(), this.getBlockPos().getY(), this.getBlockPos().getZ());
// It's... a reasonable assumption that we won't have to include more blocks at once than the area of a sphere? // It's... a reasonable assumption that we won't have to include more blocks at once than the area of a sphere?
// maybe a bit more, IDK how exactly it scales to blocks. // maybe a bit more, IDK how exactly it scales to blocks.
Deque<BlockPos> queue = new ArrayDeque<>((int)(4 * Math.PI * MAX_RANGE * MAX_RANGE)); Stack<BlockPos> stack = new Stack<>();
Deque<Integer> radiusQueue = new ArrayDeque<>((int)(4 * Math.PI * MAX_RANGE * MAX_RANGE)); Stack<Integer> radiusStack = new Stack<>();
queue.add(this.getBlockPos()); stack.add(this.getBlockPos());
radiusQueue.add(0);
// Do flood fill out from this block // Do flood fill out from this block
// Push on the top of the stack (newest), pop from the bottom of the stack (oldest). // Push on the top of the stack (newest), pop from the bottom of the stack (oldest).
@@ -48,26 +129,27 @@ public class OxygenTestBlockEntity extends BlockEntity {
// by how many "layers deep" it is (i.e. how many steps it took to get there), // by how many "layers deep" it is (i.e. how many steps it took to get there),
// and you'd see that every pos of layer 1 is together, then layer 2, then layer 3... // and you'd see that every pos of layer 1 is together, then layer 2, then layer 3...
long start = System.nanoTime();
while (!queue.isEmpty()) { BlockPos ourPos = getBlockPos();
BlockPos spreadFromPos = queue.removeFirst(); while (!stack.isEmpty()) {
int radius = radiusQueue.removeFirst(); BlockPos spreadFromPos = stack.pop();
for (Direction d : Direction.values()) { for (Direction d : Direction.values()) {
BlockPos relativePos = spreadFromPos.relative(d); BlockPos relativePos = spreadFromPos.relative(d);
if (seen.contains(relativePos)) continue;
if (canSpreadTo(relativePos) && isInRange(relativePos, radius)) { if (isInRange(relativePos, ourPos) && canSpreadTo(relativePos)) {
seen.add(relativePos); // seen.add runs seen.contains under the hood,
// + this is the most expensive operation.
// should save a lot of time!
if (seen.add(relativePos.getX(), relativePos.getY(), relativePos.getZ())) {
enclosedBlocks.add(relativePos); enclosedBlocks.add(relativePos);
stack.add(relativePos);
queue.add(relativePos); }
radiusQueue.add(radius + 1);
} }
} }
} }
long durationNanos = System.nanoTime() - start; long durationNanos = System.nanoTime() - start;
double durationMicros = durationNanos / 1000.0d; double durationMicros = durationNanos / 1000.0d;
LOGGER.info("Flood fill completed in {}µs, {}µs/block", durationMicros, durationMicros / enclosedBlocks.size()); LOGGER.info("Flood fill completed in {}µs, {} blocks at {}µs/block", durationMicros, enclosedBlocks.size(), durationMicros / enclosedBlocks.size());
enclosedCache = enclosedBlocks; enclosedCache = enclosedBlocks;
return enclosedBlocks; return enclosedBlocks;

View File

@@ -42,6 +42,7 @@ public class OxygenTestRenderer implements BlockEntityRenderer<OxygenTestBlockEn
// Renderers are relative to our block pos, so transform all points to be relative to block pos as well // Renderers are relative to our block pos, so transform all points to be relative to block pos as well
List<BlockPos> positionsToRender = toBlockPositions(be); List<BlockPos> positionsToRender = toBlockPositions(be);
BlockPos originPos = be.getBlockPos(); BlockPos originPos = be.getBlockPos();
if (true) return;
Set<BlockPos> relativePositions; Set<BlockPos> relativePositions;
if (relativePositionsCache != null) relativePositions = relativePositionsCache; if (relativePositionsCache != null) relativePositions = relativePositionsCache;