From b2dc8d6c2a90d2f0871f1bc3411108bce8be61ea Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Fri, 19 Dec 2025 08:15:26 +0000 Subject: [PATCH 1/2] Implemented Primitive Int Maps/Set/Cache Reduced ProcGen Garbage Added Mummur Hash to reduce collisions Improved LinearProbe by caching the accessed key, instead of reading it twice Added IntHashSet Switched Maps over to IntHashSet since the additional value was unnecessary Pass in worldPos when getting tileOverride since it's already calculated Fixed Iterator generating 500MB of garbage during scene load Cache is read on multiple threads, we cannot guarantee the last alhsl value will be fully written in time Cache the last searched key result Added Bounds around multiple AABBs to allow for early out Fixed Double Linear Probe Implemented Robin Hood Hashing Dynamic Model Upload Optimizations Calculate Int Bits of Vertex position once instead of multiple times per face Added Normalized value fast path for fp32 -> fp16 conversion Calculate invLength instead of diving each axis undoVanillaShading Tweaks colorAdjust only needs to be calculated within the inner part of the if, otherwise the value isn't used We can use nDotL instead of the length Early out if value should be ignored Early out on nDotL Made FindIndex static since its consistent across all collections Refactored LastRead into a 32 Byte ReadCache findIndex optimizations Switch GLState IntSet to use IntHashSet & not the generic HashSet Tweaks Pre-Compute SINE/COSINE Float Arrays matching Perspective.SINF/COSF Tweaks Add Object Support to Int Map Collections Fixes due to master rebase Sync Sync Sync --- src/main/java/rs117/hd/opengl/GLState.java | 5 +- .../renderer/legacy/LegacySceneUploader.java | 30 +- .../rs117/hd/renderer/zone/SceneManager.java | 23 +- .../rs117/hd/renderer/zone/SceneUploader.java | 95 +++--- .../hd/renderer/zone/VertexWriteCache.java | 27 +- .../java/rs117/hd/renderer/zone/Zone.java | 17 +- .../rs117/hd/scene/ModelOverrideManager.java | 13 +- .../rs117/hd/scene/ProceduralGenerator.java | 101 ++++--- .../java/rs117/hd/scene/SceneContext.java | 21 +- .../rs117/hd/scene/TileOverrideManager.java | 17 +- src/main/java/rs117/hd/scene/areas/Area.java | 28 +- .../scene/model_overrides/ModelOverride.java | 46 ++- .../hd/scene/tile_overrides/TileOverride.java | 4 +- src/main/java/rs117/hd/utils/MathUtils.java | 34 ++- .../hd/utils/collection/Int2IntCache.java | 239 +++++++++++++++ .../hd/utils/collection/Int2IntHashMap.java | 202 +++++++++++++ .../utils/collection/Int2ObjectHashMap.java | 282 ++++++++++++++++++ .../rs117/hd/utils/collection/IntHashSet.java | 193 ++++++++++++ .../java/rs117/hd/utils/collection/Util.java | 51 ++++ 19 files changed, 1248 insertions(+), 180 deletions(-) create mode 100644 src/main/java/rs117/hd/utils/collection/Int2IntCache.java create mode 100644 src/main/java/rs117/hd/utils/collection/Int2IntHashMap.java create mode 100644 src/main/java/rs117/hd/utils/collection/Int2ObjectHashMap.java create mode 100644 src/main/java/rs117/hd/utils/collection/IntHashSet.java create mode 100644 src/main/java/rs117/hd/utils/collection/Util.java 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..3be6e881bb 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; @@ -2078,12 +2075,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 +2102,7 @@ else if (color3 == -1) texturedFaceIdx ); } - + writeCache.flush(); } @@ -2113,26 +2112,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; + } +} From 3bd5aaf19ceda1b691dff88fa99674903031f046 Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:27:01 +0000 Subject: [PATCH 2/2] Sync --- .../java/rs117/hd/renderer/zone/SceneUploader.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java index 3be6e881bb..6dd49f654a 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java @@ -1835,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; } }