From 4017c1c6dcc7eff0b93bec0de5f21279ebc29471 Mon Sep 17 00:00:00 2001
From: Mike Primm <mike@primmhome.com>
Date: Sun, 13 Jan 2013 03:49:07 -0800
Subject: [PATCH] Compressed Nibble Arrays

Implement 'lightening' of NibbleArrays - only allocate
 buffers when non-trivial value Saving from 40-45% of memory use by chunk
 section data.

Finish up NibbleArray lightening work - use for Snapshots, reduce copies

Fix nibble handling with NBT - arrays aren't copied by NBTByteArray

diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java
index 1e1499e..76b081a 100644
--- a/src/main/java/net/minecraft/server/ChunkRegionLoader.java
+++ b/src/main/java/net/minecraft/server/ChunkRegionLoader.java
@@ -225,15 +225,15 @@ public class ChunkRegionLoader implements IChunkLoader, IAsyncChunkSaver {
                 nbttagcompound1.setByte("Y", (byte) (chunksection.getYPosition() >> 4 & 255));
                 nbttagcompound1.setByteArray("Blocks", chunksection.getIdArray());
                 if (chunksection.getExtendedIdArray() != null) {
-                    nbttagcompound1.setByteArray("Add", chunksection.getExtendedIdArray().a);
+                    nbttagcompound1.setByteArray("Add", chunksection.getExtendedIdArray().getValueArray()); // Spigot
                 }
 
-                nbttagcompound1.setByteArray("Data", chunksection.getDataArray().a);
-                nbttagcompound1.setByteArray("BlockLight", chunksection.getEmittedLightArray().a);
+                nbttagcompound1.setByteArray("Data", chunksection.getDataArray().getValueArray()); // Spigot
+                nbttagcompound1.setByteArray("BlockLight", chunksection.getEmittedLightArray().getValueArray()); // Spigot
                 if (flag) {
-                    nbttagcompound1.setByteArray("SkyLight", chunksection.getSkyLightArray().a);
+                    nbttagcompound1.setByteArray("SkyLight", chunksection.getSkyLightArray().getValueArray()); // Spigot
                 } else {
-                    nbttagcompound1.setByteArray("SkyLight", new byte[chunksection.getEmittedLightArray().a.length]);
+                    nbttagcompound1.setByteArray("SkyLight", new byte[chunksection.getEmittedLightArray().getValueArray().length]); // Spigot
                 }
 
                 nbttaglist.add(nbttagcompound1);
diff --git a/src/main/java/net/minecraft/server/ChunkSection.java b/src/main/java/net/minecraft/server/ChunkSection.java
index a05efa0..360de1a 100644
--- a/src/main/java/net/minecraft/server/ChunkSection.java
+++ b/src/main/java/net/minecraft/server/ChunkSection.java
@@ -140,7 +140,8 @@ public class ChunkSection {
                 }
             }
         } else {
-            byte[] ext = this.extBlockIds.a;
+            this.extBlockIds.forceToNonTrivialArray(); // Spigot
+            byte[] ext = this.extBlockIds.getValueArray();
             for (int off = 0, off2 = 0; off < blkIds.length;) {
                 byte extid = ext[off2];
                 int l = (blkIds[off] & 0xFF) | ((extid & 0xF) << 8); // Even data
@@ -171,6 +172,12 @@ public class ChunkSection {
                 off++;
                 off2++;
             }
+            // Spigot start
+            this.extBlockIds.detectAndProcessTrivialArray();
+            if (this.extBlockIds.isTrivialArray() && (this.extBlockIds.getTrivialArrayValue() == 0)) {
+                this.extBlockIds = null;
+            }
+            // Spigot end
         }
         this.nonEmptyBlockCount = cntNonEmpty;
         this.tickingBlockCount = cntTicking;
@@ -224,12 +231,11 @@ public class ChunkSection {
     public void setExtendedIdArray(NibbleArray nibblearray) {
         // CraftBukkit start - Don't hang on to an empty nibble array
         boolean empty = true;
-        for (int i = 0; i < nibblearray.a.length; i++) {
-            if (nibblearray.a[i] != 0) {
-                empty = false;
-                break;
-            }
+        // Spigot start
+        if ((!nibblearray.isTrivialArray()) || (nibblearray.getTrivialArrayValue() != 0)) {
+            empty = false;
         }
+        // Spigot end
 
         if (empty) {
             return;
@@ -253,11 +259,11 @@ public class ChunkSection {
 
     // CraftBukkit start - Validate array lengths
     private NibbleArray validateNibbleArray(NibbleArray nibbleArray) {
-        if (nibbleArray != null && nibbleArray.a.length < 2048) {
-            byte[] newArray = new byte[2048];
-            System.arraycopy(nibbleArray.a, 0, newArray, 0, ((nibbleArray.a.length > 2048) ? 2048 : nibbleArray.a.length));
-            nibbleArray = new NibbleArray(newArray, 4);
+        // Spigot start - fix for more awesome nibble arrays
+        if (nibbleArray != null && nibbleArray.getByteLength() < 2048) {
+            nibbleArray.resizeArray(2048);
         }
+        // Spigot end
 
         return nibbleArray;
     }
diff --git a/src/main/java/net/minecraft/server/NibbleArray.java b/src/main/java/net/minecraft/server/NibbleArray.java
index 5d75a54..c9bc20c 100644
--- a/src/main/java/net/minecraft/server/NibbleArray.java
+++ b/src/main/java/net/minecraft/server/NibbleArray.java
@@ -1,13 +1,117 @@
 package net.minecraft.server;
 
+import java.util.Arrays; // Spigot
+
 public class NibbleArray {
 
-    public final byte[] a;
+    private byte[] a; // Spigot - remove final, make private (anyone directly accessing this is broken already)
     private final int b;
     private final int c;
+    // Spigot start
+    private byte trivialValue;
+    private byte trivialByte;
+    private int length;
+    private static final int LEN2K = 2048; // Universal length used right now - optimize around this
+    private static final byte[][] TrivLen2k;
+
+    static {
+        TrivLen2k = new byte[16][];
+        for (int i = 0; i < 16; i++) {
+            TrivLen2k[i] = new byte[LEN2K];
+            Arrays.fill(TrivLen2k[i], (byte) (i | (i << 4)));
+        }
+    }
+
+    // Try to convert array to trivial array
+    public void detectAndProcessTrivialArray() {
+        trivialValue = (byte) (a[0] & 0xF);
+        trivialByte = (byte) (trivialValue | (trivialValue << 4));
+        for (int i = 0; i < a.length; i++) {
+            if (a[i] != trivialByte) return;
+        }
+        // All values matches, so array is trivial
+        this.length = a.length;
+        this.a = null;
+    }
+
+    // Force array to non-trivial state
+    public void forceToNonTrivialArray() {
+        if (this.a == null) {
+            this.a = new byte[this.length];
+            if (this.trivialByte != 0) {
+                Arrays.fill(this.a, this.trivialByte);
+            }
+        }
+    }
+
+    // Test if array is in trivial state
+    public boolean isTrivialArray() {
+        return (this.a == null);
+    }
+
+    // Get value of all elements (only valid if array is in trivial state)
+    public int getTrivialArrayValue() {
+        return this.trivialValue;
+    }
+
+    // Get logical length of byte array for nibble data (whether trivial or non-trivial)
+    public int getByteLength() {
+        if (this.a == null) {
+            return this.length;
+        } else {
+            return this.a.length;
+        }
+    }
+
+    // Return byte encoding of array (whether trivial or non-trivial) - returns read-only array if trivial (do not modify!)
+    public byte[] getValueArray() {
+        if (this.a != null) {
+            return this.a;
+        } else {
+            byte[] rslt;
+
+            if (this.length == LEN2K) {  // All current uses are 2k long, but be safe
+                rslt = TrivLen2k[this.trivialValue];
+            } else {
+                rslt = new byte[this.length];
+                if (this.trivialByte != 0) {
+                    Arrays.fill(rslt, this.trivialByte);
+                }
+            }
+            return rslt;
+        }
+    }
+
+    // Copy byte representation of array to given offset in given byte array
+    public int copyToByteArray(byte[] dest, int off) {
+        if (this.a == null) {
+            Arrays.fill(dest, off, off + this.length, this.trivialByte);
+            return off + this.length;
+        } else {
+            System.arraycopy(this.a, 0, dest, off, this.a.length);
+            return off + this.a.length;
+        }
+    }
+
+    // Resize array to given byte length
+    public void resizeArray(int len) {
+        if (this.a == null) {
+            this.length = len;
+        } else if (this.a.length != len) {
+            byte[] newa = new byte[len];
+            System.arraycopy(this.a, 0, newa, 0, ((this.a.length > len) ? len : this.a.length));
+            this.a = newa;
+        }
+    }
+    // Spigot end
 
     public NibbleArray(int i, int j) {
-        this.a = new byte[i >> 1];
+        // Spigot start
+        //this.a = new byte[i >> 1];
+        this.a = null; // Start off as trivial value (all same zero value)
+        this.length = i >> 1;
+        this.trivialByte = this.trivialValue = 0;
+        // Spigot end
         this.b = j;
         this.c = j + 4;
     }
@@ -16,9 +120,11 @@ public class NibbleArray {
         this.a = abyte;
         this.b = i;
         this.c = i + 4;
+        detectAndProcessTrivialArray(); // Spigot
     }
 
     public int a(int i, int j, int k) {
+        if (this.a == null) return this.trivialValue; // Spigot
         int l = j << this.c | k << this.b | i;
         int i1 = l >> 1;
         int j1 = l & 1;
@@ -27,6 +133,18 @@ public class NibbleArray {
     }
 
     public void a(int i, int j, int k, int l) {
+        // Spigot start
+        if (this.a == null) {
+            if (l != this.trivialValue) { // Not same as trivial value, array no longer trivial
+                this.a = new byte[this.length];
+                if (this.trivialByte != 0) {
+                    Arrays.fill(this.a, this.trivialByte);
+                }
+            } else {
+                return;
+            }
+        }
+        // Spigot end
         int i1 = j << this.c | k << this.b | i;
         int j1 = i1 >> 1;
         int k1 = i1 & 1;
diff --git a/src/main/java/net/minecraft/server/OldChunkLoader.java b/src/main/java/net/minecraft/server/OldChunkLoader.java
index fcb9912..6ee28cc 100644
--- a/src/main/java/net/minecraft/server/OldChunkLoader.java
+++ b/src/main/java/net/minecraft/server/OldChunkLoader.java
@@ -94,9 +94,11 @@ public class OldChunkLoader {
 
                 nbttagcompound1.setByte("Y", (byte) (k & 255));
                 nbttagcompound1.setByteArray("Blocks", abyte);
-                nbttagcompound1.setByteArray("Data", nibblearray.a);
-                nbttagcompound1.setByteArray("SkyLight", nibblearray1.a);
-                nbttagcompound1.setByteArray("BlockLight", nibblearray2.a);
+                // Spigot start - a -> getValueArray() accessor
+                nbttagcompound1.setByteArray("Data", nibblearray.getValueArray());
+                nbttagcompound1.setByteArray("SkyLight", nibblearray1.getValueArray());
+                nbttagcompound1.setByteArray("BlockLight", nibblearray2.getValueArray());
+                // Spigot end
                 nbttaglist.add(nbttagcompound1);
             }
         }
diff --git a/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java b/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java
index c7b799a..856e825 100644
--- a/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java
+++ b/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java
@@ -138,16 +138,16 @@ public class PacketPlayOutMapChunk extends Packet {
         for (l = 0; l < achunksection.length; ++l) {
             if (achunksection[l] != null && (!flag || !achunksection[l].isEmpty()) && (i & 1 << l) != 0) {
                 nibblearray = achunksection[l].getDataArray();
-                System.arraycopy(nibblearray.a, 0, abyte, j, nibblearray.a.length);
-                j += nibblearray.a.length;
+                nibblearray.copyToByteArray(abyte, j);
+                j += nibblearray.getByteLength();
             }
         }
 
         for (l = 0; l < achunksection.length; ++l) {
             if (achunksection[l] != null && (!flag || !achunksection[l].isEmpty()) && (i & 1 << l) != 0) {
                 nibblearray = achunksection[l].getEmittedLightArray();
-                System.arraycopy(nibblearray.a, 0, abyte, j, nibblearray.a.length);
-                j += nibblearray.a.length;
+                nibblearray.copyToByteArray(abyte, j);
+                j += nibblearray.getByteLength();
             }
         }
 
@@ -155,8 +155,8 @@ public class PacketPlayOutMapChunk extends Packet {
             for (l = 0; l < achunksection.length; ++l) {
                 if (achunksection[l] != null && (!flag || !achunksection[l].isEmpty()) && (i & 1 << l) != 0) {
                     nibblearray = achunksection[l].getSkyLightArray();
-                    System.arraycopy(nibblearray.a, 0, abyte, j, nibblearray.a.length);
-                    j += nibblearray.a.length;
+                    nibblearray.copyToByteArray(abyte, j);
+                    j += nibblearray.getByteLength();
                 }
             }
         }
@@ -165,8 +165,8 @@ public class PacketPlayOutMapChunk extends Packet {
             for (l = 0; l < achunksection.length; ++l) {
                 if (achunksection[l] != null && (!flag || !achunksection[l].isEmpty()) && achunksection[l].getExtendedIdArray() != null && (i & 1 << l) != 0) {
                     nibblearray = achunksection[l].getExtendedIdArray();
-                    System.arraycopy(nibblearray.a, 0, abyte, j, nibblearray.a.length);
-                    j += nibblearray.a.length;
+                    nibblearray.copyToByteArray(abyte, j);
+                    j += nibblearray.getByteLength();
                 }
             }
         }
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
index b2c6ef4..55f5225 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
@@ -174,7 +174,18 @@ public class CraftChunk implements Chunk {
                 }
 
                 if (cs[i].getExtendedIdArray() != null) { /* If we've got extended IDs */
-                    byte[] extids = cs[i].getExtendedIdArray().a;
+                    // Spigot start
+                    if (cs[i].getExtendedIdArray().isTrivialArray()) {
+                        int tval = cs[i].getExtendedIdArray().getTrivialArrayValue();
+                        if (tval != 0) {
+                            tval = tval << 8;
+                            for (int j = 0; j < 4096; j++) {
+                                blockids[j] |= tval;
+                            }
+                        }
+                  } else {
+                    byte[] extids = cs[i].getExtendedIdArray().getValueArray();
+                    // Spigot end
 
                     for (int j = 0; j < 2048; j++) {
                         short b = (short) (extids[j] & 0xFF);
@@ -186,21 +197,42 @@ public class CraftChunk implements Chunk {
                         blockids[j<<1] |= (b & 0x0F) << 8;
                         blockids[(j<<1)+1] |= (b & 0xF0) << 4;
                     }
+                    } // Spigot
                 }
 
                 sectionBlockIDs[i] = blockids;
 
                 /* Get block data nibbles */
-                sectionBlockData[i] = new byte[2048];
-                System.arraycopy(cs[i].getDataArray().a, 0, sectionBlockData[i], 0, 2048);
+                // Spigot start
+                if (cs[i].getDataArray().isTrivialArray() && (cs[i].getDataArray().getTrivialArrayValue() == 0)) {
+                    sectionBlockData[i] = emptyData;
+                } else {
+                    sectionBlockData[i] = new byte[2048];
+                    cs[i].getDataArray().copyToByteArray(sectionBlockData[i], 0);
+                }
                 if (cs[i].getSkyLightArray() == null) {
                     sectionSkyLights[i] = emptyData;
+                }
+                else if (cs[i].getSkyLightArray().isTrivialArray()) {
+                    if (cs[i].getSkyLightArray().getTrivialArrayValue() == 0) {
+                        sectionSkyLights[i] = emptyData;
+                    } else if (cs[i].getSkyLightArray().getTrivialArrayValue() == 15) {
+                        sectionSkyLights[i] = emptySkyLight;
+                    } else {
+                        sectionSkyLights[i] = new byte[2048];
+                        cs[i].getSkyLightArray().copyToByteArray(sectionSkyLights[i], 0);
+                    }
                 } else {
                     sectionSkyLights[i] = new byte[2048];
-                    System.arraycopy(cs[i].getSkyLightArray().a, 0, sectionSkyLights[i], 0, 2048);
+                    cs[i].getSkyLightArray().copyToByteArray(sectionSkyLights[i], 0);
+                }
+                if (cs[i].getEmittedLightArray().isTrivialArray() && (cs[i].getEmittedLightArray().getTrivialArrayValue() == 0)) {
+                    sectionEmitLights[i] = emptyData;
+                } else {
+                    sectionEmitLights[i] = new byte[2048];
+                    cs[i].getEmittedLightArray().copyToByteArray(sectionEmitLights[i], 0);
                 }
-                sectionEmitLights[i] = new byte[2048];
-                System.arraycopy(cs[i].getEmittedLightArray().a, 0, sectionEmitLights[i], 0, 2048);
+                // Spigot end
             }
         }
 
-- 
1.8.3.2