Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,84 @@ private static BlockFace[] opposites() {
return opposites;
}

/**
* The index into {@link #FACES} of the opposite of every face.
* <p>
* A queued position remembers the face that points back at whoever queued it, and a face is
* cheaper to carry as its index than as a reference. This is that index.
* </p>
*/
private static final int[] OPPOSITE_INDEX = oppositeIndexes();

/**
* Resolves the index of the opposite of every face once.
*
* @return the index of the opposite of every face, indexed like {@link #FACES}
*/
private static int[] oppositeIndexes() {
int[] indexes = new int[FACES.length];

for (int index = 0; index < FACES.length; index++) {
indexes[index] = OPPOSITES[index].ordinal();
}
return indexes;
}

/**
* The amount of bits a queued position occupies, leaving the ones above it for the face.
* <p>
* A position index is {@code (y << 8) | (z << 4) | x} over a column of at most a few hundred
* blocks, so twenty-four bits carry a column of 65 536 sections — four orders of magnitude past
* anything a dimension declares.
* </p>
*/
private static final int POSITION_BITS = 24;

/**
* The bits of a queue entry which carry the position.
*/
private static final int POSITION_MASK = (1 << POSITION_BITS) - 1;

/**
* The face value of an entry which nobody queued, so no direction may be skipped for it.
* Six faces occupy the indexes zero to five, which leaves this one free.
*/
private static final int NO_FACE = 7;

/**
* The bit of the opposite of every face, ready to be tested against a flat occlusion byte.
*/
private static final int[] OPPOSITE_BIT = oppositeBits();

/**
* Resolves the occlusion bit of the opposite of every face once.
*
* @return the bit of the opposite of every face, indexed like {@link #FACES}
*/
private static int[] oppositeBits() {
int[] bits = new int[FACES.length];

for (int index = 0; index < FACES.length; index++) {
bits[index] = 1 << OPPOSITE_INDEX[index];
}
return bits;
}

/**
* The occlusion bit of the face light enters a block through when it falls straight down.
*/
private static final int TOP_BIT = 1 << BlockFace.TOP.ordinal();

private byte[] levels;
private byte[] occlusion;
private int[] queue;

/**
* Creates a new propagator without any buffer. The buffers are sized on the first run.
*/
public ChunkLightPropagator() {
this.levels = new byte[0];
this.occlusion = new byte[0];
this.queue = new int[0];
}

Expand Down Expand Up @@ -114,8 +184,16 @@ private int prepare(List<SectionOpacity> sections) {
}

int height = sections.size() * LightNibbles.DIMENSION;
ensureCapacity(height * LightNibbles.DIMENSION * LightNibbles.DIMENSION);
Arrays.fill(this.levels, 0, height * LightNibbles.DIMENSION * LightNibbles.DIMENSION, (byte) 0);
int blockCount = height * LightNibbles.DIMENSION * LightNibbles.DIMENSION;
ensureCapacity(blockCount);
Arrays.fill(this.levels, 0, blockCount, (byte) 0);

// The whole column is laid out flat once, so the search reads one array instead of walking
// list, section and null test per face per queued position. It is paid for by one fill or
// one copy per section, both of which the JIT turns into vector stores.
for (int section = 0; section < sections.size(); section++) {
sections.get(section).copyOcclusionInto(this.occlusion, section * LightNibbles.BLOCK_COUNT);
}
return height;
}

Expand All @@ -132,7 +210,9 @@ private List<LightNibbles> search(List<SectionOpacity> sections, int height, int
int head = 0;

while (head < tail) {
int index = this.queue[head++];
int entry = this.queue[head++];
int index = entry & POSITION_MASK;
int arrivedFrom = entry >>> POSITION_BITS;
int level = this.levels[index];

if (level <= 1) {
Expand All @@ -145,6 +225,12 @@ private List<LightNibbles> search(List<SectionOpacity> sections, int height, int
int next = level - 1;

for (int faceIndex = 0; faceIndex < FACES.length; faceIndex++) {
// Whoever queued this position sits on the far side of that face and already holds
// a level one higher, so the test below could never pass for it. Skipping the face
// outright is the same result for a sixth less work.
if (faceIndex == arrivedFrom) {
continue;
}
BlockFace face = FACES[faceIndex];
int neighbourX = x + face.offsetX();
int neighbourY = y + face.offsetY();
Expand All @@ -153,18 +239,20 @@ private List<LightNibbles> search(List<SectionOpacity> sections, int height, int
if (isOutside(neighbourX, neighbourY, neighbourZ, height)) {
continue;
}
if (blocksFace(sections, neighbourX, neighbourY, neighbourZ, OPPOSITES[faceIndex])) {
continue;
}

int neighbourIndex = index(neighbourX, neighbourY, neighbourZ);

// The level is one array read, the occlusion is two and a branch, and the level
// rejects far more often — a position is reached from up to six directions and only
// the first of them raises it. Cheapest and most selective test first.
if (this.levels[neighbourIndex] >= next) {
continue;
}
if ((this.occlusion[neighbourIndex] & OPPOSITE_BIT[faceIndex]) != 0) {
continue;
}
this.levels[neighbourIndex] = (byte) next;
ensureRoom(tail);
this.queue[tail++] = neighbourIndex;
this.queue[tail++] = neighbourIndex | (OPPOSITE_INDEX[faceIndex] << POSITION_BITS);
}
}
return collect(sections.size());
Expand Down Expand Up @@ -194,6 +282,7 @@ private void ensureRoom(int tail) {
private void ensureCapacity(int blockCount) {
if (this.levels.length < blockCount) {
this.levels = new byte[blockCount];
this.occlusion = new byte[blockCount];
this.queue = new int[blockCount];
}
}
Expand Down Expand Up @@ -226,7 +315,7 @@ private int seed(List<SectionOpacity> sections, int height) {
int index = index(x, y, z);
this.levels[index] = (byte) emission;
ensureRoom(tail);
this.queue[tail++] = index;
this.queue[tail++] = index | (NO_FACE << POSITION_BITS);
}
}
}
Expand All @@ -251,33 +340,20 @@ private int seedSky(List<SectionOpacity> sections, int height) {
for (int z = 0; z < LightNibbles.DIMENSION; z++) {
for (int x = 0; x < LightNibbles.DIMENSION; x++) {
for (int y = height - 1; y >= 0; y--) {
if (blocksFace(sections, x, y, z, BlockFace.TOP)) {
int index = index(x, y, z);

if ((this.occlusion[index] & TOP_BIT) != 0) {
break;
}
int index = index(x, y, z);
this.levels[index] = LightNibbles.MAX_LEVEL;
ensureRoom(tail);
this.queue[tail++] = index;
this.queue[tail++] = index | (NO_FACE << POSITION_BITS);
}
}
}
return tail;
}

/**
* Checks whether light cannot enter the given position through the given face.
*
* @param sections the light properties of every section
* @param x the x coordinate inside the chunk
* @param y the y coordinate inside the column
* @param z the z coordinate inside the chunk
* @param face the face light would enter through
* @return true if light cannot pass the face, otherwise false
*/
private static boolean blocksFace(List<SectionOpacity> sections, int x, int y, int z, BlockFace face) {
return sections.get(y >> 4).blocksFace(x, y & MASK, z, face);
}

/**
* Transfers the calculated levels into one light section per section of the chunk.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
public final class LightPropagator {

private static final BlockFace[] FACES = BlockFace.values();
private static final int MASK = LightNibbles.DIMENSION - 1;

/**
* The opposite of every face, in the order of {@link #FACES}.
Expand All @@ -40,6 +41,46 @@ public final class LightPropagator {
*/
private static final BlockFace[] OPPOSITES = opposites();

/**
* The index into {@link #FACES} of the opposite of every face.
* <p>
* A queued position remembers the face that points back at whoever queued it, and a face is
* cheaper to carry as its index than as a reference. This is that index.
* </p>
*/
private static final int[] OPPOSITE_INDEX = oppositeIndexes();

/**
* The amount of bits a queued position occupies, leaving the ones above it for the face.
* A section holds 4096 positions, so twelve bits carry every one of them.
*/
private static final int POSITION_BITS = 12;

/**
* The bits of a queue entry which carry the position.
*/
private static final int POSITION_MASK = (1 << POSITION_BITS) - 1;

/**
* The face value of an entry which nobody queued, so no direction may be skipped for it.
* Six faces occupy the indexes zero to five, which leaves this one free.
*/
private static final int NO_FACE = 7;

/**
* Resolves the index of the opposite of every face once.
*
* @return the index of the opposite of every face, indexed like {@link #FACES}
*/
private static int[] oppositeIndexes() {
int[] indexes = new int[FACES.length];

for (int index = 0; index < FACES.length; index++) {
indexes[index] = OPPOSITES[index].ordinal();
}
return indexes;
}

/**
* Resolves the opposite of every face once.
*
Expand All @@ -53,7 +94,6 @@ private static BlockFace[] opposites() {
}
return opposites;
}
private static final int MASK = LightNibbles.DIMENSION - 1;

private final byte[] levels;
private int[] queue;
Expand Down Expand Up @@ -98,7 +138,9 @@ public LightNibbles propagate(SectionOpacity opacity) {
int head = 0;

while (head < tail) {
int index = this.queue[head++];
int entry = this.queue[head++];
int index = entry & POSITION_MASK;
int arrivedFrom = entry >>> POSITION_BITS;
int level = this.levels[index];

if (level <= 1) {
Expand All @@ -111,6 +153,12 @@ public LightNibbles propagate(SectionOpacity opacity) {
int next = level - 1;

for (int faceIndex = 0; faceIndex < FACES.length; faceIndex++) {
// Whoever queued this position sits on the far side of that face and already holds
// a level one higher, so the test below could never pass for it. Skipping the face
// outright is the same result for a sixth less work.
if (faceIndex == arrivedFrom) {
continue;
}
BlockFace face = FACES[faceIndex];
int neighbourX = x + face.offsetX();
int neighbourY = y + face.offsetY();
Expand All @@ -119,21 +167,23 @@ public LightNibbles propagate(SectionOpacity opacity) {
if (isOutside(neighbourX, neighbourY, neighbourZ)) {
continue;
}
int neighbourIndex = index(neighbourX, neighbourY, neighbourZ);

// The level is one array read, the occlusion is two and a branch, and the level
// rejects far more often — a position is reached from up to six directions and only
// the first of them raises it. Cheapest and most selective test first.
if (this.levels[neighbourIndex] >= next) {
continue;
}
// Only the face light enters decides whether it can pass. Testing the face it
// leaves as well would keep every emitting block that is opaque itself dark, and a
// glowstone block is exactly that.
if (opacity.blocksFace(neighbourX, neighbourY, neighbourZ, OPPOSITES[faceIndex])) {
continue;
}

int neighbourIndex = index(neighbourX, neighbourY, neighbourZ);

if (this.levels[neighbourIndex] >= next) {
continue;
}
this.levels[neighbourIndex] = (byte) next;
ensureRoom(tail);
this.queue[tail++] = neighbourIndex;
this.queue[tail++] = neighbourIndex | (OPPOSITE_INDEX[faceIndex] << POSITION_BITS);
}
}
return collect();
Expand All @@ -159,7 +209,7 @@ private int seed(SectionOpacity opacity) {
int index = index(x, y, z);
this.levels[index] = (byte) emission;
ensureRoom(tail);
this.queue[tail++] = index;
this.queue[tail++] = index | (NO_FACE << POSITION_BITS);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,37 @@ public boolean blocksFace(int x, int y, int z, BlockFace face) {
return (mask & (1 << face.ordinal())) != 0;
}

/**
* Writes the occluded faces of every block of this section into one flat array.
* <p>
* A propagation over a whole chunk reads the occlusion of a neighbour once per face per queued
* position, and reaching it through the section costs an index into the list, an interface call
* and a null test before the byte itself. Laying the whole column out flat once turns all of
* that into a single array read for the rest of the pass.
* </p>
* <p>
* A uniform section is filled rather than copied, which is why it is still cheaper than one that
* carries a table: the fill writes the same byte over a contiguous range and never touches a
* per position table, because a uniform section holds none.
* </p>
* <p>
* Package private on purpose. This hands out the internal layout of the table and is meant for
* the two propagators of this package, not for callers.
* </p>
*
* @param target the array which receives the occluded faces
* @param offset the index in the target at which this section begins
*/
void copyOcclusionInto(byte[] target, int offset) {
byte[] table = this.occlusion;

if (table == null) {
Arrays.fill(target, offset, offset + LightNibbles.BLOCK_COUNT, this.uniformOcclusion);
return;
}
System.arraycopy(table, 0, target, offset, LightNibbles.BLOCK_COUNT);
}

/**
* Returns the amount of light the block at the given position emits.
*
Expand Down
Loading