diff --git a/src/main/java/rs117/hd/opengl/GLState.java b/src/main/java/rs117/hd/opengl/GLState.java index 5775c232e0..01bba25faa 100644 --- a/src/main/java/rs117/hd/opengl/GLState.java +++ b/src/main/java/rs117/hd/opengl/GLState.java @@ -1,9 +1,8 @@ package rs117.hd.opengl; import java.util.Arrays; -import java.util.HashSet; import java.util.Objects; -import java.util.Set; +import rs117.hd.utils.collection.IntHashSet; public abstract class GLState { protected boolean hasValue; @@ -134,7 +133,7 @@ void internalApply() { } public abstract static class IntSet extends GLState { - private final Set targets = new HashSet<>(); + private final IntHashSet targets = new IntHashSet(); public void add(int target) { hasValue = true; diff --git a/src/main/java/rs117/hd/renderer/legacy/LegacySceneUploader.java b/src/main/java/rs117/hd/renderer/legacy/LegacySceneUploader.java index f5423eddf0..c7c2156303 100644 --- a/src/main/java/rs117/hd/renderer/legacy/LegacySceneUploader.java +++ b/src/main/java/rs117/hd/renderer/legacy/LegacySceneUploader.java @@ -98,7 +98,7 @@ public class LegacySceneUploader { private LegacyModelPusher modelPusher; public void upload(LegacySceneContext sceneContext) { - proceduralGenerator.generateSceneData(sceneContext); + proceduralGenerator.generateSceneData(sceneContext, null); Stopwatch stopwatch = Stopwatch.createStarted(); @@ -708,23 +708,23 @@ private int[] uploadHDTilePaintSurface( // set colors for the shoreline to create a foam effect in the water shader swColor = seColor = nwColor = neColor = 127; - if (sceneContext.vertexIsWater.containsKey(swVertexKey) && sceneContext.vertexIsLand.containsKey(swVertexKey)) + if (sceneContext.vertexIsWater.contains(swVertexKey) && sceneContext.vertexIsLand.contains(swVertexKey)) swColor = 0; - if (sceneContext.vertexIsWater.containsKey(seVertexKey) && sceneContext.vertexIsLand.containsKey(seVertexKey)) + if (sceneContext.vertexIsWater.contains(seVertexKey) && sceneContext.vertexIsLand.contains(seVertexKey)) seColor = 0; - if (sceneContext.vertexIsWater.containsKey(nwVertexKey) && sceneContext.vertexIsLand.containsKey(nwVertexKey)) + if (sceneContext.vertexIsWater.contains(nwVertexKey) && sceneContext.vertexIsLand.contains(nwVertexKey)) nwColor = 0; - if (sceneContext.vertexIsWater.containsKey(neVertexKey) && sceneContext.vertexIsLand.containsKey(neVertexKey)) + if (sceneContext.vertexIsWater.contains(neVertexKey) && sceneContext.vertexIsLand.contains(neVertexKey)) neColor = 0; } - if (sceneContext.vertexIsOverlay.containsKey(neVertexKey) && sceneContext.vertexIsUnderlay.containsKey(neVertexKey)) + if (sceneContext.vertexIsOverlay.contains(neVertexKey) && sceneContext.vertexIsUnderlay.contains(neVertexKey)) neVertexIsOverlay = true; - if (sceneContext.vertexIsOverlay.containsKey(nwVertexKey) && sceneContext.vertexIsUnderlay.containsKey(nwVertexKey)) + if (sceneContext.vertexIsOverlay.contains(nwVertexKey) && sceneContext.vertexIsUnderlay.contains(nwVertexKey)) nwVertexIsOverlay = true; - if (sceneContext.vertexIsOverlay.containsKey(seVertexKey) && sceneContext.vertexIsUnderlay.containsKey(seVertexKey)) + if (sceneContext.vertexIsOverlay.contains(seVertexKey) && sceneContext.vertexIsUnderlay.contains(seVertexKey)) seVertexIsOverlay = true; - if (sceneContext.vertexIsOverlay.containsKey(swVertexKey) && sceneContext.vertexIsUnderlay.containsKey(swVertexKey)) + if (sceneContext.vertexIsOverlay.contains(swVertexKey) && sceneContext.vertexIsUnderlay.contains(swVertexKey)) swVertexIsOverlay = true; @@ -1065,19 +1065,19 @@ private int[] uploadHDTileModelSurface( } else { // set colors for the shoreline to create a foam effect in the water shader colorA = colorB = colorC = 127; - if (sceneContext.vertexIsWater.containsKey(vertexKeyA) && sceneContext.vertexIsLand.containsKey(vertexKeyA)) + if (sceneContext.vertexIsWater.contains(vertexKeyA) && sceneContext.vertexIsLand.contains(vertexKeyA)) colorA = 0; - if (sceneContext.vertexIsWater.containsKey(vertexKeyB) && sceneContext.vertexIsLand.containsKey(vertexKeyB)) + if (sceneContext.vertexIsWater.contains(vertexKeyB) && sceneContext.vertexIsLand.contains(vertexKeyB)) colorB = 0; - if (sceneContext.vertexIsWater.containsKey(vertexKeyC) && sceneContext.vertexIsLand.containsKey(vertexKeyC)) + if (sceneContext.vertexIsWater.contains(vertexKeyC) && sceneContext.vertexIsLand.contains(vertexKeyC)) colorC = 0; } - if (sceneContext.vertexIsOverlay.containsKey(vertexKeyA) && sceneContext.vertexIsUnderlay.containsKey(vertexKeyA)) + if (sceneContext.vertexIsOverlay.contains(vertexKeyA) && sceneContext.vertexIsUnderlay.contains(vertexKeyA)) vertexAIsOverlay = true; - if (sceneContext.vertexIsOverlay.containsKey(vertexKeyB) && sceneContext.vertexIsUnderlay.containsKey(vertexKeyB)) + if (sceneContext.vertexIsOverlay.contains(vertexKeyB) && sceneContext.vertexIsUnderlay.contains(vertexKeyB)) vertexBIsOverlay = true; - if (sceneContext.vertexIsOverlay.containsKey(vertexKeyC) && sceneContext.vertexIsUnderlay.containsKey(vertexKeyC)) + if (sceneContext.vertexIsOverlay.contains(vertexKeyC) && sceneContext.vertexIsUnderlay.contains(vertexKeyC)) vertexCIsOverlay = true; for (int i = 0; i < 3; i++) diff --git a/src/main/java/rs117/hd/renderer/zone/SceneManager.java b/src/main/java/rs117/hd/renderer/zone/SceneManager.java index 6370b07e71..70d59df08e 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneManager.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneManager.java @@ -5,9 +5,7 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; @@ -32,6 +30,7 @@ import rs117.hd.scene.areas.Area; import rs117.hd.utils.NpcDisplacementCache; import rs117.hd.utils.RenderState; +import rs117.hd.utils.collection.Int2IntHashMap; import rs117.hd.utils.jobs.GenericJob; import static net.runelite.api.Constants.*; @@ -86,14 +85,13 @@ public class SceneManager { private RenderState renderState; private UBOWorldViews uboWorldViews; + private final Int2IntHashMap nextRoofChanges = new Int2IntHashMap(); @Getter private final WorldViewContext root = new WorldViewContext(null, null, null); private final WorldViewContext[] subs = new WorldViewContext[MAX_WORLDVIEWS]; - - private final Map nextRoofChanges = new HashMap<>(); + private final List sortedZones = new ArrayList<>(); private ZoneSceneContext nextSceneContext; private Zone[][] nextZones; - private final List sortedZones = new ArrayList<>(); private boolean reloadRequested; public boolean isZoneStreamingEnabled() { @@ -324,7 +322,7 @@ private static boolean isEdgeTile(Zone[][] zones, int zx, int zz) { @Getter private final GenericJob generateSceneDataTask = GenericJob.build( "ProceduralGenerator::generateSceneData", - (task) -> proceduralGenerator.generateSceneData(nextSceneContext != null ? nextSceneContext : root.sceneContext) + (task) -> proceduralGenerator.generateSceneData(nextSceneContext != null ? nextSceneContext : root.sceneContext, root.sceneContext) ); @Getter @@ -359,11 +357,14 @@ private static boolean isEdgeTile(Zone[][] zones, int zx, int zz) { int prid = prids[level][ox][oz]; int nrid = nrids[level][x][z]; if (prid > 0 && nrid > 0 && prid != nrid) { - Integer old = nextRoofChanges.putIfAbsent(prid, nrid); - if (old == null) { + boolean hasExisting = nextRoofChanges.putIfAbsent(prid, nrid); + if(hasExisting) { + int old = nextRoofChanges.getOrDefault(prid, nrid); + if (old != nrid) { + log.debug("Roof change mismatch: {} -> {} vs {}", prid, nrid, old); + } + } else { log.trace("Roof change: {} -> {}", prid, nrid); - } else if (old != nrid) { - log.debug("Roof change mismatch: {} -> {} vs {}", prid, nrid, old); } } } @@ -704,7 +705,7 @@ private void loadSubScene(WorldView worldView, Scene scene) { } var sceneContext = new ZoneSceneContext(client, worldView, scene, plugin.getExpandedMapLoadingChunks(), null); - proceduralGenerator.generateSceneData(sceneContext); + proceduralGenerator.generateSceneData(sceneContext, null); final WorldViewContext ctx = new WorldViewContext(worldView, sceneContext, uboWorldViews); ctx.initialize(renderState, injector); diff --git a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java index badfc9bb67..6dd49f654a 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java @@ -24,8 +24,6 @@ */ package rs117.hd.renderer.zone; -import java.util.HashSet; -import java.util.Set; import javax.inject.Inject; import lombok.extern.slf4j.Slf4j; import net.runelite.api.*; @@ -47,6 +45,7 @@ import rs117.hd.utils.HDUtils; import rs117.hd.utils.ModelHash; import rs117.hd.utils.buffer.GpuIntBuffer; +import rs117.hd.utils.collection.IntHashSet; import rs117.hd.utils.collections.ConcurrentPool; import rs117.hd.utils.collections.PrimitiveIntArray; @@ -118,7 +117,7 @@ public interface OnBeforeProcessTileFunc { private int basex, basez, rid, level; - private final Set roofIds = new HashSet<>(); + private final IntHashSet roofIds = new IntHashSet(); private Scene currentScene; private Tile[][][] tiles; private byte[][][] settings; @@ -217,13 +216,13 @@ public void uploadZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) throws this.level = z; if (z == 0) { - uploadZoneLevel(ctx, zone, mzx, mzz, 0, false, roofIds, vb, ab, fb); - uploadZoneLevel(ctx, zone, mzx, mzz, 0, true, roofIds, vb, ab, fb); - uploadZoneLevel(ctx, zone, mzx, mzz, 1, true, roofIds, vb, ab, fb); - uploadZoneLevel(ctx, zone, mzx, mzz, 2, true, roofIds, vb, ab, fb); - uploadZoneLevel(ctx, zone, mzx, mzz, 3, true, roofIds, vb, ab, fb); + uploadZoneLevel(ctx, zone, mzx, mzz, 0, false, vb, ab, fb); + uploadZoneLevel(ctx, zone, mzx, mzz, 0, true, vb, ab, fb); + uploadZoneLevel(ctx, zone, mzx, mzz, 1, true, vb, ab, fb); + uploadZoneLevel(ctx, zone, mzx, mzz, 2, true, vb, ab, fb); + uploadZoneLevel(ctx, zone, mzx, mzz, 3, true, vb, ab, fb); } else { - uploadZoneLevel(ctx, zone, mzx, mzz, z, false, roofIds, vb, ab, fb); + uploadZoneLevel(ctx, zone, mzx, mzz, z, false, vb, ab, fb); } if (vb != null) { @@ -246,7 +245,6 @@ private void uploadZoneLevel( int mzz, int level, boolean visbelow, - Set roofIds, GpuIntBuffer vb, GpuIntBuffer ab, GpuIntBuffer fb @@ -927,25 +925,25 @@ private void uploadTilePaint( neMaterial = groundMaterial.getRandomMaterial(worldPos[0] + 1, worldPos[1] + 1, worldPos[2]); } - if (ctx.vertexIsOverlay.containsKey(neVertexKey) && ctx.vertexIsUnderlay.containsKey(neVertexKey)) + if (ctx.vertexIsOverlay.contains(neVertexKey) && ctx.vertexIsUnderlay.contains(neVertexKey)) neVertexIsOverlay = true; - if (ctx.vertexIsOverlay.containsKey(nwVertexKey) && ctx.vertexIsUnderlay.containsKey(nwVertexKey)) + if (ctx.vertexIsOverlay.contains(nwVertexKey) && ctx.vertexIsUnderlay.contains(nwVertexKey)) nwVertexIsOverlay = true; - if (ctx.vertexIsOverlay.containsKey(seVertexKey) && ctx.vertexIsUnderlay.containsKey(seVertexKey)) + if (ctx.vertexIsOverlay.contains(seVertexKey) && ctx.vertexIsUnderlay.contains(seVertexKey)) seVertexIsOverlay = true; - if (ctx.vertexIsOverlay.containsKey(swVertexKey) && ctx.vertexIsUnderlay.containsKey(swVertexKey)) + if (ctx.vertexIsOverlay.contains(swVertexKey) && ctx.vertexIsUnderlay.contains(swVertexKey)) swVertexIsOverlay = true; } else if (onlyWaterSurface) { // set colors for the shoreline to create a foam effect in the water shader swColor = seColor = nwColor = neColor = 127; - if (ctx.vertexIsWater.containsKey(swVertexKey) && ctx.vertexIsLand.containsKey(swVertexKey)) + if (ctx.vertexIsWater.contains(swVertexKey) && ctx.vertexIsLand.contains(swVertexKey)) swColor = 0; - if (ctx.vertexIsWater.containsKey(seVertexKey) && ctx.vertexIsLand.containsKey(seVertexKey)) + if (ctx.vertexIsWater.contains(seVertexKey) && ctx.vertexIsLand.contains(seVertexKey)) seColor = 0; - if (ctx.vertexIsWater.containsKey(nwVertexKey) && ctx.vertexIsLand.containsKey(nwVertexKey)) + if (ctx.vertexIsWater.contains(nwVertexKey) && ctx.vertexIsLand.contains(nwVertexKey)) nwColor = 0; - if (ctx.vertexIsWater.containsKey(neVertexKey) && ctx.vertexIsLand.containsKey(neVertexKey)) + if (ctx.vertexIsWater.contains(neVertexKey) && ctx.vertexIsLand.contains(neVertexKey)) neColor = 0; if (seColor == 0 && nwColor == 0 && (neColor == 0 || swColor == 0)) @@ -1223,11 +1221,11 @@ private void uploadTileModel( } else if (onlyWaterSurface) { // set colors for the shoreline to create a foam effect in the water shader colorA = colorB = colorC = 127; - if (ctx.vertexIsWater.containsKey(vertexKeyA) && ctx.vertexIsLand.containsKey(vertexKeyA)) + if (ctx.vertexIsWater.contains(vertexKeyA) && ctx.vertexIsLand.contains(vertexKeyA)) colorA = 0; - if (ctx.vertexIsWater.containsKey(vertexKeyB) && ctx.vertexIsLand.containsKey(vertexKeyB)) + if (ctx.vertexIsWater.contains(vertexKeyB) && ctx.vertexIsLand.contains(vertexKeyB)) colorB = 0; - if (ctx.vertexIsWater.containsKey(vertexKeyC) && ctx.vertexIsLand.containsKey(vertexKeyC)) + if (ctx.vertexIsWater.contains(vertexKeyC) && ctx.vertexIsLand.contains(vertexKeyC)) colorC = 0; if (colorA == 0 && colorB == 0 && colorC == 0) colorA = colorB = colorC = 1 << 16; // Bias depth a bit if it's flush with underwater geometry @@ -1266,11 +1264,11 @@ private void uploadTileModel( terrainDataC = HDUtils.packTerrainData(true, max(1, depthC), waterType, tileZ); } - if (ctx.vertexIsOverlay.containsKey(vertexKeyA) && ctx.vertexIsUnderlay.containsKey(vertexKeyA)) + if (ctx.vertexIsOverlay.contains(vertexKeyA) && ctx.vertexIsUnderlay.contains(vertexKeyA)) vertexAIsOverlay = true; - if (ctx.vertexIsOverlay.containsKey(vertexKeyB) && ctx.vertexIsUnderlay.containsKey(vertexKeyB)) + if (ctx.vertexIsOverlay.contains(vertexKeyB) && ctx.vertexIsUnderlay.contains(vertexKeyB)) vertexBIsOverlay = true; - if (ctx.vertexIsOverlay.containsKey(vertexKeyC) && ctx.vertexIsUnderlay.containsKey(vertexKeyC)) + if (ctx.vertexIsOverlay.contains(vertexKeyC) && ctx.vertexIsUnderlay.contains(vertexKeyC)) vertexCIsOverlay = true; ly0 -= override.heightOffset; @@ -1502,7 +1500,7 @@ private int uploadStaticModel( int averageColor = (tilePaint.getSwColor() + tilePaint.getNwColor() + tilePaint.getNeColor() + tilePaint.getSeColor()) / 4; - var override = tileOverrideManager.getOverride(ctx, tile); + var override = tileOverrideManager.getOverride(ctx, tile, worldPos); averageColor = override.modifyColor(averageColor); color1 = color2 = color3 = averageColor; @@ -1576,7 +1574,8 @@ private int uploadStaticModel( Material material = baseMaterial; ModelOverride faceOverride = modelOverride; - if (isTextured) { + if (textureId != -1) { + color1 = color2 = color3 = 90; uvType = UvType.VANILLA; if (textureMaterial != Material.NONE) { material = textureMaterial; @@ -1591,13 +1590,11 @@ private int uploadStaticModel( } } } else if (modelOverride.colorOverrides != null) { - int ahsl = (0xFF - transparency) << 16 | color1; - for (var override : modelOverride.colorOverrides) { - if (override.ahslCondition.test(ahsl)) { - faceOverride = override; - material = faceOverride.baseMaterial; - break; - } + final int ahsl = (0xFF - transparency) << 16 | color1; + final var ahslOverride = modelOverride.testColorOverrides( ahsl); + if(ahslOverride != null) { + faceOverride = ahslOverride; + material = faceOverride.baseMaterial; } } @@ -1649,7 +1646,7 @@ private int uploadStaticModel( if (shouldRotateNormals) rotateNormals(modelNormals, orientSin, orientCos); - int depthBias = faceOverride.depthBias != -1 ? faceOverride.depthBias : + final int depthBias = faceOverride.depthBias != -1 ? faceOverride.depthBias : bias == null ? 0 : bias[face] & 0xFF; int packedAlphaBiasHsl = transparency << 24 | depthBias << 16; boolean hasAlpha = material.hasTransparency || transparency != 0; @@ -1838,13 +1835,11 @@ public boolean preprocessTempModel( } } } else if (modelOverride.colorOverrides != null) { - int ahsl = (0xFF - transparency) << 16 | model.getFaceColors1()[f]; - for (var override : modelOverride.colorOverrides) { - if (override.ahslCondition.test(ahsl)) { - faceOverride = override; - material = faceOverride.baseMaterial; - break; - } + final int ahsl = (0xFF - transparency) << 16 | model.getFaceColors1()[f]; + final var override = modelOverride.testColorOverrides(ahsl); + if (override != null) { + faceOverride = override; + material = faceOverride.baseMaterial; } } @@ -2078,12 +2073,14 @@ else if (color3 == -1) color2 |= packedAlphaBiasHsl; color3 |= packedAlphaBiasHsl; + tb.ensureFace(1); final int texturedFaceIdx = tb.putFace( color1, color2, color3, materialData, materialData, materialData, 0, 0, 0 ); + vb.ensureVertex(3); vb.putVertex( modelLocalI[vertexOffsetA], modelLocalI[vertexOffsetA + 1], modelLocalI[vertexOffsetA + 2], faceUVs[0], faceUVs[1], faceUVs[2], @@ -2103,7 +2100,7 @@ else if (color3 == -1) texturedFaceIdx ); } - + writeCache.flush(); } @@ -2113,26 +2110,26 @@ public static void calculateFaceNormal( float vx2, float vy2, float vz2, float vx3, float vy3, float vz3 ) { - float e0_x = vx2 - vx1; - float e0_y = vy2 - vy1; - float e0_z = vz2 - vz1; + final float e0_x = vx2 - vx1; + final float e0_y = vy2 - vy1; + final float e0_z = vz2 - vz1; - float e1_x = vx3 - vx1; - float e1_y = vy3 - vy1; - float e1_z = vz3 - vz1; + final float e1_x = vx3 - vx1; + final float e1_y = vy3 - vy1; + final float e1_z = vz3 - vz1; float nx = e0_y * e1_z - e0_z * e1_y; float ny = e0_z * e1_x - e0_x * e1_z; float nz = e0_x * e1_y - e0_y * e1_x; - float length = (float) Math.sqrt(nx * nx + ny * ny + nz * nz); - nx /= length; - ny /= length; - nz /= length; + final float invLength = rcp(sqrt(nx * nx + ny * ny + nz * nz)) * 2048.0f; + nx *= invLength; + ny *= invLength; + nz *= invLength; - out[0] = out[3] = out[6] = (int) (nx * 2048); - out[1] = out[4] = out[7] = (int) (ny * 2048); - out[2] = out[5] = out[8] = (int) (nz * 2048); + out[0] = out[3] = out[6] = (int) nx; + out[1] = out[4] = out[7] = (int) ny; + out[2] = out[5] = out[8] = (int) nz; } public static int interpolateHSL(int hsl, byte hue2, byte sat2, byte lum2, byte lerp) { diff --git a/src/main/java/rs117/hd/renderer/zone/VertexWriteCache.java b/src/main/java/rs117/hd/renderer/zone/VertexWriteCache.java index 828d11e4f0..d89c6e4417 100644 --- a/src/main/java/rs117/hd/renderer/zone/VertexWriteCache.java +++ b/src/main/java/rs117/hd/renderer/zone/VertexWriteCache.java @@ -38,14 +38,16 @@ private void flushAndGrow() { stagingBuffer = new int[min(stagingBuffer.length * 2, maxCapacity)]; } + public void ensureFace(int faceCount) { + if (stagingPosition + (9 * faceCount) > stagingBuffer.length) + flushAndGrow(); + } + public int putFace( int alphaBiasHslA, int alphaBiasHslB, int alphaBiasHslC, int materialDataA, int materialDataB, int materialDataC, int terrainDataA, int terrainDataB, int terrainDataC ) { - if (stagingPosition + 9 > stagingBuffer.length) - flushAndGrow(); - final int textureFaceIdx = (outputBuffer.position() + stagingPosition) / 3; final int[] stagingBuffer = this.stagingBuffer; final int stagingPosition = this.stagingPosition; @@ -67,15 +69,30 @@ public int putFace( return textureFaceIdx; } + public void ensureVertex(int vertexCount) { + if (stagingPosition + (7 * vertexCount) > stagingBuffer.length) + flushAndGrow(); + } + public void putVertex( int x, int y, int z, float u, float v, float w, int nx, int ny, int nz, int textureFaceIdx ) { - if (stagingPosition + 7 > stagingBuffer.length) - flushAndGrow(); + putVertex( + x, y, z, + Float.floatToRawIntBits(u), Float.floatToRawIntBits(v), Float.floatToRawIntBits(w), + nx, ny, nz, + textureFaceIdx); + } + public void putVertex( + int x, int y, int z, + int u, int v, int w, + int nx, int ny, int nz, + int textureFaceIdx + ) { final int[] stagingBuffer = this.stagingBuffer; final int stagingPosition = this.stagingPosition; diff --git a/src/main/java/rs117/hd/renderer/zone/Zone.java b/src/main/java/rs117/hd/renderer/zone/Zone.java index baed442f79..7a3598a99a 100644 --- a/src/main/java/rs117/hd/renderer/zone/Zone.java +++ b/src/main/java/rs117/hd/renderer/zone/Zone.java @@ -6,7 +6,6 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.Map; import java.util.concurrent.BlockingDeque; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.LinkedBlockingDeque; @@ -26,6 +25,7 @@ import rs117.hd.utils.HDUtils; import rs117.hd.utils.buffer.GLBuffer; import rs117.hd.utils.buffer.GLTextureBuffer; +import rs117.hd.utils.collection.Int2IntHashMap; import rs117.hd.utils.jobs.GenericJob; import static org.lwjgl.opengl.GL33C.*; @@ -320,7 +320,7 @@ public void setMetadata(WorldViewContext viewContext, SceneContext sceneContext, } } - void updateRoofs(Map updates) { + void updateRoofs(Int2IntHashMap updates) { for (int level = 0; level < 4; ++level) { for (int i = 0; i < rids[level].length; ++i) { rids[level][i] = updates.getOrDefault(rids[level][i], rids[level][i]); @@ -328,7 +328,7 @@ void updateRoofs(Map updates) { } for (AlphaModel m : alphaModels) { - m.rid = (short) (int) updates.getOrDefault((int) m.rid, (int) m.rid); + m.rid = (short) updates.getOrDefault(m.rid, m.rid); } } @@ -605,13 +605,10 @@ void addAlphaModel( } } } else if (modelOverride.colorOverrides != null) { - int ahsl = (0xFF - transparency) << 16 | (unlitColor != null ? unlitColor[f] & 0xFFFF : color1[f]); - for (var override : modelOverride.colorOverrides) { - if (override.ahslCondition.test(ahsl)) { - material = override.baseMaterial; - break; - } - } + final int ahsl = (0xFF - transparency) << 16 | (unlitColor != null ? unlitColor[f] & 0xFFFF : color1[f]); + final var ahslOverride = modelOverride.testColorOverrides( ahsl); + if(ahslOverride != null) + material = ahslOverride.baseMaterial; } boolean hasAlpha = material.hasTransparency || transparency != 0; diff --git a/src/main/java/rs117/hd/scene/ModelOverrideManager.java b/src/main/java/rs117/hd/scene/ModelOverrideManager.java index 008221b6ef..166a47b0cf 100644 --- a/src/main/java/rs117/hd/scene/ModelOverrideManager.java +++ b/src/main/java/rs117/hd/scene/ModelOverrideManager.java @@ -2,7 +2,6 @@ import java.io.IOException; import java.util.HashMap; -import java.util.HashSet; import java.util.Objects; import java.util.Set; import javax.annotation.Nonnull; @@ -20,6 +19,8 @@ import rs117.hd.utils.ModelHash; import rs117.hd.utils.Props; import rs117.hd.utils.ResourcePath; +import rs117.hd.utils.collection.Int2ObjectHashMap; +import rs117.hd.utils.collection.IntHashSet; import static rs117.hd.utils.ResourcePath.path; @@ -47,8 +48,8 @@ public class ModelOverrideManager { @Inject private FishingSpotReplacer fishingSpotReplacer; - private final HashMap modelOverrides = new HashMap<>(); - private final HashSet detailCullingBlacklist = new HashSet<>(); + private final Int2ObjectHashMap modelOverrides = new Int2ObjectHashMap<>(); + private final IntHashSet detailCullingBlacklist = new IntHashSet(); private FileWatcher.UnregisterCallback fileWatcher; @@ -85,9 +86,9 @@ public void startUp() { addSailingCullingOverrides(); detailCullingBlacklist.clear(); - for (var entry : modelOverrides.entrySet()) - if (entry.getValue().disableDetailCulling) - detailCullingBlacklist.add(entry.getKey()); + for (var entry : modelOverrides) + if (entry.value.disableDetailCulling) + detailCullingBlacklist.add(entry.key); log.debug("Loaded {} model overrides", modelOverrides.size()); diff --git a/src/main/java/rs117/hd/scene/ProceduralGenerator.java b/src/main/java/rs117/hd/scene/ProceduralGenerator.java index 2a3969d99e..f084c589d3 100644 --- a/src/main/java/rs117/hd/scene/ProceduralGenerator.java +++ b/src/main/java/rs117/hd/scene/ProceduralGenerator.java @@ -38,6 +38,9 @@ import rs117.hd.scene.water_types.WaterType; import rs117.hd.utils.ColorUtils; import rs117.hd.utils.buffer.GpuIntBuffer; +import rs117.hd.utils.collection.Int2IntHashMap; +import rs117.hd.utils.collection.Int2ObjectHashMap; +import rs117.hd.utils.collection.IntHashSet; import static net.runelite.api.Constants.*; import static net.runelite.api.Perspective.*; @@ -76,19 +79,19 @@ public class ProceduralGenerator { @Inject private WaterTypeManager waterTypeManager; - public void generateSceneData(SceneContext sceneContext) + public void generateSceneData(SceneContext sceneContext, SceneContext previousContext) { long timerTotal = System.currentTimeMillis(); long timerCalculateTerrainNormals, timerGenerateTerrainData, timerGenerateUnderwaterTerrain; long startTime = System.currentTimeMillis(); - generateUnderwaterTerrain(sceneContext); + generateUnderwaterTerrain(sceneContext, previousContext); timerGenerateUnderwaterTerrain = (int)(System.currentTimeMillis() - startTime); startTime = System.currentTimeMillis(); - calculateTerrainNormals(sceneContext); + calculateTerrainNormals(sceneContext, previousContext); timerCalculateTerrainNormals = (int)(System.currentTimeMillis() - startTime); startTime = System.currentTimeMillis(); - generateTerrainData(sceneContext); + generateTerrainData(sceneContext, previousContext); timerGenerateTerrainData = (int)(System.currentTimeMillis() - startTime); log.debug("procedural data generation took {}ms to complete", (System.currentTimeMillis() - timerTotal)); @@ -114,19 +117,19 @@ public void clearSceneData(SceneContext sceneContext) { * material data for each vertex of each Tile. Then adds the resulting * data to appropriate HashMaps. */ - private void generateTerrainData(SceneContext sceneContext) + private void generateTerrainData(SceneContext sceneContext, SceneContext previousContext) { - sceneContext.vertexTerrainColor = new HashMap<>(); + sceneContext.vertexTerrainColor = new Int2IntHashMap(previousContext != null && previousContext.vertexTerrainColor != null ? previousContext.vertexTerrainColor.size() : 16); // used for overriding potentially undesirable vertex colors // for example, colors that aren't supposed to be visible - sceneContext.highPriorityColor = new HashMap<>(); - sceneContext.vertexTerrainTexture = new HashMap<>(); + sceneContext.highPriorityColor = new IntHashSet(previousContext != null && previousContext.highPriorityColor != null ? previousContext.highPriorityColor.size() : 16); + sceneContext.vertexTerrainTexture = new Int2ObjectHashMap<>(previousContext != null && previousContext.vertexTerrainTexture != null ? previousContext.vertexTerrainTexture.size() : 16, Material[]::new); // for faces without an overlay is set to true - sceneContext.vertexIsUnderlay = new HashMap<>(); + sceneContext.vertexIsUnderlay = new IntHashSet(previousContext != null && previousContext.vertexIsUnderlay != null ? previousContext.vertexIsUnderlay.size() : 16); // for faces with an overlay is set to true // the result of these maps can be used to determine the vertices // between underlays and overlays for custom blending - sceneContext.vertexIsOverlay = new HashMap<>(); + sceneContext.vertexIsOverlay = new IntHashSet(previousContext != null && previousContext.vertexIsOverlay != null ? previousContext.vertexIsOverlay.size() : 16); Tile[][][] tiles = sceneContext.scene.getExtendedTiles(); int sizeX = sceneContext.sizeX; @@ -231,9 +234,14 @@ else if (tile.getSceneTileModel() != null) var overlayOverride = tileOverrideManager.getOverride(sceneContext, tile, worldPos, overlayId); var underlayOverride = tileOverrideManager.getOverride(sceneContext, tile, worldPos, underlayId); + final int[][] vertices = new int[4][3]; + final int[] vertexKeys = new int[4]; + final int[] faceColors = new int[3]; for (int face = 0; face < faceCount; face++) { - int[] faceColors = new int[]{faceColorsA[face], faceColorsB[face], faceColorsC[face]}; - int[] vertexKeys = faceVertexKeys(tile, face); + faceColors[0] = faceColorsA[face]; + faceColors[1] = faceColorsB[face]; + faceColors[2] = faceColorsC[face]; + faceVertexKeys(tile, face, vertices, vertexKeys); for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) { boolean isOverlay = isOverlayFace(tile, face); @@ -301,15 +309,15 @@ else if (tile.getSceneTileModel() != null) // this is used to determine how to blend between vertex colors if (isOverlay) { - sceneContext.vertexIsOverlay.put(vertexHashes[vertex], true); + sceneContext.vertexIsOverlay.add(vertexHashes[vertex]); } else { - sceneContext.vertexIsUnderlay.put(vertexHashes[vertex], true); + sceneContext.vertexIsUnderlay.add(vertexHashes[vertex]); } // add color and texture to hashmap - if ((!lowPriorityColor || !sceneContext.highPriorityColor.containsKey(vertexHashes[vertex])) && !vertexDefaultColor[vertex]) + if ((!lowPriorityColor || !sceneContext.highPriorityColor.contains(vertexHashes[vertex])) && !vertexDefaultColor[vertex]) { boolean shouldWrite = isOverlay || !sceneContext.vertexTerrainColor.containsKey(vertexHashes[vertex]); if (shouldWrite || !sceneContext.vertexTerrainColor.containsKey(vertexHashes[vertex])) @@ -319,7 +327,7 @@ else if (tile.getSceneTileModel() != null) sceneContext.vertexTerrainTexture.put(vertexHashes[vertex], material); if (!lowPriorityColor) - sceneContext.highPriorityColor.put(vertexHashes[vertex], true); + sceneContext.highPriorityColor.add(vertexHashes[vertex]); } } } @@ -329,23 +337,23 @@ else if (tile.getSceneTileModel() != null) * Scene, increasing the depth of each tile based on its distance from the shore. * Then stores the resulting data in a HashMap. */ - private void generateUnderwaterTerrain(SceneContext sceneContext) + private void generateUnderwaterTerrain(SceneContext sceneContext, SceneContext previousContext) { int sizeX = sceneContext.sizeX; int sizeY = sceneContext.sizeZ; // true if a tile contains at least 1 face which qualifies as water sceneContext.tileIsWater = new boolean[MAX_Z][sizeX][sizeY]; // true if a vertex is part of a face which qualifies as water; non-existent if not - sceneContext.vertexIsWater = new HashMap<>(); + sceneContext.vertexIsWater = new IntHashSet(previousContext != null && sceneContext.vertexIsWater != null ? sceneContext.vertexIsWater.size() : 0); // true if a vertex is part of a face which qualifies as land; non-existent if not // tiles along the shoreline will be true for both vertexIsWater and vertexIsLand - sceneContext.vertexIsLand = new HashMap<>(); + sceneContext.vertexIsLand = new IntHashSet(previousContext != null && sceneContext.vertexIsLand != null ? sceneContext.vertexIsLand.size() : 0); // if true, the tile will be skipped when the scene is drawn // this is due to certain edge cases with water on the same X/Y on different planes sceneContext.skipTile = new boolean[MAX_Z][sizeX][sizeY]; // the height adjustment for each vertex, to be applied to the vertex' // real height to create the underwater terrain - sceneContext.vertexUnderwaterDepth = new HashMap<>(); + sceneContext.vertexUnderwaterDepth = new Int2IntHashMap(previousContext != null && sceneContext.vertexUnderwaterDepth != null ? sceneContext.vertexUnderwaterDepth.size() : 0); // the basic 'levels' of underwater terrain, used to sink terrain based on its distance // from the shore, then used to produce the world-space height offset // 0 = land @@ -365,7 +373,9 @@ private void generateUnderwaterTerrain(SceneContext sceneContext) } Scene scene = sceneContext.scene; - Tile[][][] tiles = scene.getExtendedTiles(); + final Tile[][][] tiles = scene.getExtendedTiles(); + final int[][] vertices = new int[4][3]; + final int[] vertexKeys = new int[4]; // figure out which vertices are water and assign some data for (int z = 0; z < MAX_Z; ++z) { @@ -385,14 +395,14 @@ private void generateUnderwaterTerrain(SceneContext sceneContext) } if (tile.getSceneTilePaint() != null) { - int[] vertexKeys = tileVertexKeys(sceneContext, tile); + tileVertexKeys(sceneContext, tile, vertexKeys); int[] worldPos = sceneContext.extendedSceneToWorld(x, y, tile.getRenderLevel()); var override = tileOverrideManager.getOverride(sceneContext, tile, worldPos); if (seasonalWaterType(override, tile.getSceneTilePaint().getTexture()) == WaterType.NONE) { for (int vertexKey : vertexKeys) if (tile.getSceneTilePaint().getNeColor() != HIDDEN_HSL || override.forced) - sceneContext.vertexIsLand.put(vertexKey, true); + sceneContext.vertexIsLand.add(vertexKey); sceneContext.underwaterDepthLevels[z][x][y] = 0; sceneContext.underwaterDepthLevels[z][x + 1][y] = 0; @@ -427,7 +437,7 @@ private void generateUnderwaterTerrain(SceneContext sceneContext) for (int vertexKey : vertexKeys) { - sceneContext.vertexIsWater.put(vertexKey, true); + sceneContext.vertexIsWater.add(vertexKey); } } } @@ -489,8 +499,7 @@ else if (tile.getSceneTileModel() != null) for (int face = 0; face < faceCount; face++) { - int[][] vertices = faceVertices(tile, face); - int[] vertexKeys = faceVertexKeys(tile, face); + faceVertexKeys(tile, face, vertices, vertexKeys); var override = ProceduralGenerator.isOverlayFace(tile, face) ? overlayOverride : underlayOverride; int textureId = model.getTriangleTextureId() == null ? -1 : @@ -500,7 +509,7 @@ else if (tile.getSceneTileModel() != null) for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) { if (model.getTriangleColorA()[face] != HIDDEN_HSL || override.forced) - sceneContext.vertexIsLand.put(vertexKeys[vertex], true); + sceneContext.vertexIsLand.add(vertexKeys[vertex]); if (vertices[vertex][0] % LOCAL_TILE_SIZE == 0 && vertices[vertex][1] % LOCAL_TILE_SIZE == 0 @@ -518,7 +527,7 @@ else if (tile.getSceneTileModel() != null) for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) { - sceneContext.vertexIsWater.put(vertexKeys[vertex], true); + sceneContext.vertexIsWater.add(vertexKeys[vertex]); } } } @@ -620,7 +629,7 @@ else if (tile.getSceneTileModel() != null) tile = tile.getBridge(); } if (tile.getSceneTilePaint() != null) { - int[] vertexKeys = tileVertexKeys(sceneContext, tile); + tileVertexKeys(sceneContext, tile, vertexKeys); int swVertexKey = vertexKeys[0]; int seVertexKey = vertexKeys[1]; @@ -640,8 +649,7 @@ else if (tile.getSceneTileModel() != null) for (int face = 0; face < faceCount; face++) { - int[][] vertices = faceVertices(tile, face); - int[] vertexKeys = faceVertexKeys(tile, face); + faceVertexKeys(tile, face, vertices, vertexKeys); for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) { @@ -668,7 +676,7 @@ else if (tile.getSceneTileModel() != null) float southHeightOffset = mix(underwaterDepths[z][x][y], underwaterDepths[z][x + 1][y], lerpX); int heightOffset = (int) mix(southHeightOffset, northHeightOffset, lerpY); - if (!sceneContext.vertexIsLand.containsKey(vertexKeys[vertex])) + if (!sceneContext.vertexIsLand.contains(vertexKeys[vertex])) sceneContext.vertexUnderwaterDepth.put(vertexKeys[vertex], heightOffset); } } @@ -683,9 +691,9 @@ else if (tile.getSceneTileModel() != null) * Iterates through all Tiles in a given Scene, calculating vertex normals * for each one, then stores resulting normal data in a HashMap. */ - private void calculateTerrainNormals(SceneContext sceneContext) + private void calculateTerrainNormals(SceneContext sceneContext, SceneContext previousSceneContext) { - sceneContext.vertexTerrainNormals = new HashMap<>(); + sceneContext.vertexTerrainNormals = new Int2ObjectHashMap<>(previousSceneContext != null && previousSceneContext.vertexTerrainNormals != null ? previousSceneContext.vertexTerrainNormals.size() : 16, int[][]::new); for (Tile[][] plane : sceneContext.scene.getExtendedTiles()) { for (Tile[] column : plane) { @@ -703,11 +711,11 @@ private void calculateTerrainNormals(SceneContext sceneContext) } } - sceneContext.vertexTerrainNormals.forEach((key, normal) -> { - var n = normalize(vec(normal)); + for(var entry : sceneContext.vertexTerrainNormals) { + var n = normalize(vec(entry.value)); for (int i = 0; i < 3; i++) - normal[i] = GpuIntBuffer.normShort(n[i]); - }); + entry.value[i] = GpuIntBuffer.normShort(n[i]); + } } /** @@ -731,15 +739,16 @@ private void calculateNormalsForTile(SceneContext sceneContext, Tile tile, boole faceVertices = new int[tileModel.getFaceX().length][VERTICES_PER_FACE][3]; faceVertexKeys = new int[tileModel.getFaceX().length][VERTICES_PER_FACE]; + final int[][] vertices = new int[4][3]; + final int[] vertexKeys = new int[4]; for (int face = 0; face < tileModel.getFaceX().length; face++) { - int[][] vertices = faceVertices(tile, face); + faceVertexKeys(tile, face, vertices, vertexKeys); faceVertices[face][0] = new int[]{vertices[0][0], vertices[0][1], vertices[0][2]}; faceVertices[face][2] = new int[]{vertices[1][0], vertices[1][1], vertices[1][2]}; faceVertices[face][1] = new int[]{vertices[2][0], vertices[2][1], vertices[2][2]}; - int[] vertexKeys = faceVertexKeys(tile, face); faceVertexKeys[face][0] = vertexKeys[0]; faceVertexKeys[face][2] = vertexKeys[1]; faceVertexKeys[face][1] = vertexKeys[2]; @@ -788,11 +797,15 @@ private void calculateNormalsForTile(SceneContext sceneContext, Tile tile, boole ) ); - for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) - { + for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) { int vertexKey = faceVertexKeys[face][vertex]; - // accumulate normals to hashmap - sceneContext.vertexTerrainNormals.merge(vertexKey, vertexNormals, (a, b) -> add(a, a, b)); + + final int[] terrainNormal = sceneContext.vertexTerrainNormals.getOrDefault(vertexKey, null); + if (terrainNormal != null) { + add(terrainNormal, terrainNormal, vertexNormals); + } else { + sceneContext.vertexTerrainNormals.put(vertexKey, vertexNormals); + } } } } diff --git a/src/main/java/rs117/hd/scene/SceneContext.java b/src/main/java/rs117/hd/scene/SceneContext.java index 1852f4c08a..cd428369d0 100644 --- a/src/main/java/rs117/hd/scene/SceneContext.java +++ b/src/main/java/rs117/hd/scene/SceneContext.java @@ -15,6 +15,9 @@ import rs117.hd.scene.materials.Material; import rs117.hd.scene.tile_overrides.TileOverrideVariables; import rs117.hd.utils.HDUtils; +import rs117.hd.utils.collection.Int2IntHashMap; +import rs117.hd.utils.collection.Int2ObjectHashMap; +import rs117.hd.utils.collection.IntHashSet; import static net.runelite.api.Constants.*; import static net.runelite.api.Constants.SCENE_SIZE; @@ -55,20 +58,20 @@ public class SceneContext { public int uniqueModels; // Terrain data - public HashMap vertexTerrainColor; - public HashMap vertexTerrainTexture; - public HashMap vertexTerrainNormals; + public Int2IntHashMap vertexTerrainColor; + public Int2ObjectHashMap vertexTerrainTexture; + public Int2ObjectHashMap vertexTerrainNormals; // Used for overriding potentially low quality vertex colors - public HashMap highPriorityColor; + public IntHashSet highPriorityColor; // Water-related data public boolean[][][] tileIsWater; - public HashMap vertexIsWater; - public HashMap vertexIsLand; - public HashMap vertexIsOverlay; - public HashMap vertexIsUnderlay; + public IntHashSet vertexIsWater; + public IntHashSet vertexIsLand; + public IntHashSet vertexIsOverlay; + public IntHashSet vertexIsUnderlay; public boolean[][][] skipTile; - public HashMap vertexUnderwaterDepth; + public Int2IntHashMap vertexUnderwaterDepth; public int[][][] underwaterDepthLevels; // Thread safe tile override variables diff --git a/src/main/java/rs117/hd/scene/TileOverrideManager.java b/src/main/java/rs117/hd/scene/TileOverrideManager.java index 349db12cfd..a3faa9e029 100644 --- a/src/main/java/rs117/hd/scene/TileOverrideManager.java +++ b/src/main/java/rs117/hd/scene/TileOverrideManager.java @@ -25,6 +25,7 @@ import rs117.hd.utils.FileWatcher; import rs117.hd.utils.Props; import rs117.hd.utils.ResourcePath; +import rs117.hd.utils.collection.Int2ObjectHashMap; import static rs117.hd.scene.tile_overrides.TileOverride.OVERLAY_FLAG; import static rs117.hd.utils.HDUtils.localToWorld; @@ -53,7 +54,7 @@ public class TileOverrideManager { private FileWatcher.UnregisterCallback fileWatcher; private boolean trackReplacements; private List anyMatchOverrides; - private ListMultimap idMatchOverrides; + private Int2ObjectHashMap> idMatchOverrides; public void startUp() { fileWatcher = TILE_OVERRIDES_PATH.watch((path, first) -> clientThread.invoke(() -> reload(first))); @@ -91,7 +92,7 @@ public void reload(boolean skipSceneReload) { checkForReplacementLoops(allOverrides); List anyMatch = new ArrayList<>(); - ListMultimap idMatch = ArrayListMultimap.create(); + Int2ObjectHashMap> idMatch = new Int2ObjectHashMap<>((ArrayList[]::new)); var tileOverrideVars = plugin.vars.aliases(Map.of( "textures", "groundTextures" @@ -112,8 +113,12 @@ public void reload(boolean skipSceneReload) { override.replacement = trackReplacements ? override : override.resolveConstantReplacements(); if (override.ids != null) { - for (int id : override.ids) - idMatch.put(id, override); + for (int id : override.ids) { + List overrides = idMatch.get(id); + if(overrides == null) + idMatch.put(id, overrides = new ArrayList<>()); + overrides.add(override); + } } else { anyMatch.add(override); } @@ -251,6 +256,10 @@ public TileOverride getOverrideBeforeReplacements(@Nonnull int[] worldPos, int.. outer: for (int id : ids) { final var entries = idMatchOverrides.get(id); + if(entries == null) + continue; + // Enhanced for allocates an iterator... + // noinspection ForLoopReplaceableByForEach for (int i = 0; i < entries.size(); i++) { final var entry = entries.get(i); if (entry.area.containsPoint(worldPos)) { diff --git a/src/main/java/rs117/hd/scene/areas/Area.java b/src/main/java/rs117/hd/scene/areas/Area.java index 5e32d81286..11483f6964 100644 --- a/src/main/java/rs117/hd/scene/areas/Area.java +++ b/src/main/java/rs117/hd/scene/areas/Area.java @@ -25,6 +25,7 @@ public class Area { @JsonAdapter(AABB.ArrayAdapter.class) public AABB[] unhideAreas = {}; + public transient AABB aabbsBounds; public transient AABB[] aabbs; private transient boolean normalized; @@ -63,6 +64,17 @@ public void normalize() { } } + if(aabbs.size() > 1) { + int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE; + for (AABB aabb : aabbs) { + minX = Math.min(minX, aabb.minX); + minY = Math.min(minY, aabb.minY); + maxX = Math.max(maxX, aabb.maxX); + maxY = Math.max(maxY, aabb.maxY); + } + aabbsBounds = new AABB(minX, minY, maxX, maxY); + } + this.aabbs = aabbs.toArray(AABB[]::new); if (unhideAreas == null) @@ -70,9 +82,11 @@ public void normalize() { } public boolean containsPoint(boolean includeUnhiding, int... worldPoint) { - for (var aabb : aabbs) - if (aabb.contains(worldPoint)) - return true; + if (aabbsBounds == null || aabbsBounds.contains(worldPoint)) { + for (var aabb : aabbs) + if (aabb.contains(worldPoint)) + return true; + } if (includeUnhiding) for (var aabb : unhideAreas) if (aabb.contains(worldPoint)) @@ -85,9 +99,11 @@ public boolean containsPoint(int... worldPoint) { } public boolean intersects(boolean includeUnhiding, int minX, int minY, int maxX, int maxY) { - for (AABB aabb : aabbs) - if (aabb.intersects(minX, minY, maxX, maxY)) - return true; + if (aabbsBounds == null || aabbsBounds.intersects(minX, minY, maxX, maxY)) { + for (AABB aabb : aabbs) + if (aabb.intersects(minX, minY, maxX, maxY)) + return true; + } if (includeUnhiding) for (var aabb : unhideAreas) if (aabb.intersects(minX, minY, maxX, maxY)) diff --git a/src/main/java/rs117/hd/scene/model_overrides/ModelOverride.java b/src/main/java/rs117/hd/scene/model_overrides/ModelOverride.java index 46e26cbfb4..065a7a4a35 100644 --- a/src/main/java/rs117/hd/scene/model_overrides/ModelOverride.java +++ b/src/main/java/rs117/hd/scene/model_overrides/ModelOverride.java @@ -18,6 +18,7 @@ import rs117.hd.scene.areas.AABB; import rs117.hd.scene.materials.Material; import rs117.hd.utils.Props; +import rs117.hd.utils.collection.Int2IntCache; import static net.runelite.api.Perspective.*; import static rs117.hd.utils.ExpressionParser.asExpression; @@ -83,7 +84,7 @@ public class ModelOverride @JsonAdapter(AABB.ArrayAdapter.class) public AABB[] hideInAreas = {}; - public Map materialOverrides; + public HashMap materialOverrides; public ModelOverride[] colorOverrides; private JsonElement colors; @@ -95,6 +96,8 @@ public class ModelOverride public transient boolean mightHaveTransparency; public transient boolean modifiesVanillaTexture; + private transient final Int2IntCache aHslModelOverrideCache = new Int2IntCache(16, 512); + @FunctionalInterface public interface AhslPredicate { boolean test(int ahsl); @@ -471,13 +474,12 @@ private void computeBoxUvw(float[] out, Model model, int modelOrientation, int f v[2 * 3 + 1] = verticesY[vidx]; v[2 * 3 + 2] = verticesZ[vidx]; - float rad, cos, sin; + float cos, sin; float temp; if (modelOrientation % 2048 != 0) { // Reverse baked vertex rotation - rad = modelOrientation * JAU_TO_RAD; - cos = cos(rad); - sin = sin(rad); + cos = jauToCosF(modelOrientation); + sin = jauToSinF(modelOrientation); for (int i = 0; i < 3; i++) { temp = v[i * 3] * sin + v[i * 3 + 2] * cos; @@ -510,9 +512,8 @@ private void computeBoxUvw(float[] out, Model model, int modelOrientation, int f } if (uvOrientationX % 2048 != 0) { - rad = uvOrientationX * JAU_TO_RAD; - cos = cos(rad); - sin = sin(rad); + cos = jauToCosF(uvOrientationX); + sin = jauToSinF(uvOrientationX); for (int i = 0; i < 3; i++) { int j = i * 4; @@ -534,9 +535,8 @@ private void computeBoxUvw(float[] out, Model model, int modelOrientation, int f } if (uvOrientationY % 2048 != 0) { - rad = uvOrientationY * JAU_TO_RAD; - cos = cos(rad); - sin = sin(rad); + cos = jauToCosF(uvOrientationY); + sin = jauToSinF(uvOrientationY); for (int i = 0; i < 3; i++) { int j = i * 4; @@ -558,9 +558,8 @@ private void computeBoxUvw(float[] out, Model model, int modelOrientation, int f } if (uvOrientationZ % 2048 != 0) { - rad = uvOrientationZ * JAU_TO_RAD; - cos = cos(rad); - sin = sin(rad); + cos = jauToCosF(uvOrientationZ); + sin = jauToSinF(uvOrientationZ); for (int i = 0; i < 3; i++) { int j = i * 4; @@ -612,4 +611,23 @@ public void revertRotation(Model model) { break; } } + + public final ModelOverride testColorOverrides(int ahsl) { + final int overrideIdx = aHslModelOverrideCache.getOrDefault(ahsl, -1); + if (overrideIdx >= 0) + return colorOverrides[overrideIdx]; + if (overrideIdx == -2) + return null; + + for (int i = 0; i < colorOverrides.length; i++) { + final var override = colorOverrides[i]; + if (override.ahslCondition.test(ahsl)) { + aHslModelOverrideCache.put(ahsl, i); + return override; + } + } + + aHslModelOverrideCache.put(ahsl, -2); + return null; + } } diff --git a/src/main/java/rs117/hd/scene/tile_overrides/TileOverride.java b/src/main/java/rs117/hd/scene/tile_overrides/TileOverride.java index 7a8c8b8a0a..1adbfa5a53 100644 --- a/src/main/java/rs117/hd/scene/tile_overrides/TileOverride.java +++ b/src/main/java/rs117/hd/scene/tile_overrides/TileOverride.java @@ -231,7 +231,9 @@ public TileOverride resolveReplacements(VariableSupplier vars) { } public TileOverride resolveNextReplacement(VariableSupplier vars) { - for (var entry : replacements) { + //noinspection ForLoopReplaceableByForEach - Enhanced for allocates an iterator which generates allot of garbage during scene load + for (int i = 0; i < replacements.size(); i++) { + var entry = replacements.get(i); if (!entry.getKey().test(vars)) continue; diff --git a/src/main/java/rs117/hd/utils/MathUtils.java b/src/main/java/rs117/hd/utils/MathUtils.java index 87fc341f06..d6aefb2197 100644 --- a/src/main/java/rs117/hd/utils/MathUtils.java +++ b/src/main/java/rs117/hd/utils/MathUtils.java @@ -12,6 +12,8 @@ import java.util.Random; import javax.annotation.Nullable; +import static net.runelite.api.Perspective.*; + /** * Math utility functions similar to GLSL, including vector operations on raw float arrays. * Usability and conciseness is prioritized, however most methods at least allow avoiding unnecessary allocations. @@ -41,6 +43,18 @@ public final class MathUtils { public static final float JAU_TO_RAD = TWO_PI / 2048; public static final float RAD_TO_JAU = 1 / JAU_TO_RAD; + private static final float[] SINF = new float[2048]; + private static final float[] COSF = new float[2048]; + + static + { + // Duplicated from Perspective.class since SINF & COSF are private + for (int i = 0; i < 2048; ++i) { + SINF[i] = (float) Math.sin((double) i * UNIT); + COSF[i] = (float) Math.cos((double) i * UNIT); + } + } + public static float[] vec(float... vec) { return vec; } @@ -782,24 +796,37 @@ public static float sin(float rad) { return (float) Math.sin(rad); } + public static float jauToSinF(int JAU) { return SINF[mod(JAU, 2048)]; } + public static float cos(float rad) { return (float) Math.cos(rad); } + public static float jauToCosF(int JAU) { return COSF[mod(JAU, 2048)]; } + public static float tan(float rad) { return (float) Math.tan(rad); } + public static int floatToUnorm16(float f) { return (int)(f * 65535.0f) & 0xFFFF; } + public static int float16(float value) { - if (value == 0) + if(value == 0.0f) return 0; // float32: (-1)^sign * 2^(exponent - 127) * (1.mantissa) // float16: (-1)^sign * 2^(exponent - 15) * (1.mantissa) - int f = Float.floatToRawIntBits(value); - int sign = (f >>> 16) & 0x8000; + final int f = Float.floatToRawIntBits(value); + final int sign = (f >>> 16) & 0x8000; int exponent = ((f >>> 23) & 0xFF) - 127 + 15; int mantissa = f & 0x7FFFFF; + // Normalized range fast path + if (exponent > 0 && exponent < 0x1F) { + // round-to-nearest-even + mantissa += 0x1000; + return sign | (exponent << 10) | (mantissa >> 13); + } + if (exponent <= 0) { // Too small, subnormal if (exponent < -10) // To small to represent, return signed zero return sign; @@ -823,7 +850,6 @@ public static int float16(float value) { mantissa += 0x2000; // If rounding up caused the mantissa to overflow, increment the exponent if ((mantissa & 0x800000) != 0) { - mantissa = 0; exponent += 1; if (exponent >= 0x1F) // Return infinity if it's too large to represent again return sign | 0x7C00; // Infinity diff --git a/src/main/java/rs117/hd/utils/collection/Int2IntCache.java b/src/main/java/rs117/hd/utils/collection/Int2IntCache.java new file mode 100644 index 0000000000..1e7eafd336 --- /dev/null +++ b/src/main/java/rs117/hd/utils/collection/Int2IntCache.java @@ -0,0 +1,239 @@ +package rs117.hd.utils.collection; + +import java.util.Arrays; +import java.util.concurrent.locks.StampedLock; +import rs117.hd.utils.HDUtils; + +import static rs117.hd.utils.MathUtils.*; +import static rs117.hd.utils.collection.Util.DEFAULT_GROWTH; +import static rs117.hd.utils.collection.Util.EMPTY; +import static rs117.hd.utils.collection.Util.READ_CACHE_SIZE; +import static rs117.hd.utils.collection.Util.findIndex; +import static rs117.hd.utils.collection.Util.murmurHash3; + +public final class Int2IntCache { + private final int maxSize; + private final float growthFactor; + + private final StampedLock lock = new StampedLock(); + + private final long[] readCache = new long[READ_CACHE_SIZE]; + private int[] keys; + private int[] values; + private int[] ages; + private int[] distances; + + private int size; + private int mask; + private int ageCounter; + + public Int2IntCache(int initialCapacity, int maxSize) { + this(initialCapacity, maxSize, DEFAULT_GROWTH); + } + + public Int2IntCache(int initialCapacity, int maxSize, float growthFactor) { + int cap = max((int) HDUtils.ceilPow2(initialCapacity), 16); + + keys = new int[cap]; + values = new int[cap]; + ages = new int[cap]; + distances = new int[cap]; + + Arrays.fill(keys, EMPTY); + + this.mask = cap - 1; + this.maxSize = maxSize; + this.growthFactor = growthFactor; + } + + public int getOrDefault(int key, int defaultValue) { + long stamp = lock.tryOptimisticRead(); + int idx = findIndex(key, mask, keys, distances, readCache); + + if (!lock.validate(stamp)) { + stamp = lock.readLock(); + try { + idx = findIndex(key, mask, keys, distances, readCache); + } finally { + lock.unlockRead(stamp); + } + } + + if (idx >= 0) { + ages[idx] = ++ageCounter; + return values[idx]; + } + + return defaultValue; + } + + public void put(int key, int value) { + long stamp = lock.writeLock(); + try { + normalizeAgesIfNeeded(); + + if (size + 1.0 >= keys.length * Util.LOAD_FACTOR) + resize(); + + int idx = insertIndex(key); + values[idx] = value; + ages[idx] = ++ageCounter; + + if (size > maxSize) + evictOldest(); + } finally { + lock.unlockWrite(stamp); + } + } + + private int insertIndex(int key) { + final int[] keys = this.keys; + final int[] distances = this.distances; + + int idx = murmurHash3(key) & mask; + for (int dist = 0; ; dist++) { + final int k = keys[idx]; + + if (k == EMPTY) { + keys[idx] = key; + distances[idx] = dist; + size++; + return idx; + } + + if (k == key) + return idx; + + if (distances[idx] < dist) { + // Robin Hood swap + int tmpKey = keys[idx]; + int tmpVal = values[idx]; + int tmpAge = ages[idx]; + int tmpDist = distances[idx]; + + keys[idx] = key; + values[idx] = 0; + ages[idx] = 0; + distances[idx] = dist; + + key = tmpKey; + values[idx] = tmpVal; + ages[idx] = tmpAge; + dist = tmpDist; + } + + idx = (idx + 1) & mask; + dist++; + } + } + + private void resize() { + int newCap = (int) HDUtils.ceilPow2( + max((int) (keys.length * growthFactor), keys.length + 1) + ); + + int[] oldKeys = keys; + int[] oldValues = values; + int[] oldAges = ages; + + keys = new int[newCap]; + values = new int[newCap]; + ages = new int[newCap]; + distances = new int[newCap]; + + Arrays.fill(keys, EMPTY); + + size = 0; + mask = newCap - 1; + + for (int i = 0; i < oldKeys.length; i++) { + if (oldKeys[i] != EMPTY) { + int idx = insertIndex(oldKeys[i]); + values[idx] = oldValues[i]; + ages[idx] = oldAges[i]; + } + } + } + + private void evictOldest() { + int oldestIdx = -1; + int oldestAge = Integer.MAX_VALUE; + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != EMPTY && ages[i] < oldestAge) { + oldestAge = ages[i]; + oldestIdx = i; + } + } + + if (oldestIdx >= 0) + removeIndex(oldestIdx); + } + + private void removeIndex(int idx) { + keys[idx] = EMPTY; + values[idx] = 0; + ages[idx] = 0; + distances[idx] = 0; + size--; + + int last = idx; + while (true) { + int next = (last + 1) & mask; + if (keys[next] == EMPTY || distances[next] == 0) + break; + + keys[last] = keys[next]; + values[last] = values[next]; + ages[last] = ages[next]; + distances[last] = distances[next] - 1; + + keys[next] = EMPTY; + values[next] = 0; + ages[next] = 0; + distances[next] = 0; + + last = next; + } + } + + private void normalizeAgesIfNeeded() { + if ((ageCounter >>> 30) == 0) + return; + + int minAge = Integer.MAX_VALUE; + for (int i = 0; i < keys.length; i++) { + if (keys[i] != EMPTY) + minAge = Math.min(minAge, ages[i]); + } + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != EMPTY) + ages[i] -= minAge; + } + + ageCounter -= minAge; + } + + public int size() { + return size; + } + + public boolean isEmpty() { + return size == 0; + } + + public void clear() { + long stamp = lock.writeLock(); + try { + Arrays.fill(keys, EMPTY); + Arrays.fill(values, 0); + Arrays.fill(ages, 0); + Arrays.fill(distances, 0); + size = 0; + ageCounter = 0; + } finally { + lock.unlockWrite(stamp); + } + } +} \ No newline at end of file diff --git a/src/main/java/rs117/hd/utils/collection/Int2IntHashMap.java b/src/main/java/rs117/hd/utils/collection/Int2IntHashMap.java new file mode 100644 index 0000000000..ce87961965 --- /dev/null +++ b/src/main/java/rs117/hd/utils/collection/Int2IntHashMap.java @@ -0,0 +1,202 @@ +package rs117.hd.utils.collection; + +import java.util.Arrays; +import rs117.hd.utils.HDUtils; + +import static rs117.hd.utils.MathUtils.*; +import static rs117.hd.utils.collection.Util.DEFAULT_CAPACITY; +import static rs117.hd.utils.collection.Util.DEFAULT_GROWTH; +import static rs117.hd.utils.collection.Util.EMPTY; +import static rs117.hd.utils.collection.Util.LOAD_FACTOR; +import static rs117.hd.utils.collection.Util.READ_CACHE_SIZE; +import static rs117.hd.utils.collection.Util.findIndex; +import static rs117.hd.utils.collection.Util.murmurHash3; + +public final class Int2IntHashMap { + private final float growthFactor; + + private final long[] readCache = new long[READ_CACHE_SIZE]; + private int[] keys; + private int[] values; + private int[] distances; + + private int size; + private int mask; + + + public Int2IntHashMap() { + this(DEFAULT_CAPACITY, DEFAULT_GROWTH); + } + + public Int2IntHashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_GROWTH); + } + + public Int2IntHashMap(int initialCapacity, float growthFactor) { + int cap = max((int) HDUtils.ceilPow2(initialCapacity), DEFAULT_CAPACITY); + + keys = new int[cap]; + values = new int[cap]; + distances = new int[cap]; + + Arrays.fill(keys, EMPTY); + + this.growthFactor = growthFactor; + this.mask = cap - 1; + this.size = 0; + } + + private void resize() { + int newCapacity = (int) HDUtils.ceilPow2( + max((int) (keys.length * growthFactor), keys.length + 1) + ); + + int[] oldKeys = keys; + int[] oldValues = values; + + keys = new int[newCapacity]; + values = new int[newCapacity]; + distances = new int[newCapacity]; + + Arrays.fill(keys, EMPTY); + + mask = newCapacity - 1; + size = 0; + + for (int i = 0; i < oldKeys.length; i++) { + if (oldKeys[i] != EMPTY) { + put(oldKeys[i], oldValues[i]); + } + } + } + + public boolean put(Object key, int value) { return key != null && put(key.hashCode(), value); } + + public boolean put(int key, int value) { + return put(key, value, true); + } + + public boolean putIfAbsent(Object key, int value) { return key != null && put(key.hashCode(), value, false); } + + public boolean putIfAbsent(int key, int value) { + return put(key, value, false); + } + + private boolean put(int key, int value, boolean overwrite) { + if (size + 1.0 >= keys.length * LOAD_FACTOR) + resize(); + + final int[] keys = this.keys; + final int[] distances = this.distances; + + int idx = murmurHash3(key) & mask; + for (int dist = 0; ; dist++) { + final int k = keys[idx]; + + if (k == EMPTY) { + keys[idx] = key; + values[idx] = value; + distances[idx] = dist; + size++; + return true; + } + + if (k == key) { + if (overwrite) + values[idx] = value; + return false; + } + + // Robin Hood swap: steal slot if we've probed farther + if (distances[idx] < dist) { + int tmpKey = keys[idx]; + int tmpVal = values[idx]; + int tmpDist = distances[idx]; + + keys[idx] = key; + values[idx] = value; + distances[idx] = dist; + + key = tmpKey; + value = tmpVal; + dist = tmpDist; + } + + idx = (idx + 1) & mask; + dist++; + } + } + + public int getOrDefault(Object key, int defaultValue) { return key != null ? getOrDefault(key.hashCode(), defaultValue) : defaultValue; } + + public int getOrDefault(int key, int defaultValue) { + int idx = findIndex(key, mask, keys, distances, readCache); + return idx >= 0 ? values[idx] : defaultValue; + } + + public boolean containsKey(Object key) { return key != null && containsKey(key.hashCode()); } + + public boolean containsKey(int key) { + return findIndex(key, mask, keys, distances, readCache) >= 0; + } + + public int getValue(int idx) { + return values[idx]; + } + + public void setValue(int idx, int value) { + values[idx] = value; + } + + public boolean remove(Object key) { return key != null && remove(key.hashCode()); } + + public boolean remove(int key) { + int idx = findIndex(key, mask, keys, distances, readCache); + if (idx < 0) + return false; + + removeIndex(idx); + return true; + } + + public void removeIndex(int idx) { + keys[idx] = EMPTY; + values[idx] = 0; + distances[idx] = 0; + size--; + + int last = idx; + + // Shift backward while probe distance allows + while (true) { + int next = (last + 1) & mask; + if (keys[next] == EMPTY || distances[next] == 0) + break; + + keys[last] = keys[next]; + values[last] = values[next]; + distances[last] = distances[next] - 1; + + keys[next] = EMPTY; + values[next] = 0; + distances[next] = 0; + + last = next; + } + } + + public void clear() { + Arrays.fill(keys, EMPTY); + Arrays.fill(values, 0); + Arrays.fill(distances, 0); + size = 0; + } + + public boolean isEmpty() { + return size == 0; + } + + public int size() { + return size; + } +} diff --git a/src/main/java/rs117/hd/utils/collection/Int2ObjectHashMap.java b/src/main/java/rs117/hd/utils/collection/Int2ObjectHashMap.java new file mode 100644 index 0000000000..28194abea2 --- /dev/null +++ b/src/main/java/rs117/hd/utils/collection/Int2ObjectHashMap.java @@ -0,0 +1,282 @@ +package rs117.hd.utils.collection; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; +import rs117.hd.utils.HDUtils; + +import static rs117.hd.utils.MathUtils.*; +import static rs117.hd.utils.collection.Util.DEFAULT_CAPACITY; +import static rs117.hd.utils.collection.Util.DEFAULT_GROWTH; +import static rs117.hd.utils.collection.Util.EMPTY; +import static rs117.hd.utils.collection.Util.LOAD_FACTOR; +import static rs117.hd.utils.collection.Util.READ_CACHE_SIZE; +import static rs117.hd.utils.collection.Util.findIndex; +import static rs117.hd.utils.collection.Util.murmurHash3; + +public final class Int2ObjectHashMap implements Iterable> { + public interface Supplier { T[] get(int capacity); } + + private final Supplier defaultValueSupplier; + private final float growthFactor; + + private final long[] readCache = new long[READ_CACHE_SIZE]; + private int[] keys; + private T[] values; + private int[] distances; + + private int size; + private int mask; + + public Int2ObjectHashMap() { + this(DEFAULT_CAPACITY, DEFAULT_GROWTH, null); + } + + public Int2ObjectHashMap(Supplier defaultValueSupplier) { + this(DEFAULT_CAPACITY, DEFAULT_GROWTH, defaultValueSupplier); + } + + public Int2ObjectHashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_GROWTH, null); + } + + public Int2ObjectHashMap(int initialCapacity, Supplier defaultValueSupplier) { + this(initialCapacity, DEFAULT_GROWTH, defaultValueSupplier); + } + + @SuppressWarnings("unchecked") + public Int2ObjectHashMap(int initialCapacity, float growthFactor, Supplier defaultValueSupplier) { + this.defaultValueSupplier = + defaultValueSupplier != null + ? defaultValueSupplier + : (capacity) -> (T[]) new Object[capacity]; + + this.growthFactor = growthFactor; + + int cap = max((int) HDUtils.ceilPow2(initialCapacity), DEFAULT_CAPACITY); + + keys = new int[cap]; + values = this.defaultValueSupplier.get(cap); + distances = new int[cap]; + + Arrays.fill(keys, EMPTY); + + this.size = 0; + this.mask = cap - 1; + } + + private void resize() { + int newCapacity = (int) HDUtils.ceilPow2( + max((int) (keys.length * growthFactor), keys.length + 1) + ); + + int[] oldKeys = keys; + T[] oldValues = values; + + keys = new int[newCapacity]; + values = defaultValueSupplier.get(newCapacity); + distances = new int[newCapacity]; + + Arrays.fill(keys, EMPTY); + + mask = newCapacity - 1; + size = 0; + + for (int i = 0; i < oldKeys.length; i++) { + if (oldKeys[i] != EMPTY) { + put(oldKeys[i], oldValues[i]); + } + } + } + + public boolean put(int key, T value) { + return put(key, value, true); + } + + public boolean putIfAbsent(int key, T value) { + return put(key, value, false); + } + + private boolean put(int key, T value, boolean overwrite) { + if (size + 1.0 >= keys.length * LOAD_FACTOR) + resize(); + + final int[] keys = this.keys; + final int[] distances = this.distances; + + int idx = murmurHash3(key) & mask; + for (int dist = 0; ; dist++) { + final int k = keys[idx]; + + if (k == EMPTY) { + keys[idx] = key; + values[idx] = value; + distances[idx] = dist; + size++; + return true; + } + + if (k == key) { + if (overwrite) + values[idx] = value; + return false; + } + + // Robin Hood swap: steal slot if we probed farther + if (distances[idx] < dist) { + int tmpKey = keys[idx]; + T tmpVal = values[idx]; + int tmpDist = distances[idx]; + + keys[idx] = key; + values[idx] = value; + distances[idx] = dist; + + key = tmpKey; + value = tmpVal; + dist = tmpDist; + } + + idx = (idx + 1) & mask; + dist++; + } + } + + public T getOrDefault(Object key, T defaultValue) { + return key != null ? getOrDefault(key.hashCode(), defaultValue) : defaultValue; + } + + public T getOrDefault(int key, T defaultValue) { + int idx = findIndex(key, mask, keys, distances, readCache); + return idx >= 0 ? values[idx] : defaultValue; + } + + public T get(Object key) { + return key != null ? get(key.hashCode()) : null; + } + + public T get(int key) { + int idx = findIndex(key, mask, keys, distances, readCache); + return idx >= 0 ? values[idx] : null; + } + + public boolean containsKey(Object key) { return key != null && containsKey(key.hashCode()); } + + public boolean containsKey(int key) { + return findIndex(key, mask, keys, distances, readCache) >= 0; + } + + public T getValue(int idx) { + return values[idx]; + } + + public void setValue(int idx, T value) { + values[idx] = value; + } + + public boolean remove(Object key) { return key != null && remove(key.hashCode()); } + + public boolean remove(int key) { + int idx = findIndex(key, mask, keys, distances, readCache); + if (idx < 0) + return false; + + removeIndex(idx); + return true; + } + + public void removeIndex(int idx) { + keys[idx] = EMPTY; + values[idx] = null; + distances[idx] = 0; + size--; + + int last = idx; + + // Shift backward while probe distance allows + while (true) { + int next = (last + 1) & mask; + if (keys[next] == EMPTY || distances[next] == 0) + break; + + keys[last] = keys[next]; + values[last] = values[next]; + distances[last] = distances[next] - 1; + + keys[next] = EMPTY; + values[next] = null; + distances[next] = 0; + + last = next; + } + } + + public void clear() { + Arrays.fill(keys, EMPTY); + Arrays.fill(values, null); + Arrays.fill(distances, 0); + size = 0; + } + + public boolean isEmpty() { + return size == 0; + } + + public int size() { + return size; + } + + @Override + public Iterator> iterator() { + return new EntryIterator(); + } + + public static class Entry { + public final int key; + public T value; + + Entry(int key, T value) { + this.key = key; + this.value = value; + } + } + + private class EntryIterator implements Iterator> { + private int index = -1; + private int nextIndex = -1; + + EntryIterator() { + advance(); + } + + private void advance() { + do { + nextIndex++; + } while (nextIndex < keys.length && keys[nextIndex] == EMPTY); + } + + @Override + public boolean hasNext() { + return nextIndex < keys.length; + } + + @Override + public Entry next() { + if (!hasNext()) + throw new NoSuchElementException(); + + index = nextIndex; + advance(); + return new Entry<>(keys[index], values[index]); + } + + @Override + public void remove() { + if (index == -1) + throw new IllegalStateException(); + + removeIndex(index); + index = -1; + } + } +} diff --git a/src/main/java/rs117/hd/utils/collection/IntHashSet.java b/src/main/java/rs117/hd/utils/collection/IntHashSet.java new file mode 100644 index 0000000000..1c7021fde7 --- /dev/null +++ b/src/main/java/rs117/hd/utils/collection/IntHashSet.java @@ -0,0 +1,193 @@ +package rs117.hd.utils.collection; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; +import rs117.hd.utils.HDUtils; + +import static rs117.hd.utils.MathUtils.*; +import static rs117.hd.utils.collection.Util.DEFAULT_CAPACITY; +import static rs117.hd.utils.collection.Util.DEFAULT_GROWTH; +import static rs117.hd.utils.collection.Util.EMPTY; +import static rs117.hd.utils.collection.Util.LOAD_FACTOR; +import static rs117.hd.utils.collection.Util.READ_CACHE_SIZE; +import static rs117.hd.utils.collection.Util.findIndex; +import static rs117.hd.utils.collection.Util.murmurHash3; + +public final class IntHashSet implements Iterable { + private final float growthFactor; + + private final long[] readCache = new long[READ_CACHE_SIZE]; + private int[] keys; + private int[] distances; + + private int size; + private int mask; + + public IntHashSet() { + this(DEFAULT_CAPACITY, DEFAULT_GROWTH); + } + + public IntHashSet(int initialCapacity) { + this(initialCapacity, DEFAULT_GROWTH); + } + + public IntHashSet(int initialCapacity, float growthFactor) { + int cap = max((int) HDUtils.ceilPow2(initialCapacity), DEFAULT_CAPACITY); + + keys = new int[cap]; + distances = new int[cap]; + + Arrays.fill(keys, EMPTY); + + this.growthFactor = growthFactor; + this.size = 0; + this.mask = cap - 1; + } + + private void resize() { + int newCapacity = (int) HDUtils.ceilPow2( + max((int) (keys.length * growthFactor), keys.length + 1) + ); + + int[] oldKeys = keys; + + keys = new int[newCapacity]; + distances = new int[newCapacity]; + + Arrays.fill(keys, EMPTY); + + size = 0; + mask = newCapacity - 1; + + for (int i = 0; i < oldKeys.length; i++) { + int key = oldKeys[i]; + if (key != EMPTY) { + add(key); + } + } + } + + public boolean add(Object key) { return key != null && add(key.hashCode()); } + + public boolean add(int key) { + if (size + 1.0 >= keys.length * LOAD_FACTOR) + resize(); + + final int[] keys = this.keys; + final int[] distances = this.distances; + + int idx = murmurHash3(key) & mask; + for (int dist = 0; ; dist++) { + final int k = keys[idx]; + + if (k == EMPTY) { + keys[idx] = key; + distances[idx] = dist; + size++; + return true; + } + + if (k == key) + return false; // already present + + // Robin Hood swap + if (distances[idx] < dist) { + int tmpKey = keys[idx]; + int tmpDist = distances[idx]; + + keys[idx] = key; + distances[idx] = dist; + + key = tmpKey; + dist = tmpDist; + } + + idx = (idx + 1) & mask; + dist++; + } + } + + public boolean contains(Object key) {return key != null && contains(key.hashCode()); } + + public boolean contains(int key) { + return findIndex(key, mask, keys, distances, readCache) >= 0; + } + + public boolean remove(Object key) { return key != null && remove(key.hashCode()); } + + public boolean remove(int key) { + int idx = findIndex(key, mask, keys, distances, readCache); + if (idx < 0) + return false; + + removeIndex(idx); + return true; + } + + private void removeIndex(int idx) { + keys[idx] = EMPTY; + distances[idx] = 0; + size--; + + int last = idx; + + // Shift backward while probe distance allows + while (true) { + int next = (last + 1) & mask; + if (keys[next] == EMPTY || distances[next] == 0) + break; + + keys[last] = keys[next]; + distances[last] = distances[next] - 1; + + keys[next] = EMPTY; + distances[next] = 0; + + last = next; + } + } + + public void clear() { + Arrays.fill(keys, EMPTY); + Arrays.fill(distances, 0); + size = 0; + } + + public boolean isEmpty() { + return size == 0; + } + + public int size() { + return size; + } + + @Override + public Iterator iterator() { + return new Iterator<>() { + private int index = -1; + private int visited = 0; + + @Override + public boolean hasNext() { + return visited < size; + } + + @Override + public Integer next() { + if (!hasNext()) + throw new NoSuchElementException(); + + while (++index < keys.length) { + int key = keys[index]; + if (key != EMPTY) { + visited++; + return key; + } + } + + throw new NoSuchElementException(); + } + }; + } +} diff --git a/src/main/java/rs117/hd/utils/collection/Util.java b/src/main/java/rs117/hd/utils/collection/Util.java new file mode 100644 index 0000000000..f3037f2458 --- /dev/null +++ b/src/main/java/rs117/hd/utils/collection/Util.java @@ -0,0 +1,51 @@ +package rs117.hd.utils.collection; + +public final class Util { + public static final int DEFAULT_CAPACITY = 16; + public static final int EMPTY = Integer.MIN_VALUE; + public static final float LOAD_FACTOR = 0.7f; + public static final float DEFAULT_GROWTH = 1.5f; + public static final int READ_CACHE_SIZE = 4; + + public static int murmurHash3(int x) { + x ^= x >>> 16; + x *= 0x85ebca6b; + x ^= x >>> 13; + x *= 0xc2b2ae35; + x ^= x >>> 16; + return x; + } + + public static long murmurHash3(long x) { + x ^= x >>> 33; + x *= 0xff51afd7ed558ccdL; + x ^= x >>> 33; + x *= 0xc4ceb9fe1a85ec53L; + x ^= x >>> 33; + return x; + } + + public static int findIndex(final int key, final int mask, final int[] keys, final int[] distances, final long[] readCache) { + final int cachePos = key & (READ_CACHE_SIZE - 1); + final long lastRead = readCache[cachePos]; + if ((int) lastRead == key) + return (int) (lastRead >>> 32); + + int idx = murmurHash3(key) & mask; + for (int dist = 0; dist == 0 || distances[idx] >= dist; dist++) { + final int k = keys[idx]; + + if (k == EMPTY) + break; + + if (k == key) { + readCache[cachePos] = ((long) idx << 32) | (key & 0xFFFFFFFFL); + return idx; + } + + idx = (idx + 1) & mask; + } + + return -1; + } +}