diff --git a/falco-light/src/main/java/net/onelitefeather/falco/light/ChunkLightPropagator.java b/falco-light/src/main/java/net/onelitefeather/falco/light/ChunkLightPropagator.java index b9a0937..b03769a 100644 --- a/falco-light/src/main/java/net/onelitefeather/falco/light/ChunkLightPropagator.java +++ b/falco-light/src/main/java/net/onelitefeather/falco/light/ChunkLightPropagator.java @@ -60,7 +60,76 @@ private static BlockFace[] opposites() { return opposites; } + /** + * The index into {@link #FACES} of the opposite of every face. + *
+ * 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. + *
+ */ + 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. + *+ * 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. + *
+ */ + 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; /** @@ -68,6 +137,7 @@ private static BlockFace[] opposites() { */ public ChunkLightPropagator() { this.levels = new byte[0]; + this.occlusion = new byte[0]; this.queue = new int[0]; } @@ -114,8 +184,16 @@ private int prepare(List+ * 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. + *
+ */ + 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. * @@ -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; @@ -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) { @@ -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(); @@ -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(); @@ -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); } } } diff --git a/falco-light/src/main/java/net/onelitefeather/falco/light/SectionOpacity.java b/falco-light/src/main/java/net/onelitefeather/falco/light/SectionOpacity.java index 4134123..5cfc197 100644 --- a/falco-light/src/main/java/net/onelitefeather/falco/light/SectionOpacity.java +++ b/falco-light/src/main/java/net/onelitefeather/falco/light/SectionOpacity.java @@ -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. + *+ * 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. + *
+ *+ * 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. + *
+ *+ * 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. + *
+ * + * @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. *