diff --git a/src/main/java/rs117/hd/HdPlugin.java b/src/main/java/rs117/hd/HdPlugin.java index 5819dac606..15c6f860d8 100644 --- a/src/main/java/rs117/hd/HdPlugin.java +++ b/src/main/java/rs117/hd/HdPlugin.java @@ -128,6 +128,7 @@ import rs117.hd.utils.ResourcePath; import rs117.hd.utils.ShaderRecompile; import rs117.hd.utils.buffer.GLBuffer; +import rs117.hd.utils.collections.PooledArrayType; import rs117.hd.utils.jobs.GenericJob; import rs117.hd.utils.jobs.JobSystem; @@ -790,6 +791,7 @@ protected void shutDown() { materialManager.shutDown(); textureManager.shutDown(); + PooledArrayType.shutdown(); DestructibleHandler.flushPendingDestruction(true); if (awtContext != null) @@ -2018,6 +2020,8 @@ public void onBeforeRender(BeforeRender beforeRender) { if (ctx != null) ctx.scene.setMinLevel(ctx.isInChambersOfXeric ? client.getPlane() : ctx.scene.getMinLevel()); + gamevalManager.update(); + DestructibleHandler.flushPendingDestruction(); } diff --git a/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java b/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java index d596fc9efb..7ef5237ddb 100644 --- a/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java +++ b/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java @@ -22,6 +22,7 @@ import rs117.hd.renderer.zone.ZoneRenderer; import rs117.hd.utils.FrameTimingsRecorder; import rs117.hd.utils.NpcDisplacementCache; +import rs117.hd.utils.collections.PooledArrayType; import rs117.hd.utils.jobs.JobSystem; import static rs117.hd.renderer.zone.SceneManager.MAX_WORLDVIEWS; @@ -151,6 +152,11 @@ public Dimension render(Graphics2D g) { .right(format("%d ns", frameTimer.errorCompensation)) .build()); + children.add(LineComponent.builder() + .left("Pooled Array Size:") + .right(formatBytes(PooledArrayType.getCurrentTotalCacheSize())) + .build()); + children.add(LineComponent.builder() .left("Garbage collection count:") .right(String.valueOf(plugin.getGarbageCollectionCount())) diff --git a/src/main/java/rs117/hd/overlays/TileInfoOverlay.java b/src/main/java/rs117/hd/overlays/TileInfoOverlay.java index a951bb255a..4e30340132 100644 --- a/src/main/java/rs117/hd/overlays/TileInfoOverlay.java +++ b/src/main/java/rs117/hd/overlays/TileInfoOverlay.java @@ -138,6 +138,8 @@ public class TileInfoOverlay extends Overlay implements MouseListener, MouseWhee private int hoveredGamevalsHash; private int copiedGamevalsHash; + private GamevalManager.Handle gameValHandle; + public TileInfoOverlay() { setLayer(OverlayLayer.ABOVE_SCENE); setPosition(OverlayPosition.DYNAMIC); @@ -150,10 +152,14 @@ public void setActive(boolean activate) { // Listen to events before they're possibly consumed in DeveloperTools mouseManager.registerMouseListener(0, this); mouseManager.registerMouseWheelListener(this); + gameValHandle = gamevalManager.obtainHandle(); } else { overlayManager.remove(this); mouseManager.unregisterMouseListener(this); mouseManager.unregisterMouseWheelListener(this); + if(gameValHandle != null) + gameValHandle.close(); + gameValHandle = null; } tileOverrideManager.setTrackReplacements(activate); } @@ -719,7 +725,7 @@ private Rectangle drawTileInfo(Graphics2D g, SceneContext ctx, Tile tile, Polygo for (var spotanim : actor.getSpotAnims()) { sb .append(separator) - .append(gamevalManager.getSpotanimName(spotanim.getId())) + .append(gameValHandle.getSpotanimName(spotanim.getId())) .append(" (").append(spotanim.getId()).append(")") .append(" frame=").append(spotanim.getFrame()) .append(" cycle=").append(client.getGameCycle() - spotanim.getStartCycle()); @@ -738,7 +744,7 @@ private Rectangle drawTileInfo(Graphics2D g, SceneContext ctx, Tile tile, Polygo int x = lp.getSceneX(); int y = lp.getSceneY(); if (x - size <= tileX && tileX <= x + size && y - size <= tileY && tileY <= y + size) { - var name = gamevalManager.getNpcName(npc.getId()); + var name = gameValHandle.getNpcName(npc.getId()); hoveredGamevals.add(name); lines.add(String.format( "NPC: %s (%d) name=%s ori=[%d,%d] anim=%d impostor=?%s", @@ -756,7 +762,7 @@ private Rectangle drawTileInfo(Graphics2D g, SceneContext ctx, Tile tile, Polygo for (GraphicsObject graphicsObject : client.getGraphicsObjects()) { var lp = graphicsObject.getLocation(); if (lp.getSceneX() == tileX && lp.getSceneY() == tileY) { - var name = gamevalManager.getSpotanimName(graphicsObject.getId()); + var name = gameValHandle.getSpotanimName(graphicsObject.getId()); var anim = graphicsObject.getAnimation(); hoveredGamevals.add(name); lines.add(String.format( @@ -899,13 +905,13 @@ private Rectangle drawTileInfo(Graphics2D g, SceneContext ctx, Tile tile, Polygo private String getIdAndImpostorId(TileObject object, @Nullable Renderable renderable) { int id = object.getId(); int impostorId = getIdOrImpostorId(object, renderable); - String name = gamevalManager.getObjectName(id); + String name = gameValHandle.getObjectName(id); if (id == impostorId) { hoveredGamevals.add(name); return String.format("%s (%d)", name, id); } - String impostorName = gamevalManager.getObjectName(impostorId); + String impostorName = gameValHandle.getObjectName(impostorId); hoveredGamevals.add(impostorName); return String.format("%s (%d) -> %s (%d)", name, id, impostorName, impostorId); } diff --git a/src/main/java/rs117/hd/renderer/legacy/LegacyModelPusher.java b/src/main/java/rs117/hd/renderer/legacy/LegacyModelPusher.java index f00c54fd45..d3e429b27a 100644 --- a/src/main/java/rs117/hd/renderer/legacy/LegacyModelPusher.java +++ b/src/main/java/rs117/hd/renderer/legacy/LegacyModelPusher.java @@ -72,6 +72,7 @@ public class LegacyModelPusher { private static final int[] ZEROED_INTS = new int[12]; + private final int[] tzHaarRecolored = new int[3]; private ModelCache modelCache; public void startUp() { @@ -387,7 +388,7 @@ public void pushModel( sceneContext.modelPusherResults[1] = texturedFaceCount; } - private void getNormalDataForFace(SceneContext sceneContext, Model model, @Nonnull ModelOverride modelOverride, int face) { + private void getNormalDataForFace(LegacySceneContext sceneContext, Model model, @Nonnull ModelOverride modelOverride, int face) { assert packTerrainData(false, 0, WaterType.NONE, 0) == 0; if (modelOverride.flatNormals || !plugin.configPreserveVanillaNormals && model.getFaceColors3()[face] == -1) { Arrays.fill(sceneContext.modelFaceNormals, 0); @@ -424,7 +425,7 @@ private void getNormalDataForFace(SceneContext sceneContext, Model model, @Nonnu @SuppressWarnings({ "ReassignedVariable" }) private int[] getFaceVertices( - SceneContext sceneContext, + LegacySceneContext sceneContext, Tile tile, int uuid, Model model, @@ -587,13 +588,14 @@ private int[] getFaceVertices( } if (plugin.configLegacyTzHaarReskin && modelOverride.tzHaarRecolorType != TzHaarRecolorType.NONE) { - int[] tzHaarRecolored = ProceduralGenerator.recolorTzHaar( + ProceduralGenerator.recolorTzHaar( modelOverride, model, face, color1, color2, - color3 + color3, + tzHaarRecolored ); color1 = tzHaarRecolored[0]; color2 = tzHaarRecolored[1]; diff --git a/src/main/java/rs117/hd/renderer/legacy/LegacySceneContext.java b/src/main/java/rs117/hd/renderer/legacy/LegacySceneContext.java index 830e8e9320..2235c17808 100644 --- a/src/main/java/rs117/hd/renderer/legacy/LegacySceneContext.java +++ b/src/main/java/rs117/hd/renderer/legacy/LegacySceneContext.java @@ -6,6 +6,7 @@ import rs117.hd.utils.buffer.GpuFloatBuffer; import rs117.hd.utils.buffer.GpuIntBuffer; +import static net.runelite.api.Constants.*; import static rs117.hd.utils.MathUtils.*; public class LegacySceneContext extends SceneContext { @@ -14,11 +15,28 @@ public class LegacySceneContext extends SceneContext { public boolean isPrepared; public boolean forceDisableAreaHiding; + public byte[][][] underwaterDepthLevels; + public GpuIntBuffer staticUnorderedModelBuffer; public GpuIntBuffer stagingBufferVertices; public GpuFloatBuffer stagingBufferUvs; public GpuFloatBuffer stagingBufferNormals; + public int staticVertexCount = 0; + public int staticGapFillerTilesOffset; + public int staticGapFillerTilesVertexCount; + public int staticCustomTilesOffset; + public int staticCustomTilesVertexCount; + + // Statistics + public int uniqueModels; + + // Model pusher arrays, to avoid simultaneous usage from different threads + public final int[] modelFaceVertices = new int[12]; + public final float[] modelFaceUvs = new float[12]; + public final float[] modelFaceNormals = new float[12]; + public final int[] modelPusherResults = new int[2]; + public LegacySceneContext( Client client, Scene scene, @@ -27,6 +45,8 @@ public LegacySceneContext( ) { super(client, scene, expandedMapLoadingChunks); + underwaterDepthLevels = new byte[MAX_Z][EXTENDED_SCENE_SIZE][EXTENDED_SCENE_SIZE]; + if (previous == null) { staticUnorderedModelBuffer = new GpuIntBuffer(); stagingBufferVertices = new GpuIntBuffer(); diff --git a/src/main/java/rs117/hd/renderer/legacy/LegacySceneUploader.java b/src/main/java/rs117/hd/renderer/legacy/LegacySceneUploader.java index f56f1a54bb..6db514825b 100644 --- a/src/main/java/rs117/hd/renderer/legacy/LegacySceneUploader.java +++ b/src/main/java/rs117/hd/renderer/legacy/LegacySceneUploader.java @@ -54,6 +54,8 @@ import static net.runelite.api.Constants.*; import static net.runelite.api.Constants.SCENE_SIZE; import static net.runelite.api.Perspective.*; +import static rs117.hd.scene.SceneContext.TILE_SKIP_FLAG; +import static rs117.hd.scene.SceneContext.TILE_WATER_FLAG; import static rs117.hd.scene.tile_overrides.TileOverride.NONE; import static rs117.hd.scene.tile_overrides.TileOverride.OVERLAY_FLAG; import static rs117.hd.utils.HDUtils.HIDDEN_HSL; @@ -68,7 +70,7 @@ public class LegacySceneUploader { public static final int SCENE_ID_MASK = 0xFFFF; public static final int EXCLUDED_FROM_SCENE_BUFFER = 0xFFFFFFFF; - private static final int[] UP_NORMAL = { 0, -1, 0 }; + private static final short[] UP_NORMAL = { 0, -1, 0 }; @Inject private Client client; @@ -98,7 +100,7 @@ public class LegacySceneUploader { private LegacyModelPusher modelPusher; public void upload(LegacySceneContext sceneContext) { - proceduralGenerator.generateSceneData(sceneContext); + proceduralGenerator.generateSceneData(sceneContext, null); Stopwatch stopwatch = Stopwatch.createStarted(); @@ -643,10 +645,10 @@ private int[] uploadHDTilePaintSurface( Material neMaterial = Material.NONE; Material nwMaterial = Material.NONE; - int[] swNormals = UP_NORMAL; - int[] seNormals = UP_NORMAL; - int[] neNormals = UP_NORMAL; - int[] nwNormals = UP_NORMAL; + short[] swNormals = UP_NORMAL; + short[] seNormals = UP_NORMAL; + short[] neNormals = UP_NORMAL; + short[] nwNormals = UP_NORMAL; if (waterType == WaterType.NONE) { if (textureId != -1) { @@ -657,10 +659,10 @@ private int[] uploadHDTilePaintSurface( swMaterial = seMaterial = neMaterial = nwMaterial = material; } - swNormals = sceneContext.vertexTerrainNormals.getOrDefault(swVertexKey, swNormals); - seNormals = sceneContext.vertexTerrainNormals.getOrDefault(seVertexKey, seNormals); - neNormals = sceneContext.vertexTerrainNormals.getOrDefault(neVertexKey, neNormals); - nwNormals = sceneContext.vertexTerrainNormals.getOrDefault(nwVertexKey, nwNormals); + swNormals = sceneContext.getVertexNormalOrDefault(swVertexKey, new short[3], UP_NORMAL); + seNormals = sceneContext.getVertexNormalOrDefault(seVertexKey, new short[3], UP_NORMAL); + neNormals = sceneContext.getVertexNormalOrDefault(neVertexKey, new short[3], UP_NORMAL); + nwNormals = sceneContext.getVertexNormalOrDefault(nwVertexKey, new short[3], UP_NORMAL); boolean useBlendedMaterialAndColor = plugin.configGroundBlending && @@ -707,23 +709,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.isVertexWater(swVertexKey) && sceneContext.isVertexLand(swVertexKey)) swColor = 0; - if (sceneContext.vertexIsWater.containsKey(seVertexKey) && sceneContext.vertexIsLand.containsKey(seVertexKey)) + if (sceneContext.isVertexWater(seVertexKey) && sceneContext.isVertexLand(seVertexKey)) seColor = 0; - if (sceneContext.vertexIsWater.containsKey(nwVertexKey) && sceneContext.vertexIsLand.containsKey(nwVertexKey)) + if (sceneContext.isVertexWater(nwVertexKey) && sceneContext.isVertexLand(nwVertexKey)) nwColor = 0; - if (sceneContext.vertexIsWater.containsKey(neVertexKey) && sceneContext.vertexIsLand.containsKey(neVertexKey)) + if (sceneContext.isVertexWater(neVertexKey) && sceneContext.isVertexLand(neVertexKey)) neColor = 0; } - if (sceneContext.vertexIsOverlay.containsKey(neVertexKey) && sceneContext.vertexIsUnderlay.containsKey(neVertexKey)) + if (sceneContext.isVertexOverlay(neVertexKey) && sceneContext.isVertexUnderlay(neVertexKey)) neVertexIsOverlay = true; - if (sceneContext.vertexIsOverlay.containsKey(nwVertexKey) && sceneContext.vertexIsUnderlay.containsKey(nwVertexKey)) + if (sceneContext.isVertexOverlay(nwVertexKey) && sceneContext.isVertexUnderlay(nwVertexKey)) nwVertexIsOverlay = true; - if (sceneContext.vertexIsOverlay.containsKey(seVertexKey) && sceneContext.vertexIsUnderlay.containsKey(seVertexKey)) + if (sceneContext.isVertexOverlay(seVertexKey) && sceneContext.isVertexUnderlay(seVertexKey)) seVertexIsOverlay = true; - if (sceneContext.vertexIsOverlay.containsKey(swVertexKey) && sceneContext.vertexIsUnderlay.containsKey(swVertexKey)) + if (sceneContext.isVertexOverlay(swVertexKey) && sceneContext.isVertexUnderlay(swVertexKey)) swVertexIsOverlay = true; swHeight -= override.heightOffset; @@ -829,7 +831,7 @@ private int[] uploadHDTilePaintUnderwater(LegacySceneContext sceneContext, Tile int nwVertexKey = vertexKeys[2]; int neVertexKey = vertexKeys[3]; - if (sceneContext.tileIsWater[tileZ][tileExX][tileExY]) { + if (sceneContext.isTileFlagSet(tileZ, tileExX, tileExY, TILE_WATER_FLAG)) { // underwater terrain underwaterTerrain = 1; @@ -837,15 +839,15 @@ private int[] uploadHDTilePaintUnderwater(LegacySceneContext sceneContext, Tile int swColor, seColor, neColor, nwColor; swColor = seColor = neColor = nwColor = UNDERWATER_HSL; - int swDepth = sceneContext.vertexUnderwaterDepth.getOrDefault(swVertexKey, 0); - int seDepth = sceneContext.vertexUnderwaterDepth.getOrDefault(seVertexKey, 0); - int nwDepth = sceneContext.vertexUnderwaterDepth.getOrDefault(nwVertexKey, 0); - int neDepth = sceneContext.vertexUnderwaterDepth.getOrDefault(neVertexKey, 0); + int swDepth = sceneContext.getVertexUnderwaterDepth(swVertexKey); + int seDepth = sceneContext.getVertexUnderwaterDepth(seVertexKey); + int nwDepth = sceneContext.getVertexUnderwaterDepth(nwVertexKey); + int neDepth = sceneContext.getVertexUnderwaterDepth(neVertexKey); - int[] swNormals = sceneContext.vertexTerrainNormals.getOrDefault(swVertexKey, UP_NORMAL); - int[] seNormals = sceneContext.vertexTerrainNormals.getOrDefault(seVertexKey, UP_NORMAL); - int[] nwNormals = sceneContext.vertexTerrainNormals.getOrDefault(nwVertexKey, UP_NORMAL); - int[] neNormals = sceneContext.vertexTerrainNormals.getOrDefault(neVertexKey, UP_NORMAL); + short[] swNormals = sceneContext.getVertexNormalOrDefault(swVertexKey, new short[3], UP_NORMAL); + short[] seNormals = sceneContext.getVertexNormalOrDefault(seVertexKey, new short[3], UP_NORMAL); + short[] nwNormals = sceneContext.getVertexNormalOrDefault(nwVertexKey, new short[3], UP_NORMAL); + short[] neNormals = sceneContext.getVertexNormalOrDefault(neVertexKey, new short[3], UP_NORMAL); Material swMaterial = Material.NONE; Material seMaterial = Material.NONE; @@ -943,7 +945,7 @@ private int[] uploadHDTileModelSurface( final int tileExY = tileY + sceneContext.sceneOffset; final int tileZ = tile.getRenderLevel(); - if (!fillGaps && sceneContext.skipTile[tileZ][tileExX][tileExY]) + if (!fillGaps && sceneContext.isTileFlagSet(tileZ, tileExX, tileExY, TILE_SKIP_FLAG)) return new int[3]; int bufferLength = 0; @@ -985,9 +987,9 @@ private int[] uploadHDTileModelSurface( int uvOrientation = 0; float uvScale = 1; - int[] normalsA = UP_NORMAL; - int[] normalsB = UP_NORMAL; - int[] normalsC = UP_NORMAL; + short[] normalsA = UP_NORMAL; + short[] normalsB = UP_NORMAL; + short[] normalsC = UP_NORMAL; WaterType waterType = WaterType.NONE; @@ -1013,9 +1015,9 @@ private int[] uploadHDTileModelSurface( materialA = materialB = materialC = material; } - normalsA = sceneContext.vertexTerrainNormals.getOrDefault(vertexKeyA, normalsA); - normalsB = sceneContext.vertexTerrainNormals.getOrDefault(vertexKeyB, normalsB); - normalsC = sceneContext.vertexTerrainNormals.getOrDefault(vertexKeyC, normalsC); + normalsA = sceneContext.getVertexNormalOrDefault(vertexKeyA, new short[3], UP_NORMAL); + normalsB = sceneContext.getVertexNormalOrDefault(vertexKeyB, new short[3], UP_NORMAL); + normalsC = sceneContext.getVertexNormalOrDefault(vertexKeyC, new short[3], UP_NORMAL); GroundMaterial groundMaterial = null; @@ -1068,19 +1070,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.isVertexWater(vertexKeyA) && sceneContext.isVertexLand(vertexKeyA)) colorA = 0; - if (sceneContext.vertexIsWater.containsKey(vertexKeyB) && sceneContext.vertexIsLand.containsKey(vertexKeyB)) + if (sceneContext.isVertexWater(vertexKeyB) && sceneContext.isVertexLand(vertexKeyB)) colorB = 0; - if (sceneContext.vertexIsWater.containsKey(vertexKeyC) && sceneContext.vertexIsLand.containsKey(vertexKeyC)) + if (sceneContext.isVertexWater(vertexKeyC) && sceneContext.isVertexLand(vertexKeyC)) colorC = 0; } - if (sceneContext.vertexIsOverlay.containsKey(vertexKeyA) && sceneContext.vertexIsUnderlay.containsKey(vertexKeyA)) + if (sceneContext.isVertexOverlay(vertexKeyA) && sceneContext.isVertexUnderlay(vertexKeyA)) vertexAIsOverlay = true; - if (sceneContext.vertexIsOverlay.containsKey(vertexKeyB) && sceneContext.vertexIsUnderlay.containsKey(vertexKeyB)) + if (sceneContext.isVertexOverlay(vertexKeyB) && sceneContext.isVertexUnderlay(vertexKeyB)) vertexBIsOverlay = true; - if (sceneContext.vertexIsOverlay.containsKey(vertexKeyC) && sceneContext.vertexIsUnderlay.containsKey(vertexKeyC)) + if (sceneContext.isVertexOverlay(vertexKeyC) && sceneContext.isVertexUnderlay(vertexKeyC)) vertexCIsOverlay = true; for (int i = 0; i < 3; i++) @@ -1144,7 +1146,7 @@ private int[] uploadHDTileModelUnderwater(LegacySceneContext sceneContext, Tile int uvBufferLength = 0; int underwaterTerrain = 0; - if (sceneContext.skipTile[tileZ][tileExX][tileExY]) + if (sceneContext.isTileFlagSet(tileZ, tileExX, tileExY, TILE_SKIP_FLAG)) return new int[] { bufferLength, uvBufferLength, underwaterTerrain }; final int[] faceColorA = model.getTriangleColorA(); @@ -1159,7 +1161,7 @@ private int[] uploadHDTileModelUnderwater(LegacySceneContext sceneContext, Tile return new int[] { bufferLength, uvBufferLength, underwaterTerrain }; } - if (sceneContext.tileIsWater[tileZ][tileExX][tileExY]) { + if (sceneContext.isTileFlagSet(tileZ, tileExX, tileExY, TILE_WATER_FLAG)) { underwaterTerrain = 1; int overlayId = OVERLAY_FLAG | scene.getOverlayIds()[tileZ][tileExX][tileExY]; @@ -1188,9 +1190,9 @@ private int[] uploadHDTileModelUnderwater(LegacySceneContext sceneContext, Tile int vertexKeyB = vertexKeys[1]; int vertexKeyC = vertexKeys[2]; - int depthA = sceneContext.vertexUnderwaterDepth.getOrDefault(vertexKeyA, 0); - int depthB = sceneContext.vertexUnderwaterDepth.getOrDefault(vertexKeyB, 0); - int depthC = sceneContext.vertexUnderwaterDepth.getOrDefault(vertexKeyC, 0); + int depthA = sceneContext.getVertexUnderwaterDepth(vertexKeyA); + int depthB = sceneContext.getVertexUnderwaterDepth(vertexKeyB); + int depthC = sceneContext.getVertexUnderwaterDepth(vertexKeyC); if (plugin.configGroundTextures) { GroundMaterial groundMaterial = GroundMaterial.UNDERWATER_GENERIC; @@ -1211,9 +1213,9 @@ private int[] uploadHDTileModelUnderwater(LegacySceneContext sceneContext, Tile ); } - int[] normalsA = sceneContext.vertexTerrainNormals.getOrDefault(vertexKeyA, UP_NORMAL); - int[] normalsB = sceneContext.vertexTerrainNormals.getOrDefault(vertexKeyB, UP_NORMAL); - int[] normalsC = sceneContext.vertexTerrainNormals.getOrDefault(vertexKeyC, UP_NORMAL); + short[] normalsA = sceneContext.getVertexNormalOrDefault(vertexKeyA, new short[3], UP_NORMAL); + short[] normalsB = sceneContext.getVertexNormalOrDefault(vertexKeyB, new short[3], UP_NORMAL); + short[] normalsC = sceneContext.getVertexNormalOrDefault(vertexKeyC, new short[3], UP_NORMAL); int textureId = faceTextures == null ? -1 : faceTextures[face]; WaterType waterType = proceduralGenerator.seasonalWaterType(override, textureId); diff --git a/src/main/java/rs117/hd/renderer/zone/AsyncCachedModel.java b/src/main/java/rs117/hd/renderer/zone/AsyncCachedModel.java index 30f20ae832..c843331ad5 100644 --- a/src/main/java/rs117/hd/renderer/zone/AsyncCachedModel.java +++ b/src/main/java/rs117/hd/renderer/zone/AsyncCachedModel.java @@ -1,7 +1,6 @@ package rs117.hd.renderer.zone; import com.google.inject.Injector; -import java.lang.reflect.Array; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.LockSupport; @@ -15,11 +14,16 @@ import rs117.hd.renderer.zone.Zone.AlphaModel; import rs117.hd.scene.model_overrides.ModelOverride; import rs117.hd.utils.collections.ConcurrentPool; +import rs117.hd.utils.collections.PooledArrayType; import rs117.hd.utils.jobs.Job; +import static java.lang.reflect.Array.getLength; import static rs117.hd.HdPlugin.MAX_FACE_COUNT; import static rs117.hd.renderer.zone.SceneUploader.MAX_VERTEX_COUNT; -import static rs117.hd.utils.MathUtils.*; +import static rs117.hd.utils.collections.PooledArrayType.BYTE; +import static rs117.hd.utils.collections.PooledArrayType.FLOAT; +import static rs117.hd.utils.collections.PooledArrayType.INT; +import static rs117.hd.utils.collections.PooledArrayType.SHORT; @Slf4j @Getter @@ -35,8 +39,8 @@ private static long calculateMaxModelSizeBytes() { var m = new AsyncCachedModel(); long size = 0; for (var field : m.cachedFields) { - size += (long) field.def.stride * ( - field.arrayType == VERTEX_TYPE ? MAX_VERTEX_COUNT : MAX_FACE_COUNT + size += (long) field.arrayType.stride * ( + field.fieldType == VERTEX_TYPE ? MAX_VERTEX_COUNT : MAX_FACE_COUNT ); } return size; @@ -84,35 +88,36 @@ public static void destroy() { private final CachedArrayField[] cachedFields = new CachedArrayField[21]; - private final CachedArrayField verticesX = addField(ArrayType.VERTEX_FLOAT); - private final CachedArrayField verticesY = addField(ArrayType.VERTEX_FLOAT); - private final CachedArrayField verticesZ = addField(ArrayType.VERTEX_FLOAT); + private final CachedArrayField verticesX = addField(FLOAT, VERTEX_TYPE); + private final CachedArrayField verticesY = addField(FLOAT, VERTEX_TYPE); + private final CachedArrayField verticesZ = addField(FLOAT, VERTEX_TYPE); - private final CachedArrayField faceIndices1 = addField(ArrayType.FACE_INT); - private final CachedArrayField faceIndices2 = addField(ArrayType.FACE_INT); - private final CachedArrayField faceIndices3 = addField(ArrayType.FACE_INT); + private final CachedArrayField faceIndices1 = addField(INT, FACE_TYPE); + private final CachedArrayField faceIndices2 = addField(INT, FACE_TYPE); + private final CachedArrayField faceIndices3 = addField(INT, FACE_TYPE); - private final CachedArrayField faceColors1 = addField(ArrayType.FACE_INT); - private final CachedArrayField faceColors2 = addField(ArrayType.FACE_INT); - private final CachedArrayField faceColors3 = addField(ArrayType.FACE_INT); + private final CachedArrayField faceColors1 = addField(INT, FACE_TYPE); + private final CachedArrayField faceColors2 = addField(INT, FACE_TYPE); + private final CachedArrayField faceColors3 = addField(INT, FACE_TYPE); - private final CachedArrayField unlitFaceColors = addField(ArrayType.FACE_SHORT); - private final CachedArrayField faceTextures = addField(ArrayType.FACE_SHORT); + private final CachedArrayField unlitFaceColors = addField(SHORT, FACE_TYPE); + private final CachedArrayField faceTextures = addField(SHORT, FACE_TYPE); - private final CachedArrayField faceRenderPriorities = addField(ArrayType.FACE_BYTE); - private final CachedArrayField faceTransparencies = addField(ArrayType.FACE_BYTE); - private final CachedArrayField faceBias = addField(ArrayType.FACE_BYTE); - private final CachedArrayField textureFaces = addField(ArrayType.FACE_BYTE); + private final CachedArrayField faceRenderPriorities = addField(BYTE, FACE_TYPE); + private final CachedArrayField faceTransparencies = addField(BYTE, FACE_TYPE); + private final CachedArrayField faceBias = addField(BYTE, FACE_TYPE); + private final CachedArrayField textureFaces = addField(BYTE, FACE_TYPE); - private final CachedArrayField texIndices1 = addField(ArrayType.TEX_INT); - private final CachedArrayField texIndices2 = addField(ArrayType.TEX_INT); - private final CachedArrayField texIndices3 = addField(ArrayType.TEX_INT); + private final CachedArrayField texIndices1 = addField(INT, TEX_TYPE); + private final CachedArrayField texIndices2 = addField(INT, TEX_TYPE); + private final CachedArrayField texIndices3 = addField(INT, TEX_TYPE); - private final CachedArrayField vertexNormalsX = addField(ArrayType.VERTEX_INT); - private final CachedArrayField vertexNormalsY = addField(ArrayType.VERTEX_INT); - private final CachedArrayField vertexNormalsZ = addField(ArrayType.VERTEX_INT); + private final CachedArrayField vertexNormalsX = addField(INT, VERTEX_TYPE); + private final CachedArrayField vertexNormalsY = addField(INT, VERTEX_TYPE); + private final CachedArrayField vertexNormalsZ = addField(INT, VERTEX_TYPE); private final AtomicBoolean processing = new AtomicBoolean(false); + private final AtomicBoolean isCompleted = new AtomicBoolean(false); private WorldViewContext ctx; private Projection projection; private TileObject tileObject; @@ -129,10 +134,10 @@ public static void destroy() { private UploadModelFunc uploadFunc; @SuppressWarnings("unchecked") - private CachedArrayField addField(ArrayType fieldDef) { + private CachedArrayField addField(PooledArrayType arrayType, int fieldType) { for (int i = 0; i < cachedFields.length; i++) { if (cachedFields[i] == null) - return (CachedArrayField) (cachedFields[i] = new CachedArrayField<>(fieldDef)); + return (CachedArrayField) (cachedFields[i] = new CachedArrayField<>(this, arrayType, fieldType)); } throw new RuntimeException("Created too many fields, only expected: " + cachedFields.length); } @@ -215,6 +220,9 @@ public synchronized void queue( int x, int y, int z, @Nonnull UploadModelFunc uploadFunc ) { + // Wait for completion so that the job has cleared the job system before clearing the `processing` flag + waitForCompletion(true); + this.ctx = ctx; this.projection = projection; this.tileObject = tileObject; @@ -256,10 +264,8 @@ public synchronized void queue( verticesCount = model.getVerticesCount(); faceCount = model.getFaceCount(); - // Wait for completion so that the job has cleared the job system before clearing the `processing` flag - waitForCompletion(); - processing.set(false); + isCompleted.set(false); if (alphaModel != null) zone.pendingModelJobs.add(this); INFLIGHT.add(this); @@ -303,9 +309,9 @@ protected boolean canStart() { return true; return - verticesX.cached && verticesY.cached && verticesZ.cached && - faceIndices1.cached && faceIndices2.cached && faceIndices3.cached && - faceColors3.cached; + verticesX.isCached() && verticesY.isCached() && verticesZ.isCached() && + faceIndices1.isCached() && faceIndices2.isCached() && faceIndices3.isCached() && + faceColors3.isCached(); } @Override @@ -332,10 +338,17 @@ public boolean processModel() { orientation, x, y, z ); + isCompleted.set(true); } catch (Exception e) { log.error("Error drawing temp object", e); } finally { - INFLIGHT.remove(this); + // Reset cached status before returning to the POOL + for (int i = 0; i < cachedFields.length; i++) { + final CachedArrayField field = cachedFields[i]; + field.ensureCached(); + field.reset(); + } + if (alphaModel != null) zone.pendingModelJobs.remove(this); @@ -348,10 +361,7 @@ public boolean processModel() { modelOverride = null; drawIndex = -1; - // Reset cached status before returning to the POOL - for (int i = 0; i < cachedFields.length; i++) - cachedFields[i].cached = false; - + INFLIGHT.remove(this); POOL.recycle(this); } @@ -443,67 +453,50 @@ void upload( ); } - @FunctionalInterface - interface ArraySupplier { - T get(int capacity); - } - private static final int VERTEX_TYPE = 0; private static final int FACE_TYPE = 1; private static final int TEX_TYPE = 2; @RequiredArgsConstructor - private enum ArrayType { - VERTEX_INT(int[]::new, 4, VERTEX_TYPE), - VERTEX_FLOAT(float[]::new, 4, VERTEX_TYPE), - - FACE_INT(int[]::new, 4, FACE_TYPE), - FACE_SHORT(short[]::new, 2, FACE_TYPE), - FACE_BYTE(byte[]::new, 1, FACE_TYPE), - - TEX_INT(int[]::new, 4, TEX_TYPE); - - private final ArraySupplier supplier; - private final int stride; - private final int type; - } - private static final class CachedArrayField { - private final ArrayType def; - private final int arrayType; - private final ArraySupplier supplier; + private final AsyncCachedModel model; + private final PooledArrayType arrayType; + private final int fieldType; - private int capacity; - private T pooled; private T value; + private final AtomicBoolean cached = new AtomicBoolean(false); - public volatile boolean cached; + public boolean isCached() {return cached.get();} - private CachedArrayField(ArrayType arrayType) { - this.def = arrayType; - this.arrayType = arrayType.type; - // noinspection unchecked - this.supplier = (ArraySupplier) arrayType.supplier; - this.value = supplier.get((int) KiB); + public void ensureCached() { + while (!cached.get()) + LockSupport.parkNanos(this, 5); } public T getValue() { - while (!cached) - LockSupport.parkNanos(this, 5); + ensureCached(); return value; } + public void reset() { + if(value != null) + arrayType.release(value); + value = null; + cached.set(false); + } + public void cache(final Model m, T src) { - if (src == null) { - if (value != null) - pooled = value; - value = null; - cached = true; + assert !cached.get(); + assert value == null; + + // If model is completed, then we can skip caching since its unnecessary to continue + if (src == null || model.isCompleted.get()) { + cached.set(true); return; } final int arraySize; - switch (arrayType) { + switch (fieldType) { case VERTEX_TYPE: arraySize = m.getVerticesCount(); break; @@ -511,22 +504,12 @@ public void cache(final Model m, T src) { arraySize = m.getFaceCount(); break; default: - arraySize = Array.getLength(src); + arraySize = getLength(src); break; } - if (value == null || capacity < arraySize) { - if (pooled != null && capacity >= arraySize) { - value = pooled; - } else { - value = supplier.get(capacity = arraySize); - } - pooled = null; - } - - // noinspection SuspiciousSystemArraycopy - System.arraycopy(src, 0, value, 0, arraySize); - cached = true; + value = arrayType.cache(src, 0, arraySize); + cached.set(true); } } } diff --git a/src/main/java/rs117/hd/renderer/zone/FacePrioritySorter.java b/src/main/java/rs117/hd/renderer/zone/FacePrioritySorter.java index d203465e44..27ea196e61 100644 --- a/src/main/java/rs117/hd/renderer/zone/FacePrioritySorter.java +++ b/src/main/java/rs117/hd/renderer/zone/FacePrioritySorter.java @@ -28,7 +28,8 @@ import lombok.extern.slf4j.Slf4j; import net.runelite.api.*; import rs117.hd.utils.collections.ConcurrentPool; -import rs117.hd.utils.collections.PrimitiveIntArray; +import rs117.hd.utils.collections.PooledArrayType; +import rs117.hd.utils.collections.PrimitiveCharArray; import static rs117.hd.renderer.zone.Zone.VERT_SIZE; import static rs117.hd.utils.MathUtils.*; @@ -42,34 +43,45 @@ public final class FacePrioritySorter implements AutoCloseable { private static final int MAX_FACES_PER_PRIORITY = 4000; private static final int PRIORITY_COUNT = 12; - public final int[] faceDistances = new int[MAX_FACE_COUNT]; - - private final int[] orderedFaces = new int[PRIORITY_COUNT * MAX_FACES_PER_PRIORITY]; private final int[] numOfPriority = new int[PRIORITY_COUNT]; private final int[] eq10 = new int[MAX_FACES_PER_PRIORITY]; private final int[] eq11 = new int[MAX_FACES_PER_PRIORITY]; private final int[] lt10 = new int[PRIORITY_COUNT]; - private final int[] zsortHead = new int[MAX_DIAMETER]; - private final int[] zsortTail = new int[MAX_DIAMETER]; - private final int[] zsortNext = new int[MAX_FACE_COUNT]; + private char[] orderedFaces; + private int[] zsortHead; + private int[] zsortTail; + private int[] zsortNext; + + private void ensureCapacity(int diameter, int faceCount) { + zsortHead = PooledArrayType.INT.ensureCapacity(zsortHead, min(MAX_DIAMETER, diameter + 1)); + zsortTail = PooledArrayType.INT.ensureCapacity(zsortTail, min(MAX_DIAMETER, diameter + 1)); + zsortNext = PooledArrayType.INT.ensureCapacity(zsortNext, min(MAX_FACE_COUNT, faceCount)); + } - void sortModelFaces(PrimitiveIntArray visibleFaces, Model model) { - sortModelFaces(visibleFaces, model, false); + void sortModelFaces(PrimitiveCharArray visibleFaces, Model model, int[] faceDistances) { + sortModelFaces(visibleFaces, model, faceDistances, false); } - void sortModelFaces(PrimitiveIntArray visibleFaces, Model model, boolean depthOnly) { + void sortModelFaces(PrimitiveCharArray visibleFaces, Model model, int[] faceDistances, boolean depthOnly) { final int diameter = model.getDiameter(); if (diameter <= 0 || diameter >= MAX_DIAMETER) return; + final int visibleFaceCount = visibleFaces.length; + final int facesPerPriority = min(visibleFaceCount, MAX_FACES_PER_PRIORITY); + orderedFaces = PooledArrayType.CHAR.ensureCapacity(orderedFaces, PRIORITY_COUNT * facesPerPriority); + int unsortedCount = 0; int minFz = diameter, maxFz = 0; boolean needsClear = true; + ensureCapacity(diameter, model.getFaceCount()); + // Build the z-sorted linked list of faces - for (int i = 0; i < visibleFaces.length; ++i) { - final int faceIdx = visibleFaces.array[i]; + for (int i = 0; i < visibleFaceCount; ++i) { + final char faceIdx = visibleFaces.array[i]; + assert faceIdx < faceDistances.length; if (faceDistances[faceIdx] == Integer.MIN_VALUE) { orderedFaces[unsortedCount++] = faceIdx; continue; @@ -96,8 +108,9 @@ void sortModelFaces(PrimitiveIntArray visibleFaces, Model model, boolean depthOn } } - if (visibleFaces.length - unsortedCount == 0) + if (visibleFaces.length - unsortedCount == 0) { return; // No faces to sort, so don't modify the visible faces array + } visibleFaces.reset(); if (unsortedCount > 0) // Push unsorted faces to be drawn first @@ -107,7 +120,7 @@ void sortModelFaces(PrimitiveIntArray visibleFaces, Model model, boolean depthOn if (priorities == null) { for (int i = maxFz; i >= minFz; --i) { for (int f = zsortHead[i]; f != -1; f = zsortNext[f]) - visibleFaces.put(f); + visibleFaces.put((char) f); } return; } @@ -120,7 +133,7 @@ void sortModelFaces(PrimitiveIntArray visibleFaces, Model model, boolean depthOn final int pri = priorities[f]; final int idx = numOfPriority[pri]++; - orderedFaces[pri * MAX_FACES_PER_PRIORITY + idx] = f; + orderedFaces[pri * facesPerPriority + idx] = (char) f; if (pri < 10) lt10[pri] += i; @@ -142,12 +155,12 @@ else if (pri == 10) int drawnFaces = 0; int numDynFaces = numOfPriority[10]; - int dynBase = 10 * MAX_FACES_PER_PRIORITY; + int dynBase = 10 * facesPerPriority; int[] dynDist = eq10; if (numDynFaces == 0) { numDynFaces = numOfPriority[11]; - dynBase = 11 * MAX_FACES_PER_PRIORITY; + dynBase = 11 * facesPerPriority; dynDist = eq11; } @@ -161,10 +174,10 @@ else if (pri == 10) ) { visibleFaces.put(orderedFaces[dynBase + drawnFaces++]); - if (drawnFaces == numDynFaces && dynBase == 10 * MAX_FACES_PER_PRIORITY) { + if (drawnFaces == numDynFaces && dynBase == 10 * facesPerPriority) { drawnFaces = 0; numDynFaces = numOfPriority[11]; - dynBase = 11 * MAX_FACES_PER_PRIORITY; + dynBase = 11 * facesPerPriority; dynDist = eq11; } @@ -173,7 +186,7 @@ else if (pri == 10) visibleFaces.put( orderedFaces, - pri * MAX_FACES_PER_PRIORITY, + pri * facesPerPriority, numOfPriority[pri] ); } @@ -181,10 +194,10 @@ else if (pri == 10) while (currFaceDistance != -1000) { visibleFaces.put(orderedFaces[dynBase + drawnFaces++]); - if (drawnFaces == numDynFaces && dynBase == 10 * MAX_FACES_PER_PRIORITY) { + if (drawnFaces == numDynFaces && dynBase == 10 * facesPerPriority) { drawnFaces = 0; numDynFaces = numOfPriority[11]; - dynBase = 11 * MAX_FACES_PER_PRIORITY; + dynBase = 11 * facesPerPriority; dynDist = eq11; } @@ -192,6 +205,7 @@ else if (pri == 10) } } + void sortStaticModelFacesByDistance( Zone.AlphaModel m, int yawCos, int yawSin, @@ -203,7 +217,11 @@ void sortStaticModelFacesByDistance( return; final int faceCount = m.packedFaces.length; + final int m02 = -(yawSin * pitchCos) >> 16; + final int m12 = pitchSin; + final int m22 = (yawCos * pitchCos) >> 16; + ensureCapacity(diameter, faceCount); Arrays.fill(zsortHead, 0, diameter, -1); Arrays.fill(zsortTail, 0, diameter, -1); @@ -214,8 +232,7 @@ void sortStaticModelFacesByDistance( final int y = (packed << 11) >> 22; final int z = (packed << 21) >> 21; - int fz = ((z * yawCos - x * yawSin) >> 16); - fz = ((y * pitchSin + fz * pitchCos) >> 16) + radius; + final int fz = ((x * m02 + y * m12 + z * m22) >> 16) + radius; if (zsortTail[fz] == -1) { zsortHead[fz] = zsortTail[fz] = i; @@ -224,8 +241,7 @@ void sortStaticModelFacesByDistance( minFz = min(minFz, fz); maxFz = max(maxFz, fz); } else { - int lastFace = zsortTail[fz]; - zsortNext[lastFace] = i; + zsortNext[zsortTail[fz]] = i; zsortNext[i] = -1; zsortTail[fz] = i; } @@ -234,9 +250,6 @@ void sortStaticModelFacesByDistance( final int start = m.startpos / (VERT_SIZE >> 2); for (int i = maxFz; i >= minFz; --i) { for (int f = zsortHead[i]; f != -1; f = zsortNext[f]) { - if (m.sortedFacesLen >= m.sortedFaces.length) - break; - if (f >= faceCount) continue; @@ -246,12 +259,25 @@ void sortStaticModelFacesByDistance( m.sortedFaces[sortedOffset + 1] = faceStart + 1; m.sortedFaces[sortedOffset + 2] = faceStart + 2; m.sortedFacesLen += 3; + + if (m.sortedFacesLen >= m.sortedFaces.length) + return; } } } @Override public void close() { + PooledArrayType.CHAR.release(orderedFaces); + PooledArrayType.INT.release(zsortHead); + PooledArrayType.INT.release(zsortTail); + PooledArrayType.INT.release(zsortNext); + + orderedFaces = null; + zsortHead = null; + zsortTail = null; + zsortNext = null; + POOL.recycle(this); } } diff --git a/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java b/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java index 7b9a941558..a95812b7b3 100644 --- a/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java +++ b/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java @@ -23,7 +23,8 @@ import rs117.hd.utils.HDUtils; import rs117.hd.utils.ModelHash; import rs117.hd.utils.collections.ConcurrentPool; -import rs117.hd.utils.collections.PrimitiveIntArray; +import rs117.hd.utils.collections.PooledArrayType; +import rs117.hd.utils.collections.PrimitiveCharArray; import static net.runelite.api.Perspective.*; import static net.runelite.api.hooks.DrawCallbacks.*; @@ -37,7 +38,7 @@ @Slf4j @Singleton public class ModelStreamingManager { - public static final ConcurrentPool FACE_INDICES = new ConcurrentPool<>(PrimitiveIntArray::new); + public static final ConcurrentPool FACE_INDICES = new ConcurrentPool<>(PrimitiveCharArray::new); public static final int RL_RENDER_THREADS = 2; @Inject @@ -292,8 +293,8 @@ private void uploadTempModel( int orientation, int x, int y, int z ) { - final PrimitiveIntArray visibleFaces = FACE_INDICES.acquire(); - final PrimitiveIntArray culledFaces = FACE_INDICES.acquire(); + final PrimitiveCharArray visibleFaces = FACE_INDICES.acquire(); + final PrimitiveCharArray culledFaces = FACE_INDICES.acquire(); boolean shouldSort = renderable.getRenderMode() == Renderable.RENDERMODE_SORTED || @@ -303,10 +304,11 @@ private void uploadTempModel( SceneUploader sceneUploader = SceneUploader.POOL.acquire(); FacePrioritySorter facePrioritySorter = shouldSort ? FacePrioritySorter.POOL.acquire() : null ) { + final int[] faceDistances = shouldSort ? PooledArrayType.INT.borrow(m.getFaceCount()) : null; shouldSort &= sceneUploader.preprocessTempModel( worldProjection, plugin.cameraFrustum, - shouldSort ? facePrioritySorter.faceDistances : null, + faceDistances, visibleFaces, culledFaces, isModelPartiallyVisible, @@ -319,7 +321,10 @@ private void uploadTempModel( final boolean isSquashed = ctx.uboWorldViewStruct != null && ctx.uboWorldViewStruct.isSquashed(); if (shouldSort && !isSquashed) - facePrioritySorter.sortModelFaces(visibleFaces, m); + facePrioritySorter.sortModelFaces(visibleFaces, m, faceDistances); + + if(facePrioritySorter != null) + PooledArrayType.INT.release( faceDistances); final int preOrientation = HDUtils.getModelPreOrientation(gameObject.getConfig()); if (culledFaces.length > 0 && @@ -557,18 +562,19 @@ private void uploadDynamicModel( int orient, int x, int y, int z ) { - final PrimitiveIntArray visibleFaces = FACE_INDICES.acquire(); - final PrimitiveIntArray culledFaces = FACE_INDICES.acquire(); + final PrimitiveCharArray visibleFaces = FACE_INDICES.acquire(); + final PrimitiveCharArray culledFaces = FACE_INDICES.acquire(); boolean shouldSort = renderable.getRenderMode() != Renderable.RENDERMODE_UNSORTED; try ( SceneUploader sceneUploader = SceneUploader.POOL.acquire(); FacePrioritySorter facePrioritySorter = shouldSort ? FacePrioritySorter.POOL.acquire() : null ) { + final int[] faceDistances = shouldSort ? PooledArrayType.INT.borrow(m.getFaceCount()) : null; shouldSort &= sceneUploader.preprocessTempModel( projection, plugin.cameraFrustum, - shouldSort ? facePrioritySorter.faceDistances : null, + faceDistances, visibleFaces, culledFaces, isModelPartiallyVisible, @@ -582,7 +588,10 @@ private void uploadDynamicModel( final int preOrientation = HDUtils.getModelPreOrientation(HDUtils.getObjectConfig(tileObject)); final boolean isSquashed = ctx.uboWorldViewStruct != null && ctx.uboWorldViewStruct.isSquashed(); if (shouldSort && !isSquashed) - facePrioritySorter.sortModelFaces(visibleFaces, m, true); + facePrioritySorter.sortModelFaces(visibleFaces, m, faceDistances, true); + + if(facePrioritySorter != null) + PooledArrayType.INT.release(faceDistances); if (culledFaces.length > 0 && modelOverride.castShadows && @@ -675,7 +684,7 @@ public void ensureAsyncUploadsComplete(@Nullable Zone zone) { } - private AsyncCachedModel obtainAvailableAsyncCachedModel(boolean shouldBlock) { + private synchronized AsyncCachedModel obtainAvailableAsyncCachedModel(boolean shouldBlock) { if (AsyncCachedModel.POOL == null || numRenderThreads <= 0) return null; diff --git a/src/main/java/rs117/hd/renderer/zone/SceneManager.java b/src/main/java/rs117/hd/renderer/zone/SceneManager.java index f7258f3620..34d1abafa6 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; @@ -36,6 +34,8 @@ import rs117.hd.utils.DestructibleHandler; import rs117.hd.utils.NpcDisplacementCache; import rs117.hd.utils.RenderState; +import rs117.hd.utils.collections.Int2IntHashMap; +import rs117.hd.utils.collections.PooledArrayType; import rs117.hd.utils.jobs.GenericJob; import static net.runelite.api.Constants.*; @@ -99,7 +99,7 @@ public class SceneManager { 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 Int2IntHashMap nextRoofChanges = new Int2IntHashMap(); private ZoneSceneContext nextSceneContext; private Zone[][] nextZones; private final List sortedZones = new ArrayList<>(); @@ -371,7 +371,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 @@ -403,14 +403,16 @@ private static boolean isEdgeTile(Zone[][] zones, int zx, int zz) { task.workerHandleCancel(); // old zone still in scene? if (ox >= 0 && oz >= 0 && ox < EXTENDED_SCENE_SIZE && oz < EXTENDED_SCENE_SIZE) { - 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) { - log.trace("Roof change: {} -> {}", prid, nrid); - } else if (old != nrid) { - log.debug("Roof change mismatch: {} -> {} vs {}", prid, nrid, old); + final int prevRoofId = prids[level][ox][oz]; + final int newRoofId = nrids[level][x][z]; + if (prevRoofId > 0 && newRoofId > 0 && prevRoofId != newRoofId) { + int oldIdx = nextRoofChanges.find(prevRoofId); + if (oldIdx != -1) { + if(nextRoofChanges.getValue(oldIdx) != newRoofId) + log.debug("Roof change mismatch: {} -> {} vs {}", prevRoofId, newRoofId, nextRoofChanges.getValue(oldIdx)); + } else { + log.trace("Roof change: {} -> {}", prevRoofId, newRoofId); + nextRoofChanges.put(prevRoofId, newRoofId); } } } @@ -444,6 +446,8 @@ public synchronized void loadScene(WorldView worldView, Scene scene) { nextSceneContext.destroy(); nextSceneContext = null; + PooledArrayType.forceCleanup(); + nextZones = new Zone[NUM_ZONES][NUM_ZONES]; nextSceneContext = new ZoneSceneContext( client, @@ -664,6 +668,7 @@ public void swapScene(Scene scene) { for (var tileObject : nextSceneContext.lightSpawnsToHandleOnClientThread) lightManager.handleObjectSpawn(nextSceneContext, tileObject); nextSceneContext.lightSpawnsToHandleOnClientThread.clear(); + nextSceneContext.lightSpawnsToHandleOnClientThread.trimToSize(); lightManager.swapSceneLights(nextSceneContext, root.sceneContext); long lightsTime = sw.elapsed(TimeUnit.MILLISECONDS); @@ -706,6 +711,8 @@ public void swapScene(Scene scene) { DestructibleHandler.queueDestruction(preZone); nextZone.setMetadata(ctx, nextSceneContext, x, z); + if(preZone.rebuild) + nextZone.rebuild = true; nextSceneContext.animatedDynamicObjectIds.addAll(nextZone.animatedDynamicObjectIds); } } @@ -755,7 +762,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 bf48d1af96..079d6aed0f 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.*; @@ -35,7 +33,6 @@ import rs117.hd.scene.MaterialManager; import rs117.hd.scene.ModelOverrideManager; import rs117.hd.scene.ProceduralGenerator; -import rs117.hd.scene.TileOverrideManager; import rs117.hd.scene.ground_materials.GroundMaterial; import rs117.hd.scene.materials.Material; import rs117.hd.scene.model_overrides.InheritTileColorType; @@ -48,13 +45,18 @@ import rs117.hd.utils.ModelHash; import rs117.hd.utils.buffer.GpuIntBuffer; import rs117.hd.utils.collections.ConcurrentPool; -import rs117.hd.utils.collections.PrimitiveIntArray; +import rs117.hd.utils.collections.IntHashSet; +import rs117.hd.utils.collections.PooledArrayType; +import rs117.hd.utils.collections.PooledObjectArray; +import rs117.hd.utils.collections.PrimitiveCharArray; import static net.runelite.api.Constants.*; import static net.runelite.api.Perspective.*; -import static rs117.hd.renderer.zone.FacePrioritySorter.MAX_FACE_COUNT; +import static rs117.hd.scene.SceneContext.TILE_OVERRIDE_MAIN; +import static rs117.hd.scene.SceneContext.TILE_OVERRIDE_OVERLAY; +import static rs117.hd.scene.SceneContext.TILE_OVERRIDE_UNDERLAY; +import static rs117.hd.scene.SceneContext.TILE_WATER_FLAG; import static rs117.hd.scene.tile_overrides.TileOverride.NONE; -import static rs117.hd.scene.tile_overrides.TileOverride.OVERLAY_FLAG; import static rs117.hd.utils.HDUtils.HIDDEN_HSL; import static rs117.hd.utils.HDUtils.UNDERWATER_HSL; import static rs117.hd.utils.MathUtils.*; @@ -64,7 +66,7 @@ public class SceneUploader implements AutoCloseable { public static ConcurrentPool POOL; public static final int MAX_VERTEX_COUNT = 6500; - private static final int[] UP_NORMAL = { 0, -1, 0 }; + private static final short[] UP_NORMAL = { 0, -1, 0 }; private final int[] EMPTY_NORMALS = new int[9]; public static final float[] GEOMETRY_UVS = { @@ -100,9 +102,6 @@ public class SceneUploader implements AutoCloseable { @Inject private MaterialManager materialManager; - @Inject - private TileOverrideManager tileOverrideManager; - @Inject private ModelOverrideManager modelOverrideManager; @@ -118,13 +117,11 @@ 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; private int[][][] roofs; - private short[][][] overlayIds; - private short[][][] underlayIds; private int[][][] tileHeights; private final int[] worldPos = new int[3]; @@ -133,20 +130,22 @@ public interface OnBeforeProcessTileFunc { private final float[] workingSpace = new float[9]; private final float[] modelUvs = new float[12]; private final int[] modelNormals = new int[9]; + private final short[][] tileNormals = new short[4][3]; - public final float[] modelProjected = new float[MAX_VERTEX_COUNT * 3]; + private int[] modelVertices; public int tempModelAlphaFaces = 0; - private final float[] modelLocal = new float[MAX_VERTEX_COUNT * 3]; - private final int[] modelLocalI = new int[MAX_VERTEX_COUNT * 3]; - private final boolean[] visibility = new boolean[MAX_VERTEX_COUNT]; - - private final ModelOverride[] faceOverrides = new ModelOverride[MAX_FACE_COUNT]; - private final Material[] faceMaterials = new Material[MAX_FACE_COUNT]; - private final UvType[] faceUVTypes = new UvType[MAX_FACE_COUNT]; + private final PooledObjectArray faceOverrides = new PooledObjectArray<>(); + private final PooledObjectArray faceMaterials = new PooledObjectArray<>(); + private final PooledObjectArray faceUVTypes = new PooledObjectArray<>(); + private final int[] tzHaarRecolored = new int[3]; private final float[] projected = new float[4]; + private final GpuIntBuffer zoneVboO = new GpuIntBuffer(false); + private final GpuIntBuffer zoneVboA = new GpuIntBuffer(false); + private final GpuIntBuffer zoneTboF = new GpuIntBuffer(false); + // Lazily initialized staging buffers, only used by uploadTempModel public VertexWriteCache.Collection writeCache; @@ -158,8 +157,6 @@ public void setScene(Scene scene) { tiles = scene.getExtendedTiles(); settings = scene.getExtendedTileSettings(); roofs = scene.getRoofs(); - overlayIds = scene.getOverlayIds(); - underlayIds = scene.getUnderlayIds(); tileHeights = scene.getTileHeights(); } @@ -167,11 +164,20 @@ public void clear() { tiles = null; settings = null; roofs = null; - overlayIds = null; - underlayIds = null; tileHeights = null; currentScene = null; onBeforeProcessTile = null; + + PooledArrayType.INT.release(modelVertices); + modelVertices = null; + + faceOverrides.release(); + faceMaterials.release(); + faceUVTypes.release(); + } + + private void ensureVerticesAllocated(int vertexCount) { + modelVertices = PooledArrayType.INT.ensureCapacity(modelVertices, vertexCount * 3); } public void estimateZoneSize(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) throws InterruptedException { @@ -193,10 +199,10 @@ public void estimateZoneSize(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) } public void uploadZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) throws InterruptedException { - var vb = zone.vboO != null ? new GpuIntBuffer(zone.vboO.mapped()) : null; - var ab = zone.vboA != null ? new GpuIntBuffer(zone.vboA.mapped()) : null; - var fb = zone.tboF != null ? new GpuIntBuffer(zone.tboF.mapped()) : null; - assert fb != null; + var vb = zone.vboO != null ? zoneVboO.setBuffer(zone.vboO.mapped()) : null; + var ab = zone.vboA != null ? zoneVboA.setBuffer(zone.vboA.mapped()) : null; + var fb = zone.tboF != null ? zoneTboF.setBuffer(zone.tboF.mapped()) : null; + assert zone.tboF != null; roofIds.clear(); for (int level = 0; level <= 3; ++level) { @@ -217,13 +223,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 +252,6 @@ private void uploadZoneLevel( int mzz, int level, boolean visbelow, - Set roofIds, GpuIntBuffer vb, GpuIntBuffer ab, GpuIntBuffer fb @@ -338,14 +343,16 @@ private void uploadZoneWater( for (int level = 0; level < MAX_Z; level++) { for (int xoff = 0; xoff < 8; ++xoff) { for (int zoff = 0; zoff < 8; ++zoff) { - int msx = (mzx << 3) + xoff; - int msz = (mzz << 3) + zoff; - Tile t = tiles[level][msx][msz]; - if (t != null) { - if (onBeforeProcessTile != null) - onBeforeProcessTile.invoke(t, false); - uploadZoneTile(ctx, zone, t, false, true, vb, null, fb); - } + final int msx = (mzx << 3) + xoff; + final int msz = (mzz << 3) + zoff; + final Tile t = tiles[level][msx][msz]; + if (t == null || !ctx.isTileFlagSet(level, msx, msz, TILE_WATER_FLAG)) + continue; + + if (onBeforeProcessTile != null) + onBeforeProcessTile.invoke(t, false); + + uploadZoneTile(ctx, zone, t, false, true, vb, null, fb); } } } @@ -355,12 +362,16 @@ private void estimateZoneTileSize(ZoneSceneContext ctx, Zone z, Tile t) { var tilePoint = t.getSceneLocation(); ctx.sceneToWorld(tilePoint.getX(), tilePoint.getY(), t.getPlane(), worldPos); + int tileExX = tilePoint.getX() + ctx.sceneOffset; + int tileExY = tilePoint.getY() + ctx.sceneOffset; + int tileZ = t.getRenderLevel(); + SceneTilePaint paint = t.getSceneTilePaint(); if (paint != null && paint.getNeColor() != HIDDEN_HSL) { z.sizeO += 2; z.sizeF += 2; - TileOverride override = tileOverrideManager.getOverride(ctx, t, worldPos); + TileOverride override = ctx.getTileOverride(tileZ, tileExX, tileExY, TILE_OVERRIDE_MAIN); WaterType waterType = proceduralGenerator.seasonalWaterType(override, paint.getTexture()); if (waterType != WaterType.NONE) { z.hasWater = true; @@ -380,13 +391,8 @@ private void estimateZoneTileSize(ZoneSceneContext ctx, Zone z, Tile t) { z.sizeO += len; z.sizeF += len; - int tileExX = tilePoint.getX() + ctx.sceneOffset; - int tileExY = tilePoint.getY() + ctx.sceneOffset; - int tileZ = t.getRenderLevel(); - int overlayId = OVERLAY_FLAG | overlayIds[tileZ][tileExX][tileExY]; - int underlayId = underlayIds[tileZ][tileExX][tileExY]; - var overlayOverride = tileOverrideManager.getOverride(ctx, t, worldPos, overlayId); - var underlayOverride = tileOverrideManager.getOverride(ctx, t, worldPos, underlayId); + var overlayOverride = ctx.getTileOverride(tileZ, tileExX, tileExY, TILE_OVERRIDE_OVERLAY); + var underlayOverride = ctx.getTileOverride(tileZ, tileExX, tileExY, TILE_OVERRIDE_UNDERLAY); final int[] triangleTextures = model.getTriangleTextureId(); boolean isFallbackWater = false; @@ -437,7 +443,8 @@ private void estimateZoneTileSize(ZoneSceneContext ctx, Zone z, Tile t) { } GameObject[] gameObjects = t.getGameObjects(); - for (GameObject gameObject : gameObjects) { + for (int i = 0; i < gameObjects.length; i++) { + final GameObject gameObject = gameObjects[i]; if (gameObject == null || !gameObject.getSceneMinLocation().equals(t.getSceneLocation())) continue; @@ -496,7 +503,7 @@ private void uploadZoneTile( uploadTileModel(ctx, t, model, onlyWaterSurface, tileExX, tileExY, tileZ, basex, basez, vertexBuffer, textureBuffer); if (!onlyWaterSurface) - uploadZoneTileRenderables(ctx, zone, t, vertexBuffer, alphaBuffer, textureBuffer); + uploadZoneTileRenderables(ctx, zone, t, tileExX, tileExY, tileZ, vertexBuffer, alphaBuffer, textureBuffer); Tile bridge = t.getBridge(); if (bridge != null) @@ -507,6 +514,7 @@ private void uploadZoneTileRenderables( ZoneSceneContext ctx, Zone zone, Tile t, + int tileExX, int tileExY, int tileZ, GpuIntBuffer vertexBuffer, GpuIntBuffer alphaBuffer, GpuIntBuffer textureBuffer @@ -531,6 +539,7 @@ private void uploadZoneTileRenderables( -1, -1, wallObject.getId(), + tileExX, tileExY, tileZ, vertexBuffer, alphaBuffer, textureBuffer @@ -553,6 +562,7 @@ private void uploadZoneTileRenderables( -1, -1, wallObject.getId(), + tileExX, tileExY, tileZ, vertexBuffer, alphaBuffer, textureBuffer @@ -580,6 +590,7 @@ private void uploadZoneTileRenderables( -1, -1, decorativeObject.getId(), + tileExX, tileExY, tileZ, vertexBuffer, alphaBuffer, textureBuffer @@ -602,6 +613,7 @@ private void uploadZoneTileRenderables( -1, -1, decorativeObject.getId(), + tileExX, tileExY, tileZ, vertexBuffer, alphaBuffer, textureBuffer @@ -624,6 +636,7 @@ private void uploadZoneTileRenderables( -1, -1, -1, groundObject.getId(), + tileExX, tileExY, tileZ, vertexBuffer, alphaBuffer, textureBuffer @@ -631,7 +644,8 @@ private void uploadZoneTileRenderables( } GameObject[] gameObjects = t.getGameObjects(); - for (GameObject gameObject : gameObjects) { + for (int i = 0; i < gameObjects.length; i++) { + final GameObject gameObject = gameObjects[i]; if (gameObject == null || !renderCallbackManager.drawObject(ctx.scene, gameObject)) continue; @@ -657,6 +671,7 @@ private void uploadZoneTileRenderables( min.getY(), max.getX(), max.getY(), gameObject.getId(), + tileExX, tileExY, tileZ, vertexBuffer, alphaBuffer, textureBuffer @@ -706,6 +721,7 @@ private void uploadZoneRenderable( int ux, int uz, int id, + int tileExX, int tileExY, int tileZ, GpuIntBuffer opaqueBuffer, GpuIntBuffer alphaBuffer, GpuIntBuffer textureBuffer @@ -740,23 +756,26 @@ private void uploadZoneRenderable( ctx, tile, model, modelOverride, uuid, preOrientation, orient, x - basex, y, z - basez, + tileExX, tileExY, tileZ, opaqueBuffer, alphaBuffer, textureBuffer ); } catch (Throwable ex) { - log.warn( - "Error uploading {} {} {} {} (ID {}), override=\"{}\", opaque={}, alpha={}", - r instanceof DynamicObject ? "dynamic" : "static", - ModelHash.getTypeName(ModelHash.getUuidType(uuid)), - ModelHash.getUuidSubType(uuid), - gamevalManager.getObjectName(id), - id, - modelOverride.description, - opaqueBuffer, - alphaBuffer, - ex - ); + try(GamevalManager.Handle handle = gamevalManager.obtainHandle()) { + log.warn( + "Error uploading {} {} {} {} (ID {}), override=\"{}\", opaque={}, alpha={}", + r instanceof DynamicObject ? "dynamic" : "static", + ModelHash.getTypeName(ModelHash.getUuidType(uuid)), + ModelHash.getUuidSubType(uuid), + handle.getObjectName(id), + id, + modelOverride.description, + opaqueBuffer, + alphaBuffer, + ex + ); + } } int alphaEnd = alphaBuffer != null ? alphaBuffer.position() : 0; @@ -783,18 +802,20 @@ private void uploadZoneRenderable( rid, level, id ); } catch (Throwable ex) { - log.warn( - "Error adding alpha model for {} {} {} {} (ID {}), override=\"{}\", opaque={}, alpha={}", - r instanceof DynamicObject ? "dynamic" : "static", - ModelHash.getTypeName(ModelHash.getUuidType(uuid)), - ModelHash.getUuidSubType(uuid), - gamevalManager.getObjectName(id), - id, - modelOverride.description, - opaqueBuffer, - alphaBuffer, - ex - ); + try(GamevalManager.Handle handle = gamevalManager.obtainHandle()) { + log.warn( + "Error adding alpha model for {} {} {} {} (ID {}), override=\"{}\", opaque={}, alpha={}", + r instanceof DynamicObject ? "dynamic" : "static", + ModelHash.getTypeName(ModelHash.getUuidType(uuid)), + ModelHash.getUuidSubType(uuid), + handle.getObjectName(id), + id, + modelOverride.description, + opaqueBuffer, + alphaBuffer, + ex + ); + } } } } @@ -806,11 +827,15 @@ private void uploadTilePaint( SceneTilePaint paint, boolean onlyWaterSurface, int tileExX, int tileExY, int tileZ, - GpuIntBuffer vb, - GpuIntBuffer fb, + GpuIntBuffer opaqueBuffer, + GpuIntBuffer textureBuffer, int lx, int lz ) { + if (writeCache == null) + writeCache = new VertexWriteCache.Collection(); + writeCache.setOutputBuffers(opaqueBuffer, opaqueBuffer, textureBuffer); + int swColor = paint.getSwColor(); int seColor = paint.getSeColor(); int neColor = paint.getNeColor(); @@ -819,7 +844,7 @@ private void uploadTilePaint( if (neColor == HIDDEN_HSL) return; - TileOverride override = tileOverrideManager.getOverride(ctx, tile, worldPos); + TileOverride override = ctx.getTileOverride(tileZ, tileExX, tileExY, TILE_OVERRIDE_MAIN); WaterType waterType = proceduralGenerator.seasonalWaterType(override, paint.getTexture()); if (onlyWaterSurface && waterType == WaterType.NONE) return; @@ -866,19 +891,19 @@ private void uploadTilePaint( Material neMaterial = Material.NONE; Material nwMaterial = Material.NONE; - int[] swNormals = UP_NORMAL; - int[] seNormals = UP_NORMAL; - int[] neNormals = UP_NORMAL; - int[] nwNormals = UP_NORMAL; + short[] swNormals = UP_NORMAL; + short[] seNormals = UP_NORMAL; + short[] neNormals = UP_NORMAL; + short[] nwNormals = UP_NORMAL; int swTerrainData, seTerrainData, nwTerrainData, neTerrainData; swTerrainData = seTerrainData = nwTerrainData = neTerrainData = HDUtils.packTerrainData(true, 0, waterType, tileZ); if (!onlyWaterSurface) { - swNormals = ctx.vertexTerrainNormals.getOrDefault(swVertexKey, swNormals); - seNormals = ctx.vertexTerrainNormals.getOrDefault(seVertexKey, seNormals); - neNormals = ctx.vertexTerrainNormals.getOrDefault(neVertexKey, neNormals); - nwNormals = ctx.vertexTerrainNormals.getOrDefault(nwVertexKey, nwNormals); + swNormals = ctx.getVertexNormalOrDefault(swVertexKey, tileNormals[0], UP_NORMAL); + seNormals = ctx.getVertexNormalOrDefault(seVertexKey, tileNormals[1], UP_NORMAL); + neNormals = ctx.getVertexNormalOrDefault(neVertexKey, tileNormals[2], UP_NORMAL); + nwNormals = ctx.getVertexNormalOrDefault(nwVertexKey, tileNormals[3], UP_NORMAL); } if (waterType == WaterType.NONE) { @@ -930,25 +955,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.isVertexOverlay(neVertexKey) && ctx.isVertexUnderlay(neVertexKey)) neVertexIsOverlay = true; - if (ctx.vertexIsOverlay.containsKey(nwVertexKey) && ctx.vertexIsUnderlay.containsKey(nwVertexKey)) + if (ctx.isVertexOverlay(nwVertexKey) && ctx.isVertexUnderlay(nwVertexKey)) nwVertexIsOverlay = true; - if (ctx.vertexIsOverlay.containsKey(seVertexKey) && ctx.vertexIsUnderlay.containsKey(seVertexKey)) + if (ctx.isVertexOverlay(seVertexKey) && ctx.isVertexUnderlay(seVertexKey)) seVertexIsOverlay = true; - if (ctx.vertexIsOverlay.containsKey(swVertexKey) && ctx.vertexIsUnderlay.containsKey(swVertexKey)) + if (ctx.isVertexOverlay(swVertexKey) && ctx.isVertexUnderlay(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.isVertexWater(swVertexKey) && ctx.isVertexLand(swVertexKey)) swColor = 0; - if (ctx.vertexIsWater.containsKey(seVertexKey) && ctx.vertexIsLand.containsKey(seVertexKey)) + if (ctx.isVertexWater(seVertexKey) && ctx.isVertexLand(seVertexKey)) seColor = 0; - if (ctx.vertexIsWater.containsKey(nwVertexKey) && ctx.vertexIsLand.containsKey(nwVertexKey)) + if (ctx.isVertexWater(nwVertexKey) && ctx.isVertexLand(nwVertexKey)) nwColor = 0; - if (ctx.vertexIsWater.containsKey(neVertexKey) && ctx.vertexIsLand.containsKey(neVertexKey)) + if (ctx.isVertexWater(neVertexKey) && ctx.isVertexLand(neVertexKey)) neColor = 0; if (seColor == 0 && nwColor == 0 && (neColor == 0 || swColor == 0)) @@ -965,10 +990,10 @@ private void uploadTilePaint( neMaterial = groundMaterial.getRandomMaterial(worldPos[0] + 1, worldPos[1] + 1, worldPos[2]); } - int swDepth = ctx.vertexUnderwaterDepth.getOrDefault(swVertexKey, 0); - int seDepth = ctx.vertexUnderwaterDepth.getOrDefault(seVertexKey, 0); - int nwDepth = ctx.vertexUnderwaterDepth.getOrDefault(nwVertexKey, 0); - int neDepth = ctx.vertexUnderwaterDepth.getOrDefault(neVertexKey, 0); + int swDepth = ctx.getVertexUnderwaterDepth(swVertexKey); + int seDepth = ctx.getVertexUnderwaterDepth(seVertexKey); + int nwDepth = ctx.getVertexUnderwaterDepth(nwVertexKey); + int neDepth = ctx.getVertexUnderwaterDepth(neVertexKey); swHeight += swDepth; seHeight += seDepth; nwHeight += nwDepth; @@ -1002,59 +1027,64 @@ private void uploadTilePaint( uvx = fract(uvx * uvcos - uvy * uvsin); uvy = fract(tmp * uvsin + uvy * uvcos); - int texturedFaceIdx = fb.putFace( + final var vb = writeCache.getVertexBuffer(); + final var tb = writeCache.getTextureBuffer(); + + int texturedFaceIdx = tb.putFace( neColor, nwColor, seColor, neMaterialData, nwMaterialData, seMaterialData, neTerrainData, nwTerrainData, seTerrainData ); - vb.putVertex( + vb.putStaticVertex( lx2, neHeight, lz2, uvx, uvy, 0, neNormals[0], neNormals[2], neNormals[1], texturedFaceIdx ); - vb.putVertex( + vb.putStaticVertex( lx3, nwHeight, lz3, uvx - uvcos, uvy - uvsin, 0, nwNormals[0], nwNormals[2], nwNormals[1], texturedFaceIdx ); - vb.putVertex( + vb.putStaticVertex( lx1, seHeight, lz1, uvx + uvsin, uvy - uvcos, 0, seNormals[0], seNormals[2], seNormals[1], texturedFaceIdx ); - texturedFaceIdx = fb.putFace( + texturedFaceIdx = tb.putFace( swColor, seColor, nwColor, swMaterialData, seMaterialData, nwMaterialData, swTerrainData, seTerrainData, nwTerrainData ); - vb.putVertex( + vb.putStaticVertex( lx0, swHeight, lz0, uvx - uvcos + uvsin, uvy - uvsin - uvcos, 0, swNormals[0], swNormals[2], swNormals[1], texturedFaceIdx ); - vb.putVertex( + vb.putStaticVertex( lx1, seHeight, lz1, uvx + uvsin, uvy - uvcos, 0, seNormals[0], seNormals[2], seNormals[1], texturedFaceIdx ); - vb.putVertex( + vb.putStaticVertex( lx3, nwHeight, lz3, uvx - uvcos, uvy - uvsin, 0, nwNormals[0], nwNormals[2], nwNormals[1], texturedFaceIdx ); + + writeCache.flush(); } private void uploadTileModel( @@ -1064,9 +1094,13 @@ private void uploadTileModel( boolean onlyWaterSurface, int tileExX, int tileExY, int tileZ, int basex, int basez, - GpuIntBuffer vb, - GpuIntBuffer fb + GpuIntBuffer opaqueBuffer, + GpuIntBuffer textureBuffer ) { + if (writeCache == null) + writeCache = new VertexWriteCache.Collection(); + writeCache.setOutputBuffers(opaqueBuffer, opaqueBuffer, textureBuffer); + final int[] triangleTextures = model.getTriangleTextureId(); boolean isFallbackWater = false; if (triangleTextures != null) { @@ -1077,10 +1111,8 @@ private void uploadTileModel( } } } - int overlayId = OVERLAY_FLAG | overlayIds[tileZ][tileExX][tileExY]; - int underlayId = underlayIds[tileZ][tileExX][tileExY]; - var overlayOverride = tileOverrideManager.getOverride(ctx, tile, worldPos, overlayId); - var underlayOverride = tileOverrideManager.getOverride(ctx, tile, worldPos, underlayId); + var overlayOverride = ctx.getTileOverride(tileZ, tileExX, tileExY, TILE_OVERRIDE_OVERLAY); + var underlayOverride = ctx.getTileOverride(tileZ, tileExX, tileExY, TILE_OVERRIDE_UNDERLAY); WaterType overlayWaterType = proceduralGenerator.seasonalWaterType(overlayOverride, 0); WaterType underlayWaterType = proceduralGenerator.seasonalWaterType(underlayOverride, 0); boolean isOverlayWater = overlayWaterType != WaterType.NONE; @@ -1158,17 +1190,17 @@ private void uploadTileModel( int uvOrientation = 0; float uvScale = 1; - int[] normalsA = UP_NORMAL; - int[] normalsB = UP_NORMAL; - int[] normalsC = UP_NORMAL; + short[] normalsA = UP_NORMAL; + short[] normalsB = UP_NORMAL; + short[] normalsC = UP_NORMAL; int terrainDataA, terrainDataB, terrainDataC; terrainDataA = terrainDataB = terrainDataC = HDUtils.packTerrainData(true, 0, waterType, tileZ); if (!onlyWaterSurface) { - normalsA = ctx.vertexTerrainNormals.getOrDefault(vertexKeyA, normalsA); - normalsB = ctx.vertexTerrainNormals.getOrDefault(vertexKeyB, normalsB); - normalsC = ctx.vertexTerrainNormals.getOrDefault(vertexKeyC, normalsC); + normalsA = ctx.getVertexNormalOrDefault(vertexKeyA, tileNormals[0], UP_NORMAL); + normalsB = ctx.getVertexNormalOrDefault(vertexKeyB, tileNormals[1], UP_NORMAL); + normalsC = ctx.getVertexNormalOrDefault(vertexKeyC, tileNormals[2], UP_NORMAL); } if (!isWater) { @@ -1231,11 +1263,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.isVertexWater(vertexKeyA) && ctx.isVertexLand(vertexKeyA)) colorA = 0; - if (ctx.vertexIsWater.containsKey(vertexKeyB) && ctx.vertexIsLand.containsKey(vertexKeyB)) + if (ctx.isVertexWater(vertexKeyB) && ctx.isVertexLand(vertexKeyB)) colorB = 0; - if (ctx.vertexIsWater.containsKey(vertexKeyC) && ctx.vertexIsLand.containsKey(vertexKeyC)) + if (ctx.isVertexWater(vertexKeyC) && ctx.isVertexLand(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 @@ -1262,9 +1294,9 @@ private void uploadTileModel( ); } - int depthA = ctx.vertexUnderwaterDepth.getOrDefault(vertexKeyA, 0); - int depthB = ctx.vertexUnderwaterDepth.getOrDefault(vertexKeyB, 0); - int depthC = ctx.vertexUnderwaterDepth.getOrDefault(vertexKeyC, 0); + int depthA = ctx.getVertexUnderwaterDepth(vertexKeyA); + int depthB = ctx.getVertexUnderwaterDepth(vertexKeyB); + int depthC = ctx.getVertexUnderwaterDepth(vertexKeyC); ly0 += depthA; ly1 += depthB; ly2 += depthC; @@ -1274,11 +1306,11 @@ private void uploadTileModel( terrainDataC = HDUtils.packTerrainData(true, max(1, depthC), waterType, tileZ); } - if (ctx.vertexIsOverlay.containsKey(vertexKeyA) && ctx.vertexIsUnderlay.containsKey(vertexKeyA)) + if (ctx.isVertexOverlay(vertexKeyA) && ctx.isVertexUnderlay(vertexKeyA)) vertexAIsOverlay = true; - if (ctx.vertexIsOverlay.containsKey(vertexKeyB) && ctx.vertexIsUnderlay.containsKey(vertexKeyB)) + if (ctx.isVertexOverlay(vertexKeyB) && ctx.isVertexUnderlay(vertexKeyB)) vertexBIsOverlay = true; - if (ctx.vertexIsOverlay.containsKey(vertexKeyC) && ctx.vertexIsUnderlay.containsKey(vertexKeyC)) + if (ctx.isVertexOverlay(vertexKeyC) && ctx.isVertexUnderlay(vertexKeyC)) vertexCIsOverlay = true; ly0 -= override.heightOffset; @@ -1313,33 +1345,37 @@ private void uploadTileModel( float uvCx = uvx + dx * uvcos - dz * uvsin; float uvCy = uvy + dx * uvsin + dz * uvcos; - int texturedFaceIdx = fb.putFace( + final VertexWriteCache vb = writeCache.getVertexBuffer(); + final VertexWriteCache tb = writeCache.getTextureBuffer(); + + int texturedFaceIdx = tb.putFace( colorA, colorB, colorC, materialDataA, materialDataB, materialDataC, terrainDataA, terrainDataB, terrainDataC ); - vb.putVertex( + vb.putStaticVertex( lx0, ly0, lz0, uvAx, uvAy, 0, normalsA[0], normalsA[2], normalsA[1], texturedFaceIdx ); - vb.putVertex( + vb.putStaticVertex( lx1, ly1, lz1, uvBx, uvBy, 0, normalsB[0], normalsB[2], normalsB[1], texturedFaceIdx ); - vb.putVertex( + vb.putStaticVertex( lx2, ly2, lz2, uvCx, uvCy, 0, normalsC[0], normalsC[2], normalsC[1], texturedFaceIdx ); } + writeCache.flush(); } // scene upload @@ -1351,6 +1387,7 @@ private int uploadStaticModel( int uuid, int preOrientation, int orientation, int x, int y, int z, + int tileExX, int tileExY, int tileZ, GpuIntBuffer opaqueBuffer, GpuIntBuffer alphaBuffer, GpuIntBuffer textureBuffer @@ -1396,6 +1433,8 @@ private int uploadStaticModel( orientCos = COSINE[orientation]; } + ensureVerticesAllocated(vertexCount); + for (int v = 0, vertexOffset = 0; v < vertexCount; ++v) { int vx = (int) vertexX[v]; int vy = (int) vertexY[v]; @@ -1413,14 +1452,13 @@ private int uploadStaticModel( vz += z; if (modelOverride.terrainVertexSnap && heightFrac <= modelOverride.terrainVertexSnapThreshold) { - int plane = tile.getRenderLevel(); - int tileExX = clamp(ctx.sceneOffset + ((vx + basex) / 128), 0, EXTENDED_SCENE_SIZE - 1); - int tileExY = clamp(ctx.sceneOffset + ((vz + basez) / 128), 0, EXTENDED_SCENE_SIZE - 1); + int vertexTileExX = clamp(ctx.sceneOffset + ((vx + basex) / 128), 0, EXTENDED_SCENE_SIZE - 1); + int vertexTileExY = clamp(ctx.sceneOffset + ((vz + basez) / 128), 0, EXTENDED_SCENE_SIZE - 1); - float h00 = tileHeights[plane][tileExX][tileExY]; - float h10 = tileHeights[plane][tileExX + 1][tileExY]; - float h01 = tileHeights[plane][tileExX][tileExY + 1]; - float h11 = tileHeights[plane][tileExX + 1][tileExY + 1]; + float h00 = tileHeights[tileZ][vertexTileExX][vertexTileExY]; + float h10 = tileHeights[tileZ][vertexTileExX + 1][vertexTileExY]; + float h01 = tileHeights[tileZ][vertexTileExX][vertexTileExY + 1]; + float h11 = tileHeights[tileZ][vertexTileExX + 1][vertexTileExY + 1]; float hx0 = mix(h00, h10, (vx % 128.0f) / 128.0f); float hx1 = mix(h01, h11, (vx % 128.0f) / 128.0f); @@ -1430,9 +1468,9 @@ private int uploadStaticModel( vy = (int) mix(h, vy, blend); } - modelLocalI[vertexOffset++] = vx; - modelLocalI[vertexOffset++] = vy; - modelLocalI[vertexOffset++] = vz; + modelVertices[vertexOffset++] = vx; + modelVertices[vertexOffset++] = vy; + modelVertices[vertexOffset++] = vz; } boolean isVanillaTextured = faceTextures != null; @@ -1496,19 +1534,19 @@ private int uploadStaticModel( final int triangleC = indices3[face]; int vertexOffset = triangleA * 3; - final int vx1 = modelLocalI[vertexOffset]; - final int vy1 = modelLocalI[vertexOffset + 1]; - final int vz1 = modelLocalI[vertexOffset + 2]; + final int vx1 = modelVertices[vertexOffset]; + final int vy1 = modelVertices[vertexOffset + 1]; + final int vz1 = modelVertices[vertexOffset + 2]; vertexOffset = triangleB * 3; - final int vx2 = modelLocalI[vertexOffset]; - final int vy2 = modelLocalI[vertexOffset + 1]; - final int vz2 = modelLocalI[vertexOffset + 2]; + final int vx2 = modelVertices[vertexOffset]; + final int vy2 = modelVertices[vertexOffset + 1]; + final int vz2 = modelVertices[vertexOffset + 2]; vertexOffset = triangleC * 3; - final int vx3 = modelLocalI[vertexOffset]; - final int vy3 = modelLocalI[vertexOffset + 1]; - final int vz3 = modelLocalI[vertexOffset + 2]; + final int vx3 = modelVertices[vertexOffset]; + final int vy3 = modelVertices[vertexOffset + 1]; + final int vz3 = modelVertices[vertexOffset + 2]; boolean keepShading = isTextured; if (isTextured) { @@ -1539,8 +1577,7 @@ private int uploadStaticModel( int averageColor = (tilePaint.getSwColor() + tilePaint.getNwColor() + tilePaint.getNeColor() + tilePaint.getSeColor()) / 4; - var override = tileOverrideManager.getOverride(ctx, tile); - averageColor = override.modifyColor(averageColor); + averageColor = ctx.getTileOverride(tileZ, tileExX, tileExY, TILE_OVERRIDE_MAIN).modifyColor(averageColor); color1 = color2 = color3 = averageColor; // Let the shader know vanilla shading reversal should be skipped for this face @@ -1570,16 +1607,10 @@ private int uploadStaticModel( if (faceColorIndex != -1) { int color = tileModel.getTriangleColorA()[faceColorIndex]; if (color != HIDDEN_HSL) { - var scenePos = tile.getSceneLocation(); - int tileX = scenePos.getX(); - int tileY = scenePos.getY(); - int tileZ = tile.getRenderLevel(); - int tileExX = tileX + ctx.sceneOffset; - int tileExY = tileY + ctx.sceneOffset; - int tileId = modelOverride.inheritTileColorType == InheritTileColorType.OVERLAY ? - OVERLAY_FLAG | scene.getOverlayIds()[tileZ][tileExX][tileExY] : - scene.getUnderlayIds()[tileZ][tileExX][tileExY]; - var override = tileOverrideManager.getOverride(ctx, tile, worldPos, tileId); + var override = ctx.getTileOverride(tileZ, tileExX, tileExY, + modelOverride.inheritTileColorType == InheritTileColorType.OVERLAY ? + TILE_OVERRIDE_OVERLAY : TILE_OVERRIDE_UNDERLAY); + color = override.modifyColor(color); color1 = color2 = color3 = color; @@ -1592,20 +1623,18 @@ private int uploadStaticModel( } if (plugin.configLegacyTzHaarReskin && modelOverride.tzHaarRecolorType != TzHaarRecolorType.NONE) { - // The legacy TzHaar reskin is not thread-safe - synchronized (proceduralGenerator) { - int[] tzHaarRecolored = ProceduralGenerator.recolorTzHaar( - modelOverride, - model, - face, - color1, - color2, - color3 - ); - color1 = tzHaarRecolored[0]; - color2 = tzHaarRecolored[1]; - color3 = tzHaarRecolored[2]; - } + ProceduralGenerator.recolorTzHaar( + modelOverride, + model, + face, + color1, + color2, + color3, + tzHaarRecolored + ); + color1 = tzHaarRecolored[0]; + color2 = tzHaarRecolored[1]; + color3 = tzHaarRecolored[2]; } } @@ -1666,8 +1695,7 @@ private int uploadStaticModel( 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; - final VertexWriteCache vb = writeCache.useAlphaBuffer && hasAlpha ? writeCache.alpha : writeCache.opaque; + final VertexWriteCache vb = writeCache.getVertexBuffer(material.hasTransparency || transparency != 0); final VertexWriteCache tb = writeCache.opaqueTex; color1 |= packedAlphaBiasHsl; @@ -1710,8 +1738,8 @@ public boolean preprocessTempModel( Projection proj, float[][] sceneFrustumPlanes, int[] faceDistances, - PrimitiveIntArray visibleFaces, - PrimitiveIntArray culledFaces, + PrimitiveCharArray visibleFaces, + PrimitiveCharArray culledFaces, boolean isModelPartiallyVisible, ModelOverride modelOverride, Model model, @@ -1725,11 +1753,8 @@ public boolean preprocessTempModel( final float[] verticesY = model.getVerticesY(); final float[] verticesZ = model.getVerticesZ(); - final float[] modelLocal = this.modelLocal; - final int[] modelLocalI = this.modelLocalI; - final float[] modelProjected = this.modelProjected; - final boolean[] visibility = this.visibility; - final float[] projected = this.projected; + final boolean[] visibility = PooledArrayType.BOOL.borrow(vertexCount); + final float[] modelProjected = PooledArrayType.FLOAT.borrow(vertexCount * 3); // Identity orient, will result in no rotation float orientSinf = 0; @@ -1741,6 +1766,8 @@ public boolean preprocessTempModel( orientCosf = COSINE[orientation] / 65536f; } + ensureVerticesAllocated(vertexCount); + boolean shouldSort = true; boolean allVertsVisible = true; for (int v = 0, vertexOffset = 0; v < vertexCount; ++v) { @@ -1774,18 +1801,15 @@ public boolean preprocessTempModel( if (pZ <= 0.0f) visibility[v] = allVertsVisible = false; - modelLocal[vertexOffset] = vertexX; - modelLocalI[vertexOffset] = Float.floatToIntBits(vertexX); + modelVertices[vertexOffset] = Float.floatToIntBits(vertexX); modelProjected[vertexOffset] = pX / pZ; vertexOffset++; - modelLocal[vertexOffset] = vertexY; - modelLocalI[vertexOffset] = Float.floatToIntBits(vertexY); + modelVertices[vertexOffset] = Float.floatToIntBits(vertexY); modelProjected[vertexOffset] = pY / pZ; vertexOffset++; - modelLocal[vertexOffset] = vertexZ; - modelLocalI[vertexOffset] = Float.floatToIntBits(vertexZ); + modelVertices[vertexOffset] = Float.floatToIntBits(vertexZ); modelProjected[vertexOffset] = pZ; vertexOffset++; @@ -1818,8 +1842,12 @@ public boolean preprocessTempModel( final int zero = (int) proj.project(x, y, z, projected)[2]; final int radius = model.getRadius(); + faceOverrides.ensureCapacity(triangleCount); + faceMaterials.ensureCapacity(triangleCount); + faceUVTypes.ensureCapacity(triangleCount); + tempModelAlphaFaces = 0; - for (int f = 0; f < triangleCount; f++) { + for (char f = 0; f < triangleCount; f++) { if (color3s[f] == -2) continue; @@ -1870,9 +1898,9 @@ public boolean preprocessTempModel( } } - faceOverrides[f] = faceOverride; - faceMaterials[f] = material; - faceUVTypes[f] = uvType; + faceOverrides.set(f,faceOverride); + faceMaterials.set(f, material); + faceUVTypes.set(f, uvType); int offsetA = indices1[f]; int offsetB = indices2[f]; @@ -1923,12 +1951,15 @@ public boolean preprocessTempModel( visibleFaces.put(f); } + PooledArrayType.BOOL.release(visibility); + PooledArrayType.FLOAT.release(modelProjected); + return shouldSort; } // temp draw public void uploadTempModel( - PrimitiveIntArray faces, + PrimitiveCharArray faces, Model model, ModelOverride modelOverride, int preOrientation, @@ -1965,6 +1996,7 @@ public void uploadTempModel( final byte[] bias = model.getFaceBias(); final int[] faceNormals = isShadow ? EMPTY_NORMALS : modelNormals; + final int faceCount = model.getFaceCount(); final boolean hasBias = bias != null; final boolean modelHasNormals = model.getVertexNormalsX() != null && @@ -1986,9 +2018,9 @@ public void uploadTempModel( orientCos = COSINE[orientation]; } - final int faceCount = faces.length; - for (int f = 0; f < faceCount; ++f) { + for (int f = 0; f < faces.length; ++f) { final int face = faces.array[f]; + if(face >= faceCount) continue; int color1 = color1s[face]; int color2 = color2s[face]; @@ -2002,9 +2034,9 @@ else if (color3 == -1) final int transparency = transparencies != null ? transparencies[face] & 0xFF : 0; final int textureFace = textureFaces != null ? textureFaces[face] : -1; final int textureId = isVanillaTextured ? faceTextures[face] : -1; - final UvType uvType = faceUVTypes[face]; - final Material material = faceMaterials[face]; - final ModelOverride faceOverride = faceOverrides[face]; + final UvType uvType = faceUVTypes.get(face); + final Material material = faceMaterials.get(face); + final ModelOverride faceOverride = faceOverrides.get(face); if (textureId != -1) color1 = color2 = color3 = 90; @@ -2049,11 +2081,11 @@ else if (color3 == -1) } if (shouldCalculateFaceNormal) { - calculateFaceNormal( + calculateFaceNormalInt( faceNormals, - modelLocal[vertexOffsetA], modelLocal[vertexOffsetA + 1], modelLocal[vertexOffsetA + 2], - modelLocal[vertexOffsetB], modelLocal[vertexOffsetB + 1], modelLocal[vertexOffsetB + 2], - modelLocal[vertexOffsetC], modelLocal[vertexOffsetC + 1], modelLocal[vertexOffsetC + 2] + modelVertices[vertexOffsetA], modelVertices[vertexOffsetA + 1], modelVertices[vertexOffsetA + 2], + modelVertices[vertexOffsetB], modelVertices[vertexOffsetB + 1], modelVertices[vertexOffsetB + 2], + modelVertices[vertexOffsetC], modelVertices[vertexOffsetC + 1], modelVertices[vertexOffsetC + 2] ); } @@ -2083,18 +2115,8 @@ else if (color3 == -1) color2 |= packedAlphaBiasHsl; color3 |= packedAlphaBiasHsl; - final VertexWriteCache vb, tb; - if (writeCache.useAlphaBuffer && hasAlpha) { - vb = writeCache.alpha; - tb = writeCache.alphaTex; - } else { - vb = writeCache.opaque; - tb = writeCache.opaqueTex; - } - - color1 |= packedAlphaBiasHsl; - color2 |= packedAlphaBiasHsl; - color3 |= packedAlphaBiasHsl; + final VertexWriteCache vb = writeCache.getVertexBuffer(hasAlpha); + final VertexWriteCache tb = writeCache.getTextureBuffer(hasAlpha); final int texturedFaceIdx = tb.putFace( color1, color2, color3, @@ -2103,19 +2125,19 @@ else if (color3 == -1) ); vb.putVertex( - modelLocalI[vertexOffsetA], modelLocalI[vertexOffsetA + 1], modelLocalI[vertexOffsetA + 2], + modelVertices[vertexOffsetA], modelVertices[vertexOffsetA + 1], modelVertices[vertexOffsetA + 2], faceUVs[0], faceUVs[1], faceUVs[2], faceNormals[0], faceNormals[1], faceNormals[2], texturedFaceIdx ); vb.putVertex( - modelLocalI[vertexOffsetB], modelLocalI[vertexOffsetB + 1], modelLocalI[vertexOffsetB + 2], + modelVertices[vertexOffsetB], modelVertices[vertexOffsetB + 1], modelVertices[vertexOffsetB + 2], faceUVs[4], faceUVs[5], faceUVs[6], faceNormals[3], faceNormals[4], faceNormals[5], texturedFaceIdx ); vb.putVertex( - modelLocalI[vertexOffsetC], modelLocalI[vertexOffsetC + 1], modelLocalI[vertexOffsetC + 2], + modelVertices[vertexOffsetC], modelVertices[vertexOffsetC + 1], modelVertices[vertexOffsetC + 2], faceUVs[8], faceUVs[9], faceUVs[10], faceNormals[6], faceNormals[7], faceNormals[8], texturedFaceIdx @@ -2125,6 +2147,19 @@ else if (color3 == -1) writeCache.flush(); } + public static void calculateFaceNormalInt( + int[] out, + int vx1, int vy1, int vz1, + int vx2, int vy2, int vz2, + int vx3, int vy3, int vz3 + ) { + calculateFaceNormal(out, + Float.intBitsToFloat(vx1), Float.intBitsToFloat(vy1), Float.intBitsToFloat(vz1), + Float.intBitsToFloat(vx2), Float.intBitsToFloat(vy2), Float.intBitsToFloat(vz2), + Float.intBitsToFloat(vx3), Float.intBitsToFloat(vy3), Float.intBitsToFloat(vz3) + ); + } + public static void calculateFaceNormal( int[] out, float vx1, float vy1, float vz1, diff --git a/src/main/java/rs117/hd/renderer/zone/VertexWriteCache.java b/src/main/java/rs117/hd/renderer/zone/VertexWriteCache.java index b72914f70c..93867ba020 100644 --- a/src/main/java/rs117/hd/renderer/zone/VertexWriteCache.java +++ b/src/main/java/rs117/hd/renderer/zone/VertexWriteCache.java @@ -124,7 +124,7 @@ public void flush() { } public static class Collection { - private static final int CAPACITY = (int) (128 * KiB / Integer.BYTES); + private static final int CAPACITY = (int) (8 * KiB / Integer.BYTES); public final VertexWriteCache opaque = new VertexWriteCache("OPAQUE", CAPACITY); public final VertexWriteCache alpha = new VertexWriteCache("ALPHA", CAPACITY); @@ -158,6 +158,17 @@ public void setOutputBuffers(GpuIntBuffer opaque, GpuIntBuffer alpha, GpuIntBuff } } + public VertexWriteCache getVertexBuffer() { return opaque; } + public VertexWriteCache getTextureBuffer() { return opaqueTex; } + + public VertexWriteCache getVertexBuffer(boolean hasAlpha) { + return useAlphaBuffer && hasAlpha ? alpha : opaque; + } + + public VertexWriteCache getTextureBuffer(boolean hasAlpha) { + return useAlphaBuffer && hasAlpha ? alphaTex : opaqueTex; + } + public void flush() { opaque.flush(); alpha.flush(); diff --git a/src/main/java/rs117/hd/renderer/zone/WorldViewContext.java b/src/main/java/rs117/hd/renderer/zone/WorldViewContext.java index 429c67f3d1..8760e398d8 100644 --- a/src/main/java/rs117/hd/renderer/zone/WorldViewContext.java +++ b/src/main/java/rs117/hd/renderer/zone/WorldViewContext.java @@ -27,6 +27,7 @@ import static rs117.hd.renderer.zone.DynamicModelVAO.METADATA_SIZE; import static rs117.hd.renderer.zone.SceneManager.NUM_ZONES; import static rs117.hd.renderer.zone.ZoneRenderer.FRAMES_IN_FLIGHT; +import static rs117.hd.utils.HDUtils.quickSort; @Slf4j public class WorldViewContext { @@ -181,9 +182,9 @@ void sortStaticAlphaModels(Camera camera) { } if (!alphaZones.isEmpty()) { - alphaZones.sort(alphaSortComparator); - for (Zone z : alphaZones) - z.alphaStaticModelSort(camera); + quickSort(alphaZones, alphaSortComparator); + for (int i = 0; i < alphaZones.size(); i++) + alphaZones.get(i).alphaStaticModelSort(camera); } } diff --git a/src/main/java/rs117/hd/renderer/zone/Zone.java b/src/main/java/rs117/hd/renderer/zone/Zone.java index 97f8004142..8c6ed4dfeb 100644 --- a/src/main/java/rs117/hd/renderer/zone/Zone.java +++ b/src/main/java/rs117/hd/renderer/zone/Zone.java @@ -7,9 +7,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.function.ToIntFunction; import javax.annotation.Nullable; import javax.inject.Inject; import lombok.extern.slf4j.Slf4j; @@ -27,6 +25,7 @@ import rs117.hd.utils.HDUtils; import rs117.hd.utils.buffer.GLBuffer; import rs117.hd.utils.buffer.GLTextureBuffer; +import rs117.hd.utils.collections.Int2IntHashMap; import static org.lwjgl.opengl.GL33C.*; import static rs117.hd.HdPlugin.GL_CAPS; @@ -34,6 +33,7 @@ import static rs117.hd.HdPlugin.checkGLErrors; import static rs117.hd.renderer.zone.ZoneRenderer.TEXTURE_UNIT_TEXTURED_FACES; import static rs117.hd.renderer.zone.ZoneRenderer.eboAlpha; +import static rs117.hd.utils.HDUtils.quickSort; import static rs117.hd.utils.MathUtils.*; @Slf4j @@ -283,16 +283,15 @@ 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]); } } - for (AlphaModel m : alphaModels) { - m.rid = (short) (int) updates.getOrDefault((int) m.rid, (int) m.rid); - } + for (AlphaModel m : alphaModels) + m.rid = (short) updates.getOrDefault(m.rid, m.rid); } private static final int NUM_DRAW_RANGES = 512; @@ -403,7 +402,7 @@ private static void pushRange(int start, int end) { } } - public static class AlphaModel { + public static final class AlphaModel { int id; ModelOverride modelOverride; int startpos, endpos; @@ -441,6 +440,14 @@ boolean isTemp() { return packedFaces == null || sortedFaces == null; } + int calculateDepth(int cx, int cy, int cz, int zx, int zz) { + final int mx = (x + ((zx - zofx) << 10)); + final int mz = (z + ((zz - zofz) << 10)); + return (mx - cx) * (mx - cx) + + (y - cy) * (y - cy) + + (mz - cz) * (mz - cz); + } + void setView(DynamicModelVAO.View view) { vao = view.vao; tboF = view.tboTexId; @@ -659,31 +666,35 @@ synchronized void postAlphaPass() { private static int lastTboF; private static int lastzx, lastzz; - private static final class AlphaSortPredicate implements ToIntFunction { - int cx, cy, cz; + static class AlphaModelComparator implements Comparator { int zx, zz; + int cx, cy, cz; @Override - public int applyAsInt(AlphaModel m) { - final int mx = m.x + ((zx - m.zofx) << 10); - final int mz = m.z + ((zz - m.zofz) << 10); - final int my = m.y; - return (mx - cx) * (mx - cx) + (my - cy) * (my - cy) + (mz - cz) * (mz - cz); + public int compare(AlphaModel modelA, AlphaModel modelB) + { + return Integer.compare( + modelA.calculateDepth(cx, cy, cz, zx, zz), + modelB.calculateDepth(cx, cy, cz, zx, zz) + ); } } - private final AlphaSortPredicate alphaSortPred = new AlphaSortPredicate(); - private final Comparator alphaSortComparator = Comparator.comparingInt(alphaSortPred).reversed(); - + private static final AlphaModelComparator alphaModelComparator = new AlphaModelComparator(); private final EboAlphaWriterJob sortedAlphaFacesUpload = new EboAlphaWriterJob(); synchronized void alphaSort(int zx, int zz, Camera camera) { - alphaSortPred.cx = (int) camera.getPositionX(); - alphaSortPred.cy = (int) camera.getPositionY(); - alphaSortPred.cz = (int) camera.getPositionZ(); - alphaSortPred.zx = zx; - alphaSortPred.zz = zz; - alphaModels.sort(alphaSortComparator); + final int alphaModelCount = alphaModels.size(); + if (alphaModelCount <= 1) + return; + + alphaModelComparator.cx = (int) camera.getPositionX(); + alphaModelComparator.cy = (int) camera.getPositionY(); + alphaModelComparator.cz = (int) camera.getPositionZ(); + alphaModelComparator.zx = zx; + alphaModelComparator.zz = zz; + + quickSort(alphaModels, alphaModelComparator); } void alphaStaticModelSort(Camera camera) { diff --git a/src/main/java/rs117/hd/renderer/zone/ZoneSceneContext.java b/src/main/java/rs117/hd/renderer/zone/ZoneSceneContext.java index 7f883f98a0..5bdac44ceb 100644 --- a/src/main/java/rs117/hd/renderer/zone/ZoneSceneContext.java +++ b/src/main/java/rs117/hd/renderer/zone/ZoneSceneContext.java @@ -1,21 +1,18 @@ package rs117.hd.renderer.zone; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; import javax.annotation.Nullable; import net.runelite.api.*; import rs117.hd.scene.SceneContext; +import rs117.hd.utils.collections.Int2IntHashMap; +import rs117.hd.utils.collections.IntHashSet; public class ZoneSceneContext extends SceneContext { public int totalReused; public int totalDeferred; public int totalMapZones; - public final Set animatedDynamicObjectIds = new HashSet<>(); - public final Map animatedDynamicObjectImpostors; + public final IntHashSet animatedDynamicObjectIds = new IntHashSet(); + public final Int2IntHashMap animatedDynamicObjectImpostors; public ZoneSceneContext( Client client, @@ -29,9 +26,9 @@ public ZoneSceneContext( sceneOffset = 0; sizeX = worldView.getSizeX(); sizeZ = worldView.getSizeY(); - animatedDynamicObjectImpostors = Collections.emptyMap(); + animatedDynamicObjectImpostors = new Int2IntHashMap(0); } else { - animatedDynamicObjectImpostors = new HashMap<>(); + animatedDynamicObjectImpostors = new Int2IntHashMap(); } } } diff --git a/src/main/java/rs117/hd/scene/GamevalManager.java b/src/main/java/rs117/hd/scene/GamevalManager.java index 5286a8cb80..a16b42e2ad 100644 --- a/src/main/java/rs117/hd/scene/GamevalManager.java +++ b/src/main/java/rs117/hd/scene/GamevalManager.java @@ -39,6 +39,10 @@ public class GamevalManager { private FileWatcher.UnregisterCallback fileWatcher; + private final HashSet handles = new HashSet<>(); + private long clearTime = 0; + + private static boolean loaded = false; private static final Map> GAMEVALS = new HashMap<>(); static { @@ -50,19 +54,41 @@ private static void clearGamevals() { GAMEVALS.put(OBJECT_KEY, Collections.emptyMap()); GAMEVALS.put(ANIM_KEY, Collections.emptyMap()); GAMEVALS.put(SPOTANIM_KEY, Collections.emptyMap()); + loaded = false; } public void startUp() throws IOException { - fileWatcher = GAMEVAL_PATH.watch((path, first) -> { - try { - Map> gamevals = plugin.getGson() - .fromJson(path.toReader(), new TypeToken>>() {}.getType()); - GAMEVALS.replaceAll((k, v) -> gamevals.getOrDefault(k, Collections.emptyMap())); - log.debug("Loaded gameval mappings"); - } catch (IOException ex) { - log.error("Failed to load gamevals:", ex); - } - }); + fileWatcher = GAMEVAL_PATH.watch((path, first) -> loadGamevals()); + } + + public void update() { + if(!loaded || clearTime == 0 || System.currentTimeMillis() < clearTime) + return; + + log.debug("Clearing gameval mappings"); + clearGamevals(); + clearTime = 0; + } + + private void loadGamevals() { + try { + Map> gamevals = plugin.getGson() + .fromJson(GAMEVAL_PATH.toReader(), new TypeToken>>() {}.getType()); + GAMEVALS.replaceAll((k, v) -> gamevals.getOrDefault(k, Collections.emptyMap())); + loaded = true; + log.debug("Loaded gameval mappings"); + } catch (IOException ex) { + log.error("Failed to load gamevals:", ex); + } + } + + public Handle obtainHandle() { + if(!loaded) + loadGamevals(); + Handle handle = new Handle(); + handles.add(handle); + clearTime = 0; // Clear the clear time since we've requeued the handle + return handle; } public void shutDown() { @@ -73,6 +99,9 @@ public void shutDown() { } private String getName(String key, int id) { + if(!loaded) + log.warn("Gamevals not loaded yet, will fail to resolve name."); + return GAMEVALS .get(key) .entrySet() @@ -83,54 +112,6 @@ private String getName(String key, int id) { .orElse(null); } - public Map getNpcs() { - return GAMEVALS.get(NPC_KEY); - } - - public Map getObjects() { - return GAMEVALS.get(OBJECT_KEY); - } - - public Map getAnims() { - return GAMEVALS.get(ANIM_KEY); - } - - public Map getSpotanims() { - return GAMEVALS.get(SPOTANIM_KEY); - } - - public int getNpcId(String name) { - return getNpcs().get(name); - } - - public int getObjectId(String name) { - return getObjects().get(name); - } - - public int getAnimId(String name) { - return getAnims().get(name); - } - - public int getSpotanimId(String name) { - return getSpotanims().get(name); - } - - public String getNpcName(int id) { - return getName(NPC_KEY, id); - } - - public String getObjectName(int id) { - return getName(OBJECT_KEY, id); - } - - public String getAnimName(int id) { - return getName(ANIM_KEY, id); - } - - public String getSpotanimName(int id) { - return getName(SPOTANIM_KEY, id); - } - @Slf4j @RequiredArgsConstructor private abstract static class GamevalAdapter extends TypeAdapter> { @@ -230,4 +211,62 @@ public SpotanimAdapter() { super(SPOTANIM_KEY); } } + + public class Handle implements AutoCloseable { + + public Map getNpcs() { + return GAMEVALS.get(NPC_KEY); + } + + public Map getObjects() { + return GAMEVALS.get(OBJECT_KEY); + } + + public Map getAnims() { + return GAMEVALS.get(ANIM_KEY); + } + + public Map getSpotanims() { + return GAMEVALS.get(SPOTANIM_KEY); + } + + public int getNpcId(String name) { + return getNpcs().get(name); + } + + public int getObjectId(String name) { + return getObjects().get(name); + } + + public int getAnimId(String name) { + return getAnims().get(name); + } + + public int getSpotanimId(String name) { + return getSpotanims().get(name); + } + + public String getNpcName(int id) { + return getName(NPC_KEY, id); + } + + public String getObjectName(int id) { + return getName(OBJECT_KEY, id); + } + + public String getAnimName(int id) { + return getName(ANIM_KEY, id); + } + + public String getSpotanimName(int id) { + return getName(SPOTANIM_KEY, id); + } + + @Override + public void close() { + handles.remove(this); + if(handles.isEmpty()) + clearTime = System.currentTimeMillis() + 10000; // Set the clear time to 10 seconds from now + } + } } diff --git a/src/main/java/rs117/hd/scene/LightManager.java b/src/main/java/rs117/hd/scene/LightManager.java index 9773505838..d54738eb32 100644 --- a/src/main/java/rs117/hd/scene/LightManager.java +++ b/src/main/java/rs117/hd/scene/LightManager.java @@ -65,6 +65,7 @@ import static net.runelite.api.Constants.*; import static net.runelite.api.Perspective.*; import static rs117.hd.utils.HDUtils.isSphereIntersectingFrustum; +import static rs117.hd.utils.HDUtils.quickSort; import static rs117.hd.utils.MathUtils.*; import static rs117.hd.utils.ResourcePath.path; @@ -483,9 +484,11 @@ public void update(@Nonnull SceneContext sceneContext, int[] cameraShift, float[ } // Order visible lights first, then by distance. Leave hidden lights unordered at the end. - sceneContext.lights.sort((a, b) -> a.visible && b.visible ? - Float.compare(a.distanceSquared, b.distanceSquared) : - Boolean.compare(b.visible, a.visible)); + quickSort(sceneContext.lights, + (a, b) -> a.visible && b.visible ? + Float.compare(a.distanceSquared, b.distanceSquared) : + Boolean.compare(b.visible, a.visible) + ); // Count number of visible lights sceneContext.numVisibleLights = 0; diff --git a/src/main/java/rs117/hd/scene/ModelOverrideManager.java b/src/main/java/rs117/hd/scene/ModelOverrideManager.java index d3ac0da735..73827ca1b3 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.collections.Int2ObjectHashMap; +import rs117.hd.utils.collections.IntHashSet; import static rs117.hd.utils.ResourcePath.path; @@ -47,14 +48,14 @@ 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; public void startUp() { fileWatcher = MODEL_OVERRIDES_PATH.watch((path, first) -> clientThread.invoke(() -> { - try { + try (GamevalManager.Handle gamevalHandle = gamevalManager.obtainHandle()) { sceneManager.getLoadingLock().lock(); sceneManager.completeAllStreaming(); @@ -71,23 +72,26 @@ public void startUp() { continue; } - addOverride(override); + addOverride(override, gamevalHandle); if (override.hideInAreas.length > 0) { var hider = override.copy(); hider.hide = true; hider.areas = override.hideInAreas; - addOverride(hider); + addOverride(hider, gamevalHandle); } } - addOverride(fishingSpotReplacer.getModelOverride()); - addSailingCullingOverrides(); + addOverride(fishingSpotReplacer.getModelOverride(), gamevalHandle); + addSailingCullingOverrides(gamevalHandle); detailCullingBlacklist.clear(); - for (var entry : modelOverrides.entrySet()) + for (var entry : modelOverrides) { + final ModelOverride override = entry.getValue(); if (entry.getValue().disableDetailCulling) detailCullingBlacklist.add(entry.getKey()); + override.clearIds(); + } log.debug("Loaded {} model overrides", modelOverrides.size()); @@ -119,21 +123,21 @@ public void reload() { startUp(); } - private void addOverride(@Nullable ModelOverride override) { + private void addOverride(@Nullable ModelOverride override, GamevalManager.Handle gamevalHandle) { if (override == null || override.seasonalTheme != null && override.seasonalTheme != plugin.configSeasonalTheme) return; for (int id : override.npcIds) - addEntry(ModelHash.TYPE_NPC, id, override); + addEntry(ModelHash.TYPE_NPC, id, override, gamevalHandle); for (int id : override.objectIds) - addEntry(ModelHash.TYPE_OBJECT, id, override); + addEntry(ModelHash.TYPE_OBJECT, id, override, gamevalHandle); for (int id : override.projectileIds) - addEntry(ModelHash.TYPE_PROJECTILE, id, override); + addEntry(ModelHash.TYPE_PROJECTILE, id, override, gamevalHandle); for (int id : override.graphicsObjectIds) - addEntry(ModelHash.TYPE_GRAPHICS_OBJECT, id, override); + addEntry(ModelHash.TYPE_GRAPHICS_OBJECT, id, override, gamevalHandle); } - private void addEntry(int type, int id, ModelOverride entry) { + private void addEntry(int type, int id, ModelOverride entry, GamevalManager.Handle gamevalHandle) { int uuid = ModelHash.packUuid(type, id); ModelOverride current = modelOverrides.get(uuid); @@ -157,14 +161,14 @@ private void addEntry(int type, int id, ModelOverride entry) { String name = null; switch (type) { case ModelHash.TYPE_NPC: - name = gamevalManager.getNpcName(id); + name = gamevalHandle.getNpcName(id); break; case ModelHash.TYPE_OBJECT: - name = gamevalManager.getObjectName(id); + name = gamevalHandle.getObjectName(id); break; case ModelHash.TYPE_PROJECTILE: case ModelHash.TYPE_GRAPHICS_OBJECT: - name = gamevalManager.getSpotanimName(id); + name = gamevalHandle.getSpotanimName(id); break; } @@ -207,7 +211,7 @@ private void addEntry(int type, int id, ModelOverride entry) { } } - private void addSailingCullingOverrides() { + private void addSailingCullingOverrides(GamevalManager.Handle gamevalHandle) { try { for (Integer row : client.getDBTableRows(DBTableID.SailingBoatSail.ID)) { Integer sailId = (Integer) client.getDBTableField(row, DBTableID.SailingBoatSail.COL_LOC, 0)[0]; @@ -219,7 +223,7 @@ private void addSailingCullingOverrides() { sailOverride.objectIds = Set.of(sailId); sailOverride.disableDetailCulling = true; sailOverride.normalize(plugin); - addOverride(sailOverride); + addOverride(sailOverride, gamevalHandle); } } catch (Exception ex) { log.error("Error while setting up model overrides for disabling detail culling of sails:", ex); diff --git a/src/main/java/rs117/hd/scene/ProceduralGenerator.java b/src/main/java/rs117/hd/scene/ProceduralGenerator.java index 2a3969d99e..b1167e3ebe 100644 --- a/src/main/java/rs117/hd/scene/ProceduralGenerator.java +++ b/src/main/java/rs117/hd/scene/ProceduralGenerator.java @@ -25,7 +25,6 @@ package rs117.hd.scene; import java.util.Arrays; -import java.util.HashMap; import javax.inject.Inject; import javax.inject.Singleton; import lombok.extern.slf4j.Slf4j; @@ -37,14 +36,23 @@ import rs117.hd.scene.tile_overrides.TileOverride; import rs117.hd.scene.water_types.WaterType; import rs117.hd.utils.ColorUtils; -import rs117.hd.utils.buffer.GpuIntBuffer; +import rs117.hd.utils.collections.ConcurrentPool; +import rs117.hd.utils.collections.Int2IntHashMap; +import rs117.hd.utils.collections.Int2ObjectHashMap; +import rs117.hd.utils.collections.PooledArrayType; import static net.runelite.api.Constants.*; import static net.runelite.api.Perspective.*; +import static rs117.hd.scene.SceneContext.TILE_OVERRIDE_COUNT; +import static rs117.hd.scene.SceneContext.TILE_OVERRIDE_MAIN; +import static rs117.hd.scene.SceneContext.TILE_OVERRIDE_OVERLAY; +import static rs117.hd.scene.SceneContext.TILE_OVERRIDE_UNDERLAY; +import static rs117.hd.scene.SceneContext.TILE_SKIP_FLAG; +import static rs117.hd.scene.SceneContext.TILE_WATER_FLAG; import static rs117.hd.scene.tile_overrides.TileOverride.OVERLAY_FLAG; import static rs117.hd.utils.HDUtils.HIDDEN_HSL; import static rs117.hd.utils.HDUtils.calculateSurfaceNormals; -import static rs117.hd.utils.HDUtils.fastVertexHash; +import static rs117.hd.utils.HDUtils.fastVertex3Hash; import static rs117.hd.utils.MathUtils.*; @Slf4j @@ -76,729 +84,254 @@ public class ProceduralGenerator { @Inject private WaterTypeManager waterTypeManager; - public void generateSceneData(SceneContext sceneContext) - { - long timerTotal = System.currentTimeMillis(); - long timerCalculateTerrainNormals, timerGenerateTerrainData, timerGenerateUnderwaterTerrain; - - long startTime = System.currentTimeMillis(); - generateUnderwaterTerrain(sceneContext); - timerGenerateUnderwaterTerrain = (int)(System.currentTimeMillis() - startTime); - startTime = System.currentTimeMillis(); - calculateTerrainNormals(sceneContext); - timerCalculateTerrainNormals = (int)(System.currentTimeMillis() - startTime); - startTime = System.currentTimeMillis(); - generateTerrainData(sceneContext); - timerGenerateTerrainData = (int)(System.currentTimeMillis() - startTime); - - log.debug("procedural data generation took {}ms to complete", (System.currentTimeMillis() - timerTotal)); - log.debug("-- calculateTerrainNormals: {}ms", timerCalculateTerrainNormals); - log.debug("-- generateTerrainData: {}ms", timerGenerateTerrainData); - log.debug("-- generateUnderwaterTerrain: {}ms", timerGenerateUnderwaterTerrain); - } + private final ConcurrentPool GENERATOR_POOL = new ConcurrentPool<>(GeneratorContext::new); - public void clearSceneData(SceneContext sceneContext) { - sceneContext.tileIsWater = null; - sceneContext.vertexIsWater = null; - sceneContext.vertexIsLand = null; - sceneContext.vertexIsOverlay = null; - sceneContext.vertexIsUnderlay = null; - sceneContext.skipTile = null; - sceneContext.vertexUnderwaterDepth = null; - if (!(sceneContext instanceof LegacySceneContext)) - sceneContext.underwaterDepthLevels = null; - } + final class GeneratorContext implements AutoCloseable { + final MainTileOverridesGenerator mainTileOverridesGenerator = new MainTileOverridesGenerator(); + final TerrainDataGenerator terrainDataGenerator = new TerrainDataGenerator(); + final UnderwaterTerrainGenerator underwaterTerrainGenerator = new UnderwaterTerrainGenerator(); + final TerrainNormalGenerator terrainNormalGenerator = new TerrainNormalGenerator(); - /** - * Iterates through all Tiles in a given Scene, producing color and - * material data for each vertex of each Tile. Then adds the resulting - * data to appropriate HashMaps. - */ - private void generateTerrainData(SceneContext sceneContext) - { - sceneContext.vertexTerrainColor = new HashMap<>(); - // 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<>(); - // for faces without an overlay is set to true - sceneContext.vertexIsUnderlay = new HashMap<>(); - // 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<>(); - - Tile[][][] tiles = sceneContext.scene.getExtendedTiles(); - int sizeX = sceneContext.sizeX; - int sizeY = sceneContext.sizeZ; - for (int z = 0; z < MAX_Z; ++z) { - for (int x = 0; x < sizeX; ++x) - for (int y = 0; y < sizeY; ++y) - if (tiles[z][x][y] != null) - generateDataForTile(sceneContext, tiles[z][x][y], x, y); - - for (int x = 0; x < sizeX; ++x) - for (int y = 0; y < sizeY; ++y) - if (tiles[z][x][y] != null && tiles[z][x][y].getBridge() != null) - generateDataForTile(sceneContext, tiles[z][x][y].getBridge(), x, y); + @Override + public void close() { + GENERATOR_POOL.recycle(this); } } /** - * Produces color and material data for the vertices of the provided Tile. - * Then adds the resulting data to appropriate HashMaps. + * Gets the vertex keys of a Tile Paint tile for use in retrieving data from hashmaps. + * Writes the vertex keys in following order: SW, SE, NW, NE * - * @param sceneContext that the tile is associated with - * @param tile to generate terrain data for + * @param ctx that the tile is from + * @param tile to get the vertex keys of */ - private void generateDataForTile(SceneContext sceneContext, Tile tile, int tileExX, int tileExY) - { - int faceCount; - if (tile.getSceneTilePaint() != null) { - faceCount = 2; - } else if (tile.getSceneTileModel() != null) { - faceCount = tile.getSceneTileModel().getFaceX().length; - } else { - return; - } - - int[] vertexHashes = new int[faceCount * VERTICES_PER_FACE]; - int[] vertexColors = new int[faceCount * VERTICES_PER_FACE]; - TileOverride[] vertexOverrides = new TileOverride[faceCount * VERTICES_PER_FACE]; - boolean[] vertexIsOverlay = new boolean[faceCount * VERTICES_PER_FACE]; - boolean[] vertexDefaultColor = new boolean[faceCount * VERTICES_PER_FACE]; - - int tileX = tileExX - sceneContext.sceneOffset; - int tileY = tileExY - sceneContext.sceneOffset; - int tileZ = tile.getRenderLevel(); - int[] worldPos = sceneContext.sceneToWorld(tileX, tileY, tileZ); - - Scene scene = sceneContext.scene; - if (tile.getSceneTilePaint() != null) { - // tile paint - - var override = tileOverrideManager.getOverride(sceneContext, tile, worldPos); - if (override.waterType != WaterType.NONE) { - // skip water tiles - return; - } - - int swColor = tile.getSceneTilePaint().getSwColor(); - int seColor = tile.getSceneTilePaint().getSeColor(); - int nwColor = tile.getSceneTilePaint().getNwColor(); - int neColor = tile.getSceneTilePaint().getNeColor(); - - vertexHashes = tileVertexKeys(sceneContext, tile); - - if (tileExX >= EXTENDED_SCENE_SIZE - 2 && tileExY >= EXTENDED_SCENE_SIZE - 2) { - // reduce the black scene edges by assigning surrounding colors - neColor = swColor; - nwColor = swColor; - seColor = swColor; - } else if (tileExY >= EXTENDED_SCENE_SIZE - 2) { - nwColor = swColor; - neColor = seColor; - } else if (tileExX >= EXTENDED_SCENE_SIZE - 2) { - neColor = nwColor; - seColor = swColor; - } - - vertexColors[0] = swColor; - vertexColors[1] = seColor; - vertexColors[2] = nwColor; - vertexColors[3] = neColor; - - for (int i = 0; i < 4; i++) { - vertexOverrides[i] = override; - vertexIsOverlay[i] = override.queriedAsOverlay; - } - if (useDefaultColor(tile, override)) - for (int i = 0; i < 4; i++) - vertexDefaultColor[i] = true; - } - else if (tile.getSceneTileModel() != null) - { - // tile model - - SceneTileModel sceneTileModel = tile.getSceneTileModel(); - - final int[] faceColorsA = sceneTileModel.getTriangleColorA(); - final int[] faceColorsB = sceneTileModel.getTriangleColorB(); - final int[] faceColorsC = sceneTileModel.getTriangleColorC(); - - int overlayId = OVERLAY_FLAG | scene.getOverlayIds()[tileZ][tileExX][tileExY]; - int underlayId = scene.getUnderlayIds()[tileZ][tileExX][tileExY]; - var overlayOverride = tileOverrideManager.getOverride(sceneContext, tile, worldPos, overlayId); - var underlayOverride = tileOverrideManager.getOverride(sceneContext, tile, worldPos, underlayId); - - for (int face = 0; face < faceCount; face++) { - int[] faceColors = new int[]{faceColorsA[face], faceColorsB[face], faceColorsC[face]}; - int[] vertexKeys = faceVertexKeys(tile, face); - - for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) { - boolean isOverlay = isOverlayFace(tile, face); - var override = isOverlay ? overlayOverride : underlayOverride; - if (override.waterType != WaterType.NONE) - continue; // skip water faces - - vertexHashes[face * VERTICES_PER_FACE + vertex] = vertexKeys[vertex]; - - int color = faceColors[vertex]; - vertexColors[face * VERTICES_PER_FACE + vertex] = color; - - vertexOverrides[face * VERTICES_PER_FACE + vertex] = override; - vertexIsOverlay[face * VERTICES_PER_FACE + vertex] = isOverlay; - - if (isOverlay && useDefaultColor(tile, override)) - vertexDefaultColor[face * VERTICES_PER_FACE + vertex] = true; - } - } - } - - for (int vertex = 0; vertex < vertexHashes.length; vertex++) - { - if (vertexHashes[vertex] == 0) - continue; - - int color = vertexColors[vertex]; - var override = vertexOverrides[vertex]; - if (color < 0 || color == HIDDEN_HSL && !override.forced) - continue; - - // if this vertex already has a 'high priority' color assigned, - // skip assigning a 'low priority' color unless there is no color assigned. - // Near-solid-black tiles that are used in some places under wall objects - boolean lowPriorityColor = vertexColors[vertex] <= 2; - - float lightenMultiplier = 1.5f; - int lightenBase = 15; - int lightenAdd = 3; - float darkenMultiplier = 0.5f; - - int[] vNormals = sceneContext.vertexTerrainNormals.getOrDefault(vertexHashes[vertex], new int[] { 0, 0, 0 }); - - float dot = dot(vNormals); - if (dot < EPSILON) { - dot = 0; - } else { - // Approximately reverse vanilla tile lighting - dot = (vNormals[0] + vNormals[1]) / sqrt(2 * dot); - } - int lightness = color & 0x7F; - lightness = (int) mix(lightness, (int) (max(lightness - lightenAdd, 0) * lightenMultiplier) + lightenBase, max(dot, 0)); - lightness = (int) (1.25f * mix(lightness, (int) (lightness * darkenMultiplier), -min(dot, 0))); - final int maxBrightness = 55; // reduces overexposure - lightness = min(lightness, maxBrightness); - color = color & ~0x7F | lightness; - - Material material = override.groundMaterial.getRandomMaterial(worldPos); - boolean isOverlay = vertexIsOverlay[vertex] != override.blendedAsOpposite; - color = override.modifyColor(color); - - vertexColors[vertex] = color; - - // mark the vertex as either an overlay or underlay. - // this is used to determine how to blend between vertex colors - if (isOverlay) - { - sceneContext.vertexIsOverlay.put(vertexHashes[vertex], true); - } - else - { - sceneContext.vertexIsUnderlay.put(vertexHashes[vertex], true); - } - - // add color and texture to hashmap - if ((!lowPriorityColor || !sceneContext.highPriorityColor.containsKey(vertexHashes[vertex])) && !vertexDefaultColor[vertex]) - { - boolean shouldWrite = isOverlay || !sceneContext.vertexTerrainColor.containsKey(vertexHashes[vertex]); - if (shouldWrite || !sceneContext.vertexTerrainColor.containsKey(vertexHashes[vertex])) - sceneContext.vertexTerrainColor.put(vertexHashes[vertex], vertexColors[vertex]); - - if (shouldWrite || !sceneContext.vertexTerrainTexture.containsKey(vertexHashes[vertex])) - sceneContext.vertexTerrainTexture.put(vertexHashes[vertex], material); + public static void tileVertexKeys(SceneContext ctx, Tile tile, int[][] tileVertices, int[] vertexHashes) { + tileVertices(ctx, tile, tileVertices); - if (!lowPriorityColor) - sceneContext.highPriorityColor.put(vertexHashes[vertex], true); - } - } + vertexHashes[0] = fastVertex3Hash(tileVertices[0]); + vertexHashes[1] = fastVertex3Hash(tileVertices[1]); + vertexHashes[2] = fastVertex3Hash(tileVertices[2]); + vertexHashes[3] = fastVertex3Hash(tileVertices[3]); } - /** - * Generates underwater terrain data by iterating through all Tiles in a given - * 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) - { - 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<>(); - // 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<>(); - // 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<>(); - // 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 - sceneContext.underwaterDepthLevels = new int[MAX_Z][sizeX + 1][sizeY + 1]; - // the world-space height offsets of each vertex on the tile grid - // these offsets are interpolated to calculate offsets for vertices not on the grid (tilemodels) - final int[][][] underwaterDepths = new int[MAX_Z][sizeX + 1][sizeY + 1]; - - for (int z = 0; z < MAX_Z; ++z) - { - for (int x = 0; x < sizeX; ++x) { - // set the array to 1 initially - // this assumes that all vertices are water; - // we will set non-water vertices to 0 in the next loop - Arrays.fill(sceneContext.underwaterDepthLevels[z][x], 1); - } - } - - Scene scene = sceneContext.scene; - Tile[][][] tiles = scene.getExtendedTiles(); - - // figure out which vertices are water and assign some data - for (int z = 0; z < MAX_Z; ++z) { - for (int x = 0; x < sizeX; ++x) { - for (int y = 0; y < sizeY; ++y) { - if (tiles[z][x][y] == null) { - sceneContext.underwaterDepthLevels[z][x][y] = 0; - sceneContext.underwaterDepthLevels[z][x + 1][y] = 0; - sceneContext.underwaterDepthLevels[z][x][y + 1] = 0; - sceneContext.underwaterDepthLevels[z][x + 1][y + 1] = 0; - continue; - } - - Tile tile = tiles[z][x][y]; - if (tile.getBridge() != null) { - tile = tile.getBridge(); - } - - if (tile.getSceneTilePaint() != null) { - int[] vertexKeys = tileVertexKeys(sceneContext, tile); - - 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.underwaterDepthLevels[z][x][y] = 0; - sceneContext.underwaterDepthLevels[z][x + 1][y] = 0; - sceneContext.underwaterDepthLevels[z][x][y + 1] = 0; - sceneContext.underwaterDepthLevels[z][x + 1][y + 1] = 0; - } else { - // Stop tiles on the same X,Y coordinates on different planes from - // each generating water. Prevents undesirable results in certain places. - if (z > 0) { - boolean continueLoop = false; - - for (int checkZ = 0; checkZ < z; ++checkZ) { - if (sceneContext.tileIsWater[checkZ][x][y]) { - sceneContext.underwaterDepthLevels[z][x][y] = 0; - sceneContext.underwaterDepthLevels[z][x + 1][y] = 0; - sceneContext.underwaterDepthLevels[z][x][y + 1] = 0; - sceneContext.underwaterDepthLevels[z][x + 1][y + 1] = 0; - - sceneContext.skipTile[z][x][y] = true; - - continueLoop = true; - - break; - } - } - - if (continueLoop) - continue; - } - - sceneContext.tileIsWater[z][x][y] = true; - - for (int vertexKey : vertexKeys) - { - sceneContext.vertexIsWater.put(vertexKey, true); - } - } - } - else if (tile.getSceneTileModel() != null) - { - SceneTileModel model = tile.getSceneTileModel(); - - int faceCount = model.getFaceX().length; - - int tileZ = tile.getRenderLevel(); - int[] worldPos = sceneContext.extendedSceneToWorld(x, y, tileZ); - int overlayId = OVERLAY_FLAG | scene.getOverlayIds()[tileZ][x][y]; - int underlayId = scene.getUnderlayIds()[tileZ][x][y]; - var overlayOverride = tileOverrideManager.getOverride(sceneContext, tile, worldPos, overlayId); - var underlayOverride = tileOverrideManager.getOverride(sceneContext, tile, worldPos, underlayId); - - // Stop tiles on the same X,Y coordinates on different planes from - // each generating water. Prevents undesirable results in certain places. - if (z > 0) - { - boolean tileIncludesWater = false; - for (int face = 0; face < faceCount; face++) - { - var override = ProceduralGenerator.isOverlayFace(tile, face) ? overlayOverride : underlayOverride; - int textureId = model.getTriangleTextureId() == null ? -1 : - model.getTriangleTextureId()[face]; - if (seasonalWaterType(override, textureId) != WaterType.NONE) - { - tileIncludesWater = true; - break; - } - } - - if (tileIncludesWater) - { - boolean continueLoop = false; - - for (int checkZ = 0; checkZ < z; ++checkZ) - { - if (sceneContext.tileIsWater[checkZ][x][y]) - { - sceneContext.underwaterDepthLevels[z][x][y] = 0; - sceneContext.underwaterDepthLevels[z][x + 1][y] = 0; - sceneContext.underwaterDepthLevels[z][x][y + 1] = 0; - sceneContext.underwaterDepthLevels[z][x + 1][y + 1] = 0; - - sceneContext.skipTile[z][x][y] = true; - - continueLoop = true; - - break; - } - } - - if (continueLoop) - continue; - } - } + public void clearSceneData(SceneContext sceneContext) { + sceneContext.tileFlags = null; + sceneContext.vertexTerrainData = null; + sceneContext.tileOverrideIndices = null; + sceneContext.vertexTerrainTexture = null; + sceneContext.vertexTerrainNormals = null; + } - for (int face = 0; face < faceCount; face++) - { - int[][] vertices = faceVertices(tile, face); - int[] vertexKeys = faceVertexKeys(tile, face); - - var override = ProceduralGenerator.isOverlayFace(tile, face) ? overlayOverride : underlayOverride; - int textureId = model.getTriangleTextureId() == null ? -1 : - model.getTriangleTextureId()[face]; - if (seasonalWaterType(override, textureId) == WaterType.NONE) - { - for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) - { - if (model.getTriangleColorA()[face] != HIDDEN_HSL || override.forced) - sceneContext.vertexIsLand.put(vertexKeys[vertex], true); + public static void faceVertexKeys(Tile tile, int face, int[][] vertices, int[] vertexHashes) { + faceVertices(tile, face, vertices); - if (vertices[vertex][0] % LOCAL_TILE_SIZE == 0 && - vertices[vertex][1] % LOCAL_TILE_SIZE == 0 - ) { - int vX = (vertices[vertex][0] >> LOCAL_COORD_BITS) + sceneContext.sceneOffset; - int vY = (vertices[vertex][1] >> LOCAL_COORD_BITS) + sceneContext.sceneOffset; + vertexHashes[0] = fastVertex3Hash(vertices[0]); + vertexHashes[1] = fastVertex3Hash(vertices[1]); + vertexHashes[2] = fastVertex3Hash(vertices[2]); + } - sceneContext.underwaterDepthLevels[z][vX][vY] = 0; - } - } - } - else - { - sceneContext.tileIsWater[z][x][y] = true; + public static int[] faceVertexKeys(Tile tile, int face) { + int[][] vertices = new int[3][3]; + int[] vertexHashes = new int[3]; + faceVertexKeys(tile, face, vertices, vertexHashes); + return vertexHashes; + } - for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) - { - sceneContext.vertexIsWater.put(vertexKeys[vertex], true); - } - } - } - } - else - { - sceneContext.underwaterDepthLevels[z][x][y] = 0; - sceneContext.underwaterDepthLevels[z][x + 1][y] = 0; - sceneContext.underwaterDepthLevels[z][x][y + 1] = 0; - sceneContext.underwaterDepthLevels[z][x + 1][y + 1] = 0; - } - } - } + public void generateSceneData(SceneContext sceneCtx, SceneContext prevSceneCtx) { + try (GeneratorContext ctx = GENERATOR_POOL.acquire()) { + long timerTotal = System.currentTimeMillis(); + long timerCalculateMainOverrides, timerCalculateTerrainNormals, timerGenerateTerrainData, timerGenerateUnderwaterTerrain; + + long startTime = System.currentTimeMillis(); + ctx.mainTileOverridesGenerator.generate(sceneCtx, prevSceneCtx); + timerCalculateMainOverrides = (int) (System.currentTimeMillis() - startTime); + startTime = System.currentTimeMillis(); + ctx.underwaterTerrainGenerator.generate(sceneCtx, prevSceneCtx); + timerGenerateUnderwaterTerrain = (int) (System.currentTimeMillis() - startTime); + startTime = System.currentTimeMillis(); + ctx.terrainNormalGenerator.generate(sceneCtx, prevSceneCtx); + timerCalculateTerrainNormals = (int) (System.currentTimeMillis() - startTime); + startTime = System.currentTimeMillis(); + ctx.terrainDataGenerator.generate(sceneCtx, prevSceneCtx); + timerGenerateTerrainData = (int) (System.currentTimeMillis() - startTime); + + log.debug("procedural data generation took {}ms to complete", (System.currentTimeMillis() - timerTotal)); + log.debug("-- calculateMainTileOverrides: {}ms", timerCalculateMainOverrides); + log.debug("-- calculateTerrainNormals: {}ms", timerCalculateTerrainNormals); + log.debug("-- generateTerrainData: {}ms", timerGenerateTerrainData); + log.debug("-- generateUnderwaterTerrain: {}ms", timerGenerateUnderwaterTerrain); } + } - // Sink terrain further from shore by desired levels. - for (int level = 0; level < DEPTH_LEVEL_SLOPE.length - 1; level++) - { - for (int z = 0; z < MAX_Z; ++z) - { - for (int x = 0; x < sceneContext.underwaterDepthLevels[z].length; x++) - { - for (int y = 0; y < sceneContext.underwaterDepthLevels[z][x].length; y++) - { - if (sceneContext.underwaterDepthLevels[z][x][y] == 0) - { - // Skip the tile if it isn't water. - continue; - } - // If it's on the edge of the scene, reset the depth so - // it creates a 'wall' to prevent fog from passing through. - // Not incredibly effective, but better than nothing. - if (x == 0 || y == 0 || x == EXTENDED_SCENE_SIZE || y == EXTENDED_SCENE_SIZE) { - sceneContext.underwaterDepthLevels[z][x][y] = 0; + static class TerrainNormalGenerator { + private final int[][] vertices = new int[4][3]; + private final int[] hashes = new int[4]; + + private final int[] vertexHeights = new int[4]; + private final int[] surfaceNormal = new int[3]; + private final int[] normalA = new int[3]; + private final int[] normalB = new int[3]; + private final int[] normalC = new int[3]; + + private int[][][] faceVertices = new int[2][VERTICES_PER_FACE][3]; + private int[][] faceVertexKeys = new int[VERTICES_PER_FACE][3]; + private int[] vertexNormals; + private int vertexNormalsPos = 0; + + /** + * Iterates through all Tiles in a given Scene, calculating vertex normals + * for each one, then stores resulting normal data in a HashMap. + */ + private void generate(SceneContext sceneContext, SceneContext prevSceneContext) { + final Tile[][][] tiles = sceneContext.scene.getExtendedTiles(); + + sceneContext.vertexTerrainNormalIndices = new Int2IntHashMap(prevSceneContext != null && prevSceneContext.vertexTerrainNormalIndices != null ? prevSceneContext.vertexTerrainNormalIndices.capacity() : 0); + + vertexNormals = PooledArrayType.INT.borrow(prevSceneContext != null && prevSceneContext.vertexTerrainNormals != null ? prevSceneContext.vertexTerrainNormals.length : 3000); + vertexNormalsPos = 0; + + for (int z = 0; z < MAX_Z; z++) { + final Tile[][] zTiles = tiles[z]; + for (int x = 0; x < sceneContext.sizeX; x++) { + final Tile[] xTiles = zTiles[x]; + for (int y = 0; y < sceneContext.sizeZ; y++) { + final Tile tile = xTiles[y]; + if (tile == null) continue; - } - int tileHeight = sceneContext.underwaterDepthLevels[z][x][y]; - if (sceneContext.underwaterDepthLevels[z][x - 1][y] < tileHeight) - { - // West - continue; - } - if (x < sceneContext.underwaterDepthLevels[z].length - 1 && sceneContext.underwaterDepthLevels[z][x + 1][y] < tileHeight) - { - // East - continue; - } - if (sceneContext.underwaterDepthLevels[z][x][y - 1] < tileHeight) - { - // South - continue; - } - if (y < sceneContext.underwaterDepthLevels[z].length - 1 && sceneContext.underwaterDepthLevels[z][x][y + 1] < tileHeight) - { - // North - continue; - } - // At this point, it's surrounded only by other depth-adjusted vertices. - sceneContext.underwaterDepthLevels[z][x][y]++; - } - } - } - } - - // Adjust the height levels to world coordinate offsets and add to an array. - for (int z = 0; z < MAX_Z; ++z) - { - for (int x = 0; x < sceneContext.underwaterDepthLevels[z].length; x++) - { - for (int y = 0; y < sceneContext.underwaterDepthLevels[z][x].length; y++) - { - if (sceneContext.underwaterDepthLevels[z][x][y] == 0) - { - continue; + final boolean isBridge = tile.getBridge() != null; + if (isBridge) + calculateNormalsForTile(sceneContext, tile.getBridge(), false); + calculateNormalsForTile(sceneContext, tile, isBridge); } - int depth = DEPTH_LEVEL_SLOPE[sceneContext.underwaterDepthLevels[z][x][y] - 1]; - int heightOffset = (int) (depth * .55f); // legacy weirdness - underwaterDepths[z][x][y] = heightOffset; } } - } - - // Store the height offsets in a hashmap and calculate interpolated - // height offsets for non-corner vertices. - for (int z = 0; z < MAX_Z; ++z) { - for (int x = 0; x < sizeX; ++x) { - for (int y = 0; y < sizeY; ++y) { - if (!sceneContext.tileIsWater[z][x][y]) { - continue; - } - Tile tile = tiles[z][x][y]; - if (tile == null) { - continue; - } + sceneContext.vertexTerrainNormals = new short[vertexNormalsPos]; + for(int offset = 0; offset < vertexNormalsPos; offset += 3) { + final float x = vertexNormals[offset]; + final float y = vertexNormals[offset + 1]; + final float z = vertexNormals[offset + 2]; - if (tile.getBridge() != null) { - tile = tile.getBridge(); - } - if (tile.getSceneTilePaint() != null) { - int[] vertexKeys = tileVertexKeys(sceneContext, tile); - - int swVertexKey = vertexKeys[0]; - int seVertexKey = vertexKeys[1]; - int nwVertexKey = vertexKeys[2]; - int neVertexKey = vertexKeys[3]; - - sceneContext.vertexUnderwaterDepth.put(swVertexKey, underwaterDepths[z][x][y]); - sceneContext.vertexUnderwaterDepth.put(seVertexKey, underwaterDepths[z][x + 1][y]); - sceneContext.vertexUnderwaterDepth.put(nwVertexKey, underwaterDepths[z][x][y + 1]); - sceneContext.vertexUnderwaterDepth.put(neVertexKey, underwaterDepths[z][x + 1][y + 1]); - } - else if (tile.getSceneTileModel() != null) - { - SceneTileModel sceneTileModel = tile.getSceneTileModel(); + final float len = x * x + y * y + z * z; + if (len == 0) + continue; - int faceCount = sceneTileModel.getFaceX().length; - - for (int face = 0; face < faceCount; face++) - { - int[][] vertices = faceVertices(tile, face); - int[] vertexKeys = faceVertexKeys(tile, face); - - for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) - { - if (vertices[vertex][0] % LOCAL_TILE_SIZE == 0 && - vertices[vertex][1] % LOCAL_TILE_SIZE == 0 - ) { - // The vertex is at the corner of the tile; - // simply use the offset in the tile grid array. + final float invLen = rcp(sqrt(len)); + sceneContext.vertexTerrainNormals[offset] = normShort(x * invLen); + sceneContext.vertexTerrainNormals[offset + 1] = normShort(y * invLen); + sceneContext.vertexTerrainNormals[offset + 2] = normShort(z * invLen); + } - int vX = (vertices[vertex][0] >> LOCAL_COORD_BITS) + sceneContext.sceneOffset; - int vY = (vertices[vertex][1] >> LOCAL_COORD_BITS) + sceneContext.sceneOffset; + PooledArrayType.INT.release(vertexNormals); + vertexNormals = null; + } - sceneContext.vertexUnderwaterDepth.put(vertexKeys[vertex], underwaterDepths[z][vX][vY]); - } - else - { - // If the tile is a tile model and this vertex is shared only by faces that are water, - // interpolate between the height offsets at each corner to get the height offset - // of the vertex. - - float lerpX = fract(vertices[vertex][0] / (float) LOCAL_TILE_SIZE); - float lerpY = fract(vertices[vertex][1] / (float) LOCAL_TILE_SIZE); - float northHeightOffset = mix(underwaterDepths[z][x][y + 1], underwaterDepths[z][x + 1][y + 1], lerpX); - 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])) - sceneContext.vertexUnderwaterDepth.put(vertexKeys[vertex], heightOffset); - } - } - } - } + /** + * Calculates vertex normals for a given Tile, + * then stores resulting normal data in a HashMap. + * + * @param sceneContext that the tile is associated with + * @param tile to calculate normals for + * @param isBridge whether the tile is a bridge tile, i.e. tile above + */ + private void calculateNormalsForTile(SceneContext sceneContext, Tile tile, boolean isBridge) { + int faceCount = 2; + if (tile.getSceneTileModel() != null) { + // Tile model + SceneTileModel tileModel = tile.getSceneTileModel(); + faceCount = tileModel.getFaceX().length; + if (faceVertices.length < faceCount) { + faceVertices = new int[faceCount][VERTICES_PER_FACE][3]; + faceVertexKeys = new int[faceCount][VERTICES_PER_FACE]; } - } - } - } - /** - * 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) - { - sceneContext.vertexTerrainNormals = new HashMap<>(); + for (int face = 0; face < faceCount; face++) { + faceVertexKeys(tile, face, vertices, hashes); - for (Tile[][] plane : sceneContext.scene.getExtendedTiles()) { - for (Tile[] column : plane) { - for (Tile tile : column) { - if (tile != null) { - boolean isBridge = false; + ivec3(faceVertices[face][0], vertices[0][0], vertices[0][1], vertices[0][2]); + ivec3(faceVertices[face][2], vertices[1][0], vertices[1][1], vertices[1][2]); + ivec3(faceVertices[face][1], vertices[2][0], vertices[2][1], vertices[2][2]); - if (tile.getBridge() != null) { - calculateNormalsForTile(sceneContext, tile.getBridge(), false); - isBridge = true; - } - calculateNormalsForTile(sceneContext, tile, isBridge); - } + ivec3(faceVertexKeys[face], hashes[0], hashes[1], hashes[2]); } - } - } + } else { + tileVertexKeys(sceneContext, tile, vertices, hashes); - sceneContext.vertexTerrainNormals.forEach((key, normal) -> { - var n = normalize(vec(normal)); - for (int i = 0; i < 3; i++) - normal[i] = GpuIntBuffer.normShort(n[i]); - }); - } + ivec3(faceVertices[0][0], vertices[3][0], vertices[3][1], vertices[3][2]); + ivec3(faceVertices[0][1], vertices[1][0], vertices[1][1], vertices[1][2]); + ivec3(faceVertices[0][2], vertices[2][0], vertices[2][1], vertices[2][2]); - /** - * Calculates vertex normals for a given Tile, - * then stores resulting normal data in a HashMap. - * - * @param sceneContext that the tile is associated with - * @param tile to calculate normals for - * @param isBridge whether the tile is a bridge tile, i.e. tile above - */ - private void calculateNormalsForTile(SceneContext sceneContext, Tile tile, boolean isBridge) - { - // Make array of tile's tris with vertices - int[][][] faceVertices; // Array of tile's tri vertices - int[][] faceVertexKeys; + ivec3(faceVertices[1][0], vertices[0][0], vertices[0][1], vertices[0][2]); + ivec3(faceVertices[1][1], vertices[2][0], vertices[2][1], vertices[2][2]); + ivec3(faceVertices[1][2], vertices[1][0], vertices[1][1], vertices[1][2]); - if (tile.getSceneTileModel() != null) - { - // Tile model - SceneTileModel tileModel = tile.getSceneTileModel(); - faceVertices = new int[tileModel.getFaceX().length][VERTICES_PER_FACE][3]; - faceVertexKeys = new int[tileModel.getFaceX().length][VERTICES_PER_FACE]; - - for (int face = 0; face < tileModel.getFaceX().length; face++) - { - int[][] vertices = faceVertices(tile, face); - - 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]; + ivec3(faceVertexKeys[0], hashes[3], hashes[1], hashes[2]); + ivec3(faceVertexKeys[1], hashes[0], hashes[2], hashes[1]); } - } - else - { - faceVertices = new int[2][VERTICES_PER_FACE][3]; - faceVertexKeys = new int[VERTICES_PER_FACE][3]; - int[][] vertices = tileVertices(sceneContext, tile); - faceVertices[0] = new int[][]{vertices[3], vertices[1], vertices[2]}; - faceVertices[1] = new int[][]{vertices[0], vertices[2], vertices[1]}; - - int[] vertexKeys = tileVertexKeys(sceneContext, tile); - faceVertexKeys[0] = new int[]{vertexKeys[3], vertexKeys[1], vertexKeys[2]}; - faceVertexKeys[1] = new int[]{vertexKeys[0], vertexKeys[2], vertexKeys[1]}; - } - // Loop through tris to calculate and accumulate normals - for (int face = 0; face < faceVertices.length; face++) - { - // XYZ - int[] vertexHeights = new int[]{faceVertices[face][0][2], faceVertices[face][1][2], faceVertices[face][2][2]}; - if (!isBridge) - { - vertexHeights[0] += sceneContext.vertexUnderwaterDepth.getOrDefault(faceVertexKeys[face][0], 0); - vertexHeights[1] += sceneContext.vertexUnderwaterDepth.getOrDefault(faceVertexKeys[face][1], 0); - vertexHeights[2] += sceneContext.vertexUnderwaterDepth.getOrDefault(faceVertexKeys[face][2], 0); - } + // Loop through tris to calculate and accumulate normals + for (int face = 0; face < faceCount; face++) { + // XYZ + ivec3(vertexHeights, faceVertices[face][0][2], faceVertices[face][1][2], faceVertices[face][2][2]); + if (!isBridge) { + vertexHeights[0] += sceneContext.getVertexUnderwaterDepth(faceVertexKeys[face][0]); + vertexHeights[1] += sceneContext.getVertexUnderwaterDepth(faceVertexKeys[face][1]); + vertexHeights[2] += sceneContext.getVertexUnderwaterDepth(faceVertexKeys[face][2]); + } - int[] vertexNormals = calculateSurfaceNormals( - ivec( + ivec3( + normalA, faceVertices[face][0][0], faceVertices[face][0][1], vertexHeights[0] - ), - ivec( + ); + + ivec3( + normalB, faceVertices[face][1][0], faceVertices[face][1][1], vertexHeights[1] - ), - ivec( + ); + + ivec3( + normalC, faceVertices[face][2][0], faceVertices[face][2][1], vertexHeights[2] - ) - ); - - 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)); + ); + + calculateSurfaceNormals(surfaceNormal, normalA, normalB, normalC); + + for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) { + final int vertexKey = faceVertexKeys[face][vertex]; + final int terrainNormalIdx = sceneContext.vertexTerrainNormalIndices.getOrDefault(vertexKey, -1); + if (terrainNormalIdx == -1) { + sceneContext.vertexTerrainNormalIndices.put(vertexKey, vertexNormalsPos / 3); + + if(vertexNormalsPos + 3 >= vertexNormals.length) { + int[] newVertexNormals = PooledArrayType.INT.borrow(vertexNormalsPos * 2); + System.arraycopy(vertexNormals, 0, newVertexNormals, 0, vertexNormalsPos); + + PooledArrayType.INT.release(vertexNormals); + vertexNormals = newVertexNormals; + } + + vertexNormals[vertexNormalsPos++] = surfaceNormal[0]; + vertexNormals[vertexNormalsPos++] = surfaceNormal[1]; + vertexNormals[vertexNormalsPos++] = surfaceNormal[2]; + } else { + final int offset = terrainNormalIdx * 3; + vertexNormals[offset] += surfaceNormal[0]; + vertexNormals[offset + 1] += surfaceNormal[1]; + vertexNormals[offset + 2] += surfaceNormal[2]; + } + } } } } - public boolean useDefaultColor(Tile tile, TileOverride override) - { + public boolean useDefaultColor(Tile tile, TileOverride override) { if ((tile.getSceneTilePaint() != null && tile.getSceneTilePaint().getTexture() >= 0) || (tile.getSceneTileModel() != null && tile.getSceneTileModel().getTriangleTextureId() != null)) { @@ -812,8 +345,7 @@ public boolean useDefaultColor(Tile tile, TileOverride override) return !override.blended; } - public WaterType seasonalWaterType(TileOverride override, int textureId) - { + public WaterType seasonalWaterType(TileOverride override, int textureId) { var waterType = override.waterType; // As a fallback, always consider vanilla textured water tiles as water @@ -842,17 +374,13 @@ public WaterType seasonalWaterType(TileOverride override, int textureId) return waterType; } - private static boolean[] getTileOverlayTris(int tileShapeIndex) - { - if (tileShapeIndex >= TILE_OVERLAY_TRIS.length) - { + private static boolean[] getTileOverlayTris(int tileShapeIndex) { + if (tileShapeIndex >= TILE_OVERLAY_TRIS.length) { log.debug("getTileOverlayTris(): unknown tileShapeIndex ({})", tileShapeIndex); return new boolean[10]; // false } - else - { - return TILE_OVERLAY_TRIS[tileShapeIndex]; - } + + return TILE_OVERLAY_TRIS[tileShapeIndex]; } public static boolean isOverlayFace(Tile tile, int face) { @@ -864,32 +392,44 @@ public static boolean isOverlayFace(Tile tile, int face) { } private static void tileVertices(SceneContext ctx, Tile tile, int[][] vertices) { - int tileX = tile.getSceneLocation().getX(); - int tileY = tile.getSceneLocation().getY(); - int tileExX = tileX + ctx.sceneOffset; - int tileExY = tileY + ctx.sceneOffset; - int tileZ = tile.getRenderLevel(); - int[][][] tileHeights = ctx.scene.getTileHeights(); + final Point tileLocation = tile.getSceneLocation(); + final int tileX = tileLocation.getX(); + final int tileY = tileLocation.getY(); + final int tileExX = tileX + ctx.sceneOffset; + final int tileExY = tileY + ctx.sceneOffset; + final int[][] tileHeights = ctx.scene.getTileHeights()[tile.getRenderLevel()]; // swVertex - vertices[0][0] = tileX * LOCAL_TILE_SIZE; - vertices[0][1] = tileY * LOCAL_TILE_SIZE; - vertices[0][2] = tileHeights[tileZ][tileExX][tileExY]; + ivec3( + vertices[0], + tileX * LOCAL_TILE_SIZE, + tileY * LOCAL_TILE_SIZE, + tileHeights[tileExX][tileExY] + ); // seVertex - vertices[1][0] = (tileX + 1) * LOCAL_TILE_SIZE; - vertices[1][1] = tileY * LOCAL_TILE_SIZE; - vertices[1][2] = tileHeights[tileZ][tileExX + 1][tileExY]; + ivec3( + vertices[1], + (tileX + 1) * LOCAL_TILE_SIZE, + tileY * LOCAL_TILE_SIZE, + tileHeights[tileExX + 1][tileExY] + ); // nwVertex - vertices[2][0] = tileX * LOCAL_TILE_SIZE; - vertices[2][1] = (tileY + 1) * LOCAL_TILE_SIZE; - vertices[2][2] = tileHeights[tileZ][tileExX][tileExY + 1]; + ivec3( + vertices[2], + tileX * LOCAL_TILE_SIZE, + (tileY + 1) * LOCAL_TILE_SIZE, + tileHeights[tileExX][tileExY + 1] + ); //neVertex - vertices[3][0] = (tileX + 1) * LOCAL_TILE_SIZE; - vertices[3][1] = (tileY + 1) * LOCAL_TILE_SIZE; - vertices[3][2] = tileHeights[tileZ][tileExX + 1][tileExY + 1]; + ivec3( + vertices[3], + (tileX + 1) * LOCAL_TILE_SIZE, + (tileY + 1) * LOCAL_TILE_SIZE, + tileHeights[tileExX + 1][tileExY + 1] + ); } private static int[][] tileVertices(SceneContext ctx, Tile tile) { @@ -898,38 +438,40 @@ private static int[][] tileVertices(SceneContext ctx, Tile tile) { return vertices; } - private static void faceVertices(Tile tile, int face, int[][] vertices) - { + private static void faceVertices(Tile tile, int face, int[][] vertices) { SceneTileModel sceneTileModel = tile.getSceneTileModel(); - final int[] faceA = sceneTileModel.getFaceX(); - final int[] faceB = sceneTileModel.getFaceY(); - final int[] faceC = sceneTileModel.getFaceZ(); - final int[] vertexX = sceneTileModel.getVertexX(); final int[] vertexY = sceneTileModel.getVertexY(); final int[] vertexZ = sceneTileModel.getVertexZ(); - int vertexFacesA = faceA[face]; - int vertexFacesB = faceB[face]; - int vertexFacesC = faceC[face]; - - // scene X - vertices[0][0] = vertexX[vertexFacesA]; - vertices[1][0] = vertexX[vertexFacesB]; - vertices[2][0] = vertexX[vertexFacesC]; - // scene Y - vertices[0][1] = vertexZ[vertexFacesA]; - vertices[1][1] = vertexZ[vertexFacesB]; - vertices[2][1] = vertexZ[vertexFacesC]; - // scene Z - heights - vertices[0][2] = vertexY[vertexFacesA]; - vertices[1][2] = vertexY[vertexFacesB]; - vertices[2][2] = vertexY[vertexFacesC]; + final int vertexFacesA = sceneTileModel.getFaceX()[face]; + final int vertexFacesB = sceneTileModel.getFaceY()[face]; + final int vertexFacesC = sceneTileModel.getFaceZ()[face]; + + ivec3( + vertices[0], + vertexX[vertexFacesA], + vertexZ[vertexFacesA], + vertexY[vertexFacesA] + ); + + ivec3( + vertices[1], + vertexX[vertexFacesB], + vertexZ[vertexFacesB], + vertexY[vertexFacesB] + ); + + ivec3( + vertices[2], + vertexX[vertexFacesC], + vertexZ[vertexFacesC], + vertexY[vertexFacesC] + ); } - private static int[][] faceVertices(Tile tile, int face) - { + private static int[][] faceVertices(Tile tile, int face) { int[][] vertices = new int[3][3]; faceVertices(tile, face, vertices); return vertices; @@ -963,49 +505,651 @@ public static int[][] faceLocalVertices(Tile tile, int face) { return vertices; } - /** - * Gets the vertex keys of a Tile Paint tile for use in retrieving data from hashmaps. - * Writes the vertex keys in following order: SW, SE, NW, NE - * - * @param ctx that the tile is from - * @param tile to get the vertex keys of - */ - public static void tileVertexKeys(SceneContext ctx, Tile tile, int[][] tileVertices, int[] vertexHashes) - { - tileVertices(ctx, tile, tileVertices); - for (int vertex = 0; vertex < tileVertices.length; ++vertex) - vertexHashes[vertex] = fastVertexHash(tileVertices[vertex]); - } - - public static void tileVertexKeys(SceneContext ctx, Tile tile, int[] vertexHashes) - { + public static void tileVertexKeys(SceneContext ctx, Tile tile, int[] vertexHashes) { int[][] vertices = new int[4][3]; tileVertexKeys(ctx, tile, vertices, vertexHashes); } - public static int[] tileVertexKeys(SceneContext ctx, Tile tile) - { + public static int[] tileVertexKeys(SceneContext ctx, Tile tile) { int[] vertexHashes = new int[4]; tileVertexKeys(ctx, tile, vertexHashes); return vertexHashes; } - public static void faceVertexKeys(Tile tile, int face, int[][] vertices, int[] vertexHashes) - { - faceVertices(tile, face, vertices); - for (int vertex = 0; vertex < vertices.length; ++vertex) - vertexHashes[vertex] = fastVertexHash(vertices[vertex]); + final class MainTileOverridesGenerator { + private final TileOverride[] overrides = new TileOverride[TILE_OVERRIDE_COUNT]; + private final int[] worldPos = new int[3]; + private final int[] ids = new int[2]; + + private void generate(SceneContext sceneContext, SceneContext preSceneCtx) { + final boolean canReuseScene = preSceneCtx != null && sceneContext.scene.isInstance() == preSceneCtx.scene.isInstance() && sceneContext.currentArea == preSceneCtx.currentArea; + final Tile[][][] prevTiles = canReuseScene ? preSceneCtx.scene.getExtendedTiles() : null; + + final Tile[][][] tiles = sceneContext.scene.getExtendedTiles(); + final short[][][] overlayIds = sceneContext.scene.getOverlayIds(); + final short[][][] underlayIds = sceneContext.scene.getUnderlayIds(); + + final int dX = canReuseScene ? (sceneContext.scene.getBaseX() - preSceneCtx.scene.getBaseX() >> 3) << 3 : 0; + final int dY = canReuseScene ? (sceneContext.scene.getBaseY() - preSceneCtx.scene.getBaseY() >> 3) << 3 : 0; + + sceneContext.tileOverrideIndices = new char[MAX_Z * sceneContext.sizeX * sceneContext.sizeZ * 3]; + + for (int z = 0; z < MAX_Z; ++z) { + final Tile[][] prevZTiles = canReuseScene ? prevTiles[z] : null; + final Tile[][] zTiles = tiles[z]; + for (int x = 0; x < sceneContext.sizeX; ++x) { + final Tile[] prevXTiles = canReuseScene ? prevZTiles[z] : null; + final Tile[] xTiles = zTiles[x]; + for (int y = 0; y < sceneContext.sizeZ; ++y) { + final Tile tile = xTiles[y]; + if (tile == null) + continue; + + final int oX = x + dX; + final int oY = y + dY; + final boolean canReuseTile = canReuseScene && + oX >= 0 && oX < sceneContext.sizeX && + oY >= 0 && oY < sceneContext.sizeZ && + prevXTiles[oY] != null; + + int tileZ = tile.getRenderLevel(); + ids[0] = OVERLAY_FLAG | overlayIds[tileZ][x][y]; + ids[1] = underlayIds[tileZ][x][y]; + calculateTileOverride(sceneContext, canReuseTile ? preSceneCtx : null, tile, tileZ, x, y, oX, oY); + + final Tile bridge = tile.getBridge(); + if(bridge != null) { + tileZ = bridge.getRenderLevel(); + ids[0] = OVERLAY_FLAG | overlayIds[tileZ][x][y]; + ids[1] = underlayIds[tileZ][x][y]; + calculateTileOverride(sceneContext, canReuseTile ? preSceneCtx : null, bridge, tileZ, x, y, oX, oY); + } + } + } + } + } + + private void calculateTileOverride(SceneContext sceneContext, SceneContext prevSceneContext, Tile tile, int tileZ, int tileExX, int tileExY, int prevTileExX, int prevTileExY) { + if(prevSceneContext != null && prevSceneContext.getTileOverrides(tileZ, prevTileExX, prevTileExY, overrides)) { + sceneContext.setTileOverride(tileZ, tileExX, tileExY, overrides); + return; + } + + sceneContext.extendedSceneToWorld(tileExX, tileExY, tileZ, worldPos); + + overrides[0] = tileOverrideManager.getOverride(sceneContext, tile, worldPos, ids); + overrides[1] = tileOverrideManager.getOverride(sceneContext, tile, worldPos, ids[1]); + overrides[2] = tileOverrideManager.getOverride(sceneContext, tile, worldPos, ids[0]); + + sceneContext.setTileOverride(tileZ, tileExX, tileExY, overrides); + } } - public static int[] faceVertexKeys(Tile tile, int face) - { - int[][] vertices = new int[3][3]; - int[] vertexHashes = new int[4]; - faceVertexKeys(tile, face, vertices, vertexHashes); - return vertexHashes; + final class TerrainDataGenerator { + private final int[][] vertices = new int[4][3]; + private final int[] hashes = new int[4]; + private final int[] worldPos = new int[3]; + private final short[] vNormals = new short[3]; + + private int[] vertexHashes; + private int[] vertexColors; + private TileOverride[] vertexOverrides; + private boolean[] vertexIsOverlay; + private boolean[] vertexDefaultColor; + + /** + * Iterates through all Tiles in a given Scene, producing color and + * material data for each vertex of each Tile. Then adds the resulting + * data to appropriate HashMaps. + */ + private void generate(SceneContext sceneContext, SceneContext prevSceneCtx) { + sceneContext.vertexTerrainColor = new Int2IntHashMap(prevSceneCtx != null && prevSceneCtx.vertexTerrainColor != null ? prevSceneCtx.vertexTerrainColor.capacity() : 0); + sceneContext.vertexTerrainTexture = new Int2ObjectHashMap<>(prevSceneCtx != null && prevSceneCtx.vertexTerrainTexture != null ? prevSceneCtx.vertexTerrainTexture.capacity() : 0); + + final Tile[][][] tiles = sceneContext.scene.getExtendedTiles(); + for (int z = 0; z < MAX_Z; ++z) { + final Tile[][] zTiles = tiles[z]; + for (int x = 0; x < sceneContext.sizeX; ++x) { + final Tile[] xTiles = zTiles[x]; + for (int y = 0; y < sceneContext.sizeZ; ++y) { + final var tile = xTiles[y]; + if(tile == null) + continue; + + generateDataForTile(sceneContext, tile, x, y, z); + + final var bridge = tile.getBridge(); + if(bridge != null) + generateDataForTile(sceneContext, bridge, x, y, z); + } + } + } + } + + /** + * Produces color and material data for the vertices of the provided Tile. + * Then adds the resulting data to appropriate HashMaps. + * + * @param sceneContext that the tile is associated with + * @param tile to generate terrain data for + */ + private void generateDataForTile(SceneContext sceneContext, Tile tile, int tileExX, int tileExY, int plane) { + int faceCount; + if (tile.getSceneTilePaint() != null) { + faceCount = 2; + } else if (tile.getSceneTileModel() != null) { + faceCount = tile.getSceneTileModel().getFaceX().length; + } else { + return; + } + + final int tileX = tileExX - sceneContext.sceneOffset; + final int tileY = tileExY - sceneContext.sceneOffset; + final int tileZ = tile.getRenderLevel(); + sceneContext.sceneToWorld(tileX, tileY, tileZ, worldPos); + + vertexHashes = ensureCapacity(vertexHashes, faceCount * VERTICES_PER_FACE); + vertexColors = ensureCapacity(vertexColors, faceCount * VERTICES_PER_FACE); + vertexOverrides = ensureCapacity(vertexOverrides, faceCount * VERTICES_PER_FACE, TileOverride[]::new); + vertexIsOverlay = ensureCapacity(vertexIsOverlay, faceCount * VERTICES_PER_FACE); + vertexDefaultColor = ensureCapacity(vertexDefaultColor, faceCount * VERTICES_PER_FACE); + + Arrays.fill(vertexHashes, 0, faceCount * VERTICES_PER_FACE, 0); + Arrays.fill(vertexColors, 0, faceCount * VERTICES_PER_FACE, 0); + Arrays.fill(vertexOverrides, 0, faceCount * VERTICES_PER_FACE, null); + Arrays.fill(vertexIsOverlay, 0, faceCount * VERTICES_PER_FACE, false); + Arrays.fill(vertexDefaultColor, 0, faceCount * VERTICES_PER_FACE, false); + + if (tile.getSceneTilePaint() != null) { + // tile paint + + var override = sceneContext.getTileOverride(tileZ, tileExX, tileExY, TILE_OVERRIDE_MAIN); + if (override.waterType != WaterType.NONE) { + // skip water tiles + return; + } + + int swColor = tile.getSceneTilePaint().getSwColor(); + int seColor = tile.getSceneTilePaint().getSeColor(); + int nwColor = tile.getSceneTilePaint().getNwColor(); + int neColor = tile.getSceneTilePaint().getNeColor(); + + tileVertexKeys(sceneContext, tile, vertices, vertexHashes); + + if (tileExX >= EXTENDED_SCENE_SIZE - 2 && tileExY >= EXTENDED_SCENE_SIZE - 2) { + // reduce the black scene edges by assigning surrounding colors + neColor = swColor; + nwColor = swColor; + seColor = swColor; + } else if (tileExY >= EXTENDED_SCENE_SIZE - 2) { + nwColor = swColor; + neColor = seColor; + } else if (tileExX >= EXTENDED_SCENE_SIZE - 2) { + neColor = nwColor; + seColor = swColor; + } + + vertexColors[0] = swColor; + vertexColors[1] = seColor; + vertexColors[2] = nwColor; + vertexColors[3] = neColor; + + final boolean useDefaultColor = useDefaultColor(tile, override); + for (int i = 0; i < 4; i++) { + vertexOverrides[i] = override; + vertexIsOverlay[i] = override.queriedAsOverlay; + if (useDefaultColor) + vertexDefaultColor[i] = true; + } + } else if (tile.getSceneTileModel() != null) { + // tile model + + SceneTileModel sceneTileModel = tile.getSceneTileModel(); + + final int[] faceColorsA = sceneTileModel.getTriangleColorA(); + final int[] faceColorsB = sceneTileModel.getTriangleColorB(); + final int[] faceColorsC = sceneTileModel.getTriangleColorC(); + + var overlayOverride = sceneContext.getTileOverride(tileZ, tileExX, tileExY, TILE_OVERRIDE_OVERLAY); + var underlayOverride = sceneContext.getTileOverride(tileZ, tileExX, tileExY, TILE_OVERRIDE_UNDERLAY); + + for (int face = 0; face < faceCount; face++) { + faceVertexKeys(tile, face, vertices, hashes); + + for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) { + final boolean isOverlay = isOverlayFace(tile, face); + final var override = isOverlay ? overlayOverride : underlayOverride; + if (override.waterType != WaterType.NONE) + continue; // skip water faces + + final int base = face * VERTICES_PER_FACE + vertex; + + vertexHashes[base] = hashes[vertex]; + vertexColors[base] = (vertex == 0 ? faceColorsA : vertex == 1 ? faceColorsB : faceColorsC)[face]; + + vertexOverrides[base] = override; + vertexIsOverlay[base] = isOverlay; + + if (isOverlay && useDefaultColor(tile, override)) + vertexDefaultColor[base] = true; + } + } + } + + final int vertexCount = faceCount * VERTICES_PER_FACE; + for (int vertex = 0; vertex < vertexCount; vertex++) { + if (vertexHashes[vertex] == 0) + continue; + + int color = vertexColors[vertex]; + var override = vertexOverrides[vertex]; + if (color < 0 || color == HIDDEN_HSL && !override.forced) + continue; + + // if this vertex already has a 'high priority' color assigned, + // skip assigning a 'low priority' color unless there is no color assigned. + // Near-solid-black tiles that are used in some places under wall objects + boolean lowPriorityColor = vertexColors[vertex] <= 2; + + float lightenMultiplier = 1.5f; + int lightenBase = 15; + int lightenAdd = 3; + float darkenMultiplier = 0.5f; + + float dot = 0; + final int key = vertexHashes[vertex]; + if (sceneContext.getVertexNormal(key, vNormals) == null) { + vNormals[0] = vNormals[1] = vNormals[2] = 0; + } else { + dot = dot(vNormals); + if(dot >= EPSILON) { + // Approximately reverse vanilla tile lighting + dot = (vNormals[0] + vNormals[1]) / sqrt(2 * dot); + } + } + + int lightness = color & 0x7F; + + float lightFactor = Math.max(dot, 0f); + int adjustedLight = (int)((Math.max(lightness - lightenAdd, 0) * lightenMultiplier) + lightenBase); + lightness = (int)mix(lightness, adjustedLight, lightFactor); + + float darkFactor = -Math.min(dot, 0f); + int darkened = (int)(lightness * darkenMultiplier); + lightness = (int)(1.25f * mix(lightness, darkened, darkFactor)); + + lightness = Math.min(lightness, 55); // reduces overexposure + color = (color & ~0x7F) | lightness; + + color = override.modifyColor(color); + + final Material material = override.groundMaterial.getRandomMaterial(worldPos); + final boolean isOverlay = vertexIsOverlay[vertex] != override.blendedAsOpposite; + + // mark the vertex as either an overlay or underlay. + // this is used to determine how to blend between vertex colors + sceneContext.setVertexIsOverlay(key, isOverlay); + + // add color and texture to hashmap + if ((!lowPriorityColor || !sceneContext.isVertexHighPriorityColor(key)) && !vertexDefaultColor[vertex]) { + boolean shouldWrite = isOverlay || !sceneContext.vertexTerrainColor.containsKey(key); + + if (shouldWrite) { + sceneContext.vertexTerrainColor.put(key, color); + sceneContext.vertexTerrainTexture.put(key, material); + } else { + sceneContext.vertexTerrainColor.putIfAbsent(key, color); + sceneContext.vertexTerrainTexture.putIfAbsent(key, material); + } + + if (!lowPriorityColor) + sceneContext.setVertexHighPriorityColor(key); + } + } + } + } + + final class UnderwaterTerrainGenerator { + private final int[][] vertices = new int[4][3]; + private final int[] hashes = new int[4]; + + private final byte[][][] underwaterDepthLevels = new byte[MAX_Z][EXTENDED_SCENE_SIZE + 1][EXTENDED_SCENE_SIZE + 1]; + + // Defines ranges of water tiles + private final int[] minX = new int[MAX_Z]; + private final int[] maxX = new int[MAX_Z]; + private final int[] minY = new int[MAX_Z]; + private final int[] maxY = new int[MAX_Z]; + + /** + * Generates underwater terrain data by iterating through all Tiles in a given + * Scene, increasing the depth of each tile based on its distance from the shore. + * Then stores the resulting data in a HashMap. + */ + private void generate(SceneContext sceneContext, SceneContext prevSceneCtx) { + final Tile[][][] tiles = sceneContext.scene.getExtendedTiles(); + final int sizeX = sceneContext.sizeX; + final int sizeY = sceneContext.sizeZ; + // bit 1 set if a tile contains at least 1 face which qualifies as water + // bit 2 set if a 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.tileFlags = new byte[MAX_Z * sizeX * sizeY]; + sceneContext.vertexTerrainData = new Int2IntHashMap(prevSceneCtx != null && prevSceneCtx.vertexTerrainData != null ? prevSceneCtx.vertexTerrainData.capacity() : 0); + // the world-space height offsets of each vertex on the tile grid + // these offsets are interpolated to calculate offsets for vertices not on the grid (tilemodels) + + for (int z = 0; z < MAX_Z; ++z) { + for (int x = 0; x < sizeX; ++x) { + // set the array to 1 initially + // this assumes that all vertices are water; + // we will set non-water vertices to 0 in the next loop + Arrays.fill(underwaterDepthLevels[z][x], (byte)1); + } + } + + int minZ = MAX_Z, maxZ = 0; + Arrays.fill(minX, sizeX); + Arrays.fill(minY, sizeY); + Arrays.fill(maxX, 0); + Arrays.fill(maxY, 0); + + // figure out which vertices are water and assign some data + for (int z = 0; z < MAX_Z; ++z) { + final Tile[][] zTiles = tiles[z]; + final byte[][] zUnderwaterDepthLevels = underwaterDepthLevels[z]; + for (int x = 0; x < sizeX; ++x) { + final Tile[] xTiles = zTiles[x]; + for (int y = 0; y < sizeY; ++y) { + Tile tile = xTiles[y]; + if (tile == null) { + zUnderwaterDepthLevels[x][y] = 0; + zUnderwaterDepthLevels[x + 1][y] = 0; + zUnderwaterDepthLevels[x][y + 1] = 0; + zUnderwaterDepthLevels[x + 1][y + 1] = 0; + continue; + } + + if (tile.getBridge() != null) + tile = tile.getBridge(); + + int tileZ = tile.getRenderLevel(); + if (tile.getSceneTilePaint() != null) { + tileVertexKeys(sceneContext, tile, vertices, hashes); + + var override = sceneContext.getTileOverride(tileZ, x, y, TILE_OVERRIDE_MAIN); + if (seasonalWaterType(override, tile.getSceneTilePaint().getTexture()) == WaterType.NONE) { + for (int i = 0; i < hashes.length; i++) + if (tile.getSceneTilePaint().getNeColor() != HIDDEN_HSL || override.forced) + sceneContext.setVertexIsLand(hashes[i]); + + zUnderwaterDepthLevels[x][y] = 0; + zUnderwaterDepthLevels[x + 1][y] = 0; + zUnderwaterDepthLevels[x][y + 1] = 0; + zUnderwaterDepthLevels[x + 1][y + 1] = 0; + } else { + // Stop tiles on the same X,Y coordinates on different planes from + // each generating water. Prevents undesirable results in certain places. + if (z > 0) { + boolean continueLoop = false; + + for (int checkZ = 0; checkZ < z; ++checkZ) { + if (sceneContext.isTileFlagSet(checkZ, x, y, TILE_WATER_FLAG)) { + zUnderwaterDepthLevels[x][y] = 0; + zUnderwaterDepthLevels[x + 1][y] = 0; + zUnderwaterDepthLevels[x][y + 1] = 0; + zUnderwaterDepthLevels[x + 1][y + 1] = 0; + + sceneContext.setTileFlag(z, x, y, TILE_SKIP_FLAG); + + continueLoop = true; + + break; + } + } + + if (continueLoop) + continue; + } + + sceneContext.setTileFlag(z, x, y, TILE_WATER_FLAG); + maxZ = max(maxZ, z); + minZ = min(minZ, z); + minX[z] = min(minX[z], x); + maxX[z] = max(maxX[z], x); + minY[z] = min(minY[z], y); + maxY[z] = max(maxY[z], y); + + for (int i = 0; i < hashes.length; i++) + sceneContext.setVertexIsWater(hashes[i]); + } + } else if (tile.getSceneTileModel() != null) { + SceneTileModel model = tile.getSceneTileModel(); + + int faceCount = model.getFaceX().length; + + var overlayOverride = sceneContext.getTileOverride(tileZ, x, y, TILE_OVERRIDE_OVERLAY); + var underlayOverride = sceneContext.getTileOverride(tileZ, x, y, TILE_OVERRIDE_UNDERLAY); + + // Stop tiles on the same X,Y coordinates on different planes from + // each generating water. Prevents undesirable results in certain places. + if (z > 0) { + boolean tileIncludesWater = false; + for (int face = 0; face < faceCount; face++) { + var override = ProceduralGenerator.isOverlayFace(tile, face) ? overlayOverride : underlayOverride; + int textureId = model.getTriangleTextureId() == null ? -1 : + model.getTriangleTextureId()[face]; + if (seasonalWaterType(override, textureId) != WaterType.NONE) { + tileIncludesWater = true; + break; + } + } + + if (tileIncludesWater) { + boolean continueLoop = false; + + for (int checkZ = 0; checkZ < z; ++checkZ) { + if (sceneContext.isTileFlagSet(checkZ, x, y, TILE_WATER_FLAG)) { + zUnderwaterDepthLevels[x][y] = 0; + zUnderwaterDepthLevels[x + 1][y] = 0; + zUnderwaterDepthLevels[x][y + 1] = 0; + zUnderwaterDepthLevels[x + 1][y + 1] = 0; + + sceneContext.setTileFlag(z, x, y, TILE_SKIP_FLAG); + + continueLoop = true; + + break; + } + } + + if (continueLoop) + continue; + } + } + + for (int face = 0; face < faceCount; face++) { + faceVertexKeys(tile, face, vertices, hashes); + + var override = ProceduralGenerator.isOverlayFace(tile, face) ? overlayOverride : underlayOverride; + int textureId = model.getTriangleTextureId() == null ? -1 : + model.getTriangleTextureId()[face]; + if (seasonalWaterType(override, textureId) == WaterType.NONE) { + for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) { + if (model.getTriangleColorA()[face] != HIDDEN_HSL || override.forced) + sceneContext.setVertexIsLand(hashes[vertex]); + + if (vertices[vertex][0] % LOCAL_TILE_SIZE == 0 && + vertices[vertex][1] % LOCAL_TILE_SIZE == 0 + ) { + int vX = (vertices[vertex][0] >> LOCAL_COORD_BITS) + sceneContext.sceneOffset; + int vY = (vertices[vertex][1] >> LOCAL_COORD_BITS) + sceneContext.sceneOffset; + + zUnderwaterDepthLevels[vX][vY] = 0; + } + } + } else { + sceneContext.setTileFlag(z, x, y, TILE_WATER_FLAG); + minZ = min(minZ, z); + maxZ = max(maxZ, z); + minX[z] = min(minX[z], x); + maxX[z] = max(maxX[z], x); + minY[z] = min(minY[z], y); + maxY[z] = max(maxY[z], y); + + for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) + sceneContext.setVertexIsWater(hashes[vertex]); + } + } + } else { + zUnderwaterDepthLevels[x][y] = 0; + zUnderwaterDepthLevels[x + 1][y] = 0; + zUnderwaterDepthLevels[x][y + 1] = 0; + zUnderwaterDepthLevels[x + 1][y + 1] = 0; + } + } + } + } + + // Sink terrain further from shore by desired levels. + for (int level = 0; level < DEPTH_LEVEL_SLOPE.length - 1; level++) { + for (int z = minZ; z <= maxZ; ++z) { + final byte[][] zUnderwaterDepthLevels = underwaterDepthLevels[z]; + for (int x = minX[z]; x <= maxX[z]; x++) { + for (int y = minY[z]; y <= maxY[z]; y++) { + int tileHeight = zUnderwaterDepthLevels[x][y]; + if (tileHeight == 0 || tileHeight >= Byte.MAX_VALUE) { + // Skip the tile if it isn't water. + continue; + } + + // If it's on the edge of the scene, reset the depth so + // it creates a 'wall' to prevent fog from passing through. + // Not incredibly effective, but better than nothing. + if (x == 0 || y == 0 || x == EXTENDED_SCENE_SIZE || y == EXTENDED_SCENE_SIZE) { + zUnderwaterDepthLevels[x][y] = 0; + continue; + } + + if (zUnderwaterDepthLevels[x - 1][y] < tileHeight) { + // West + continue; + } + if (x < zUnderwaterDepthLevels.length - 1 + && zUnderwaterDepthLevels[x + 1][y] < tileHeight) { + // East + continue; + } + if (zUnderwaterDepthLevels[x][y - 1] < tileHeight) { + // South + continue; + } + if (y < zUnderwaterDepthLevels.length - 1 + && zUnderwaterDepthLevels[x][y + 1] < tileHeight) { + // North + continue; + } + // At this point, it's surrounded only by other depth-adjusted vertices. + zUnderwaterDepthLevels[x][y]++; + } + } + } + } + + // Store the height offsets in a hashmap and calculate interpolated + // height offsets for non-corner vertices. + for (int z = minZ; z <= maxZ; ++z) { + final Tile[][] zTiles = tiles[z]; + for (int x = minX[z]; x <= maxX[z]; x++) { + final Tile[] xTiles = zTiles[x]; + for (int y = minY[z]; y <= maxY[z]; y++) { + Tile tile = xTiles[y]; + if (tile == null) + continue; + + if (!sceneContext.isTileFlagSet(z, x, y, TILE_WATER_FLAG)) + continue; + + if (tile.getBridge() != null) + tile = tile.getBridge(); + + if (tile.getSceneTilePaint() != null) { + tileVertexKeys(sceneContext, tile, vertices, hashes); + + final int swVertex = getHeightOffset(z, x, y); + final int seVertex = getHeightOffset(z, x + 1, y); + final int nwVertex = getHeightOffset(z, x, y + 1); + final int neVertex = getHeightOffset(z, x + 1, y + 1); + + sceneContext.setVertexUnderwaterDepth(hashes[0], swVertex); + sceneContext.setVertexUnderwaterDepth(hashes[1], seVertex); + sceneContext.setVertexUnderwaterDepth(hashes[2], nwVertex); + sceneContext.setVertexUnderwaterDepth(hashes[3], neVertex); + } else if (tile.getSceneTileModel() != null) { + SceneTileModel sceneTileModel = tile.getSceneTileModel(); + + int faceCount = sceneTileModel.getFaceX().length; + + for (int face = 0; face < faceCount; face++) { + faceVertexKeys(tile, face, vertices, hashes); + + for (int vertex = 0; vertex < VERTICES_PER_FACE; vertex++) { + if (vertices[vertex][0] % LOCAL_TILE_SIZE == 0 && + vertices[vertex][1] % LOCAL_TILE_SIZE == 0 + ) { + // The vertex is at the corner of the tile; + // simply use the offset in the tile grid array. + + int vX = (vertices[vertex][0] >> LOCAL_COORD_BITS) + sceneContext.sceneOffset; + int vY = (vertices[vertex][1] >> LOCAL_COORD_BITS) + sceneContext.sceneOffset; + + int height = getHeightOffset(z, vX, vY); + sceneContext.setVertexUnderwaterDepth(hashes[vertex], height); + } else { + // If the tile is a tile model and this vertex is shared only by faces that are water, + // interpolate between the height offsets at each corner to get the height offset + // of the vertex. + + float lerpX = fract(vertices[vertex][0] / (float) LOCAL_TILE_SIZE); + float lerpY = fract(vertices[vertex][1] / (float) LOCAL_TILE_SIZE); + float northHeightOffset = mix( + getHeightOffset(z, x, y + 1), + getHeightOffset(z, x + 1, y + 1), + lerpX + ); + float southHeightOffset = mix( + getHeightOffset(z, x, y), + getHeightOffset(z, x + 1, y), + lerpX); + int heightOffset = (int) mix(southHeightOffset, northHeightOffset, lerpY); + + if (!sceneContext.isVertexLand(hashes[vertex])) + sceneContext.setVertexUnderwaterDepth(hashes[vertex], heightOffset); + } + } + } + } + } + } + } + + if(sceneContext instanceof LegacySceneContext) { + byte[][][] sceneUnderwaterDepthLevels = ((LegacySceneContext)sceneContext).underwaterDepthLevels; + for (int z = 0; z < MAX_Z; ++z) { + for (int x = 0; x < sizeX; ++x) + System.arraycopy(this.underwaterDepthLevels[z][x], 0, sceneUnderwaterDepthLevels[z][x], 0, sizeY); + } + } + } + + private int getHeightOffset(int z, int x, int y) { + int depthLevel = underwaterDepthLevels[z][x][y]; + if (depthLevel == 0) + return 0; + + int depth = DEPTH_LEVEL_SLOPE[depthLevel - 1]; + return (int) (depth * .55f); // legacy weirdness + } } - private static final int[] tzHaarRecolored = new int[3]; // used when calculating the gradient to apply to the walls of TzHaar // to emulate the style from 2008 HD rework private static final float[] gradientBaseColor = vec(3, 4, 26); @@ -1019,7 +1163,8 @@ public static int[] recolorTzHaar( int face, int color1, int color2, - int color3 + int color3, + int[] out ) { int[] hsl1 = ColorUtils.unpackRawHsl(color1); int[] hsl2 = ColorUtils.unpackRawHsl(color2); @@ -1043,9 +1188,7 @@ public static int[] recolorTzHaar( round(hsl2, mix(gradientDarkColor, gradientBaseColor, pos)); round(hsl3, mix(gradientDarkColor, gradientBaseColor, pos)); } - } - else if (modelOverride.tzHaarRecolorType == TzHaarRecolorType.HUE_SHIFT) - { + } else if (modelOverride.tzHaarRecolorType == TzHaarRecolorType.HUE_SHIFT) { // objects around the entrance to The Inferno only need a hue-shift // and very slight lightening to match the lightened terrain hsl1[2] += 1; @@ -1053,10 +1196,10 @@ else if (modelOverride.tzHaarRecolorType == TzHaarRecolorType.HUE_SHIFT) hsl3[2] += 1; } - tzHaarRecolored[0] = ColorUtils.packRawHsl(hsl1); - tzHaarRecolored[1] = ColorUtils.packRawHsl(hsl2); - tzHaarRecolored[2] = ColorUtils.packRawHsl(hsl3); + out[0] = ColorUtils.packRawHsl(hsl1); + out[1] = ColorUtils.packRawHsl(hsl2); + out[2] = ColorUtils.packRawHsl(hsl3); - return tzHaarRecolored; + return out; } } diff --git a/src/main/java/rs117/hd/scene/SceneContext.java b/src/main/java/rs117/hd/scene/SceneContext.java index 85c424d02a..5103cc3de4 100644 --- a/src/main/java/rs117/hd/scene/SceneContext.java +++ b/src/main/java/rs117/hd/scene/SceneContext.java @@ -1,7 +1,6 @@ package rs117.hd.scene; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.Objects; import java.util.stream.Stream; @@ -13,8 +12,11 @@ import rs117.hd.scene.environments.Environment; import rs117.hd.scene.lights.Light; import rs117.hd.scene.materials.Material; +import rs117.hd.scene.tile_overrides.TileOverride; import rs117.hd.scene.tile_overrides.TileOverrideVariables; import rs117.hd.utils.HDUtils; +import rs117.hd.utils.collections.Int2IntHashMap; +import rs117.hd.utils.collections.Int2ObjectHashMap; import static net.runelite.api.Constants.*; import static net.runelite.api.Constants.SCENE_SIZE; @@ -22,16 +24,38 @@ import static rs117.hd.utils.MathUtils.*; public class SceneContext { + public static final byte TILE_WATER_FLAG = 1; + public static final byte TILE_SKIP_FLAG = 1 << 1; + + public static final int TILE_OVERRIDE_MAIN = 0; + public static final int TILE_OVERRIDE_UNDERLAY = 1; + public static final int TILE_OVERRIDE_OVERLAY = 2; + public static final int TILE_OVERRIDE_COUNT = 3; + + public static final int VERTEX_IS_LAND = 1 << 1; + public static final int VERTEX_IS_WATER = 1 << 2; + public static final int VERTEX_IS_OVERLAY = 1 << 3; + public static final int VERTEX_IS_UNDERLAY = 1 << 4; + public static final int VERTEX_IS_HIGH_PRIORITY_COLOR = 1 << 5; + public static final int VERTEX_UNDER_WATER_DEPTH_SHIFT = 6; + public static final int VERTEX_UNDER_WATER_DEPTH_MASK = 0x3FFFFFF; + + // Thread safe tile override variables + public final static ThreadLocal tileOverrideVars = ThreadLocal.withInitial(TileOverrideVariables::new); public final Client client; public final Scene scene; public final int expandedMapLoadingChunks; - public int sizeX, sizeZ; @Nullable public final int[] sceneBase; public final AABB sceneBounds; + public final ArrayList environments = new ArrayList<>(); + public final ArrayList lights = new ArrayList<>(); + public final HashSet knownProjectiles = new HashSet<>(); + public final ArrayList lightSpawnsToHandleOnClientThread = new ArrayList<>(); + public int sizeX, sizeZ; public int sceneOffset; - + public int numVisibleLights = 0; public boolean enableAreaHiding; public boolean fillGaps; public boolean isInChambersOfXeric; @@ -40,49 +64,16 @@ public class SceneContext { @Nullable public Area currentArea; public Area[] possibleAreas = new Area[0]; - public final ArrayList environments = new ArrayList<>(); public byte[][] filledTiles = new byte[EXTENDED_SCENE_SIZE][EXTENDED_SCENE_SIZE]; - - public int staticVertexCount = 0; - - public int staticGapFillerTilesOffset; - public int staticGapFillerTilesVertexCount; - public int staticCustomTilesOffset; - public int staticCustomTilesVertexCount; - - // Statistics - public int uniqueModels; + public byte[] tileFlags; + public char[] tileOverrideIndices; // Terrain data - public HashMap vertexTerrainColor; - public HashMap vertexTerrainTexture; - public HashMap vertexTerrainNormals; - // Used for overriding potentially low quality vertex colors - public HashMap highPriorityColor; - - // Water-related data - public boolean[][][] tileIsWater; - public HashMap vertexIsWater; - public HashMap vertexIsLand; - public HashMap vertexIsOverlay; - public HashMap vertexIsUnderlay; - public boolean[][][] skipTile; - public HashMap vertexUnderwaterDepth; - public int[][][] underwaterDepthLevels; - - // Thread safe tile override variables - public final ThreadLocal tileOverrideVars = ThreadLocal.withInitial(TileOverrideVariables::new); - - public int numVisibleLights = 0; - public final ArrayList lights = new ArrayList<>(); - public final HashSet knownProjectiles = new HashSet<>(); - public final ArrayList lightSpawnsToHandleOnClientThread = new ArrayList<>(); - - // Model pusher arrays, to avoid simultaneous usage from different threads - public final int[] modelFaceVertices = new int[12]; - public final float[] modelFaceUvs = new float[12]; - public final float[] modelFaceNormals = new float[12]; - public final int[] modelPusherResults = new int[2]; + public Int2ObjectHashMap vertexTerrainTexture; + public Int2IntHashMap vertexTerrainColor; + public Int2IntHashMap vertexTerrainData; + public Int2IntHashMap vertexTerrainNormalIndices; + public short[] vertexTerrainNormals; public SceneContext(Client client, Scene scene, int expandedMapLoadingChunks) { this.client = client; @@ -98,6 +89,149 @@ public SceneContext(Client client, Scene scene, int expandedMapLoadingChunks) { public synchronized void destroy() {} + public void setVertexIsLand(int hash) { + vertexTerrainData.or(hash, VERTEX_IS_LAND, 0); + } + + public void setVertexIsWater(int hash) { + vertexTerrainData.or(hash, VERTEX_IS_WATER, 0); + } + + public void setVertexHighPriorityColor(int hash) { + vertexTerrainData.or(hash, VERTEX_IS_HIGH_PRIORITY_COLOR, 0); + } + + public void setVertexIsOverlay(int hash, boolean isOverlay) { + vertexTerrainData.or(hash, isOverlay ? VERTEX_IS_OVERLAY : VERTEX_IS_UNDERLAY, 0); + } + + public void setVertexUnderwaterDepth(int hash, int depth) { + int value = (depth << VERTEX_UNDER_WATER_DEPTH_SHIFT); + int mask = VERTEX_UNDER_WATER_DEPTH_MASK << VERTEX_UNDER_WATER_DEPTH_SHIFT; + + vertexTerrainData.setBits(hash, value, mask, 0); + } + + public boolean isVertexHighPriorityColor(int hash) { + return vertexTerrainData.test(hash, VERTEX_IS_HIGH_PRIORITY_COLOR); + } + + public boolean isVertexLand(int hash) { + return vertexTerrainData.test(hash, VERTEX_IS_LAND); + } + + public boolean isVertexWater(int hash) { + return vertexTerrainData.test(hash, VERTEX_IS_WATER); + } + + public boolean isVertexOverlay(int hash) { + return vertexTerrainData.test(hash, VERTEX_IS_OVERLAY); + } + + public boolean isVertexUnderlay(int hash) { + return vertexTerrainData.test(hash, VERTEX_IS_UNDERLAY); + } + + public short[] getVertexNormalOrDefault(int hash, short[] result, short[] defaultNormal) { + if(getVertexNormal(hash, result) != null) + return result; + + result[0] = defaultNormal[0]; + result[1] = defaultNormal[1]; + result[2] = defaultNormal[2]; + + return result; + } + + public short[] getVertexNormal(int hash, short[] result) { + int index = vertexTerrainNormalIndices.getOrDefault(hash, -1); + if(index == -1) + return null; + + final int offset = index * 3; + result[0] = vertexTerrainNormals[offset]; + result[1] = vertexTerrainNormals[offset + 1]; + result[2] = vertexTerrainNormals[offset + 2]; + return result; + } + + public int getVertexUnderwaterDepth(int hash) { + return vertexTerrainData.getBits( + hash, + VERTEX_UNDER_WATER_DEPTH_MASK, + VERTEX_UNDER_WATER_DEPTH_SHIFT, + 0 + ); + } + + public void setTileFlag(int plane, int x, int y, byte flag) { + tileFlags[getTileIdx(plane, x, y)] |= flag; + } + + public boolean isTileFlagSet(int plane, int x, int y, byte flag) { + return (tileFlags[getTileIdx(plane, x, y)] & flag) != 0; + } + + public void setTileOverride( + int plane, + int x, + int y, + TileOverride[] overrides + ) { + setTileOverride(plane, x, y, overrides[0], overrides[1], overrides[2]); + } + + public void setTileOverride( + int plane, + int x, + int y, + TileOverride mainOverride, + TileOverride underlayOverride, + TileOverride overlayOverride + ) { + final int offset = getTileIdx(plane, x, y) * TILE_OVERRIDE_COUNT; + + if(mainOverride != null && mainOverride != TileOverride.NONE) + tileOverrideIndices[offset + TILE_OVERRIDE_MAIN] = (char) (mainOverride.index + 1); + + if(underlayOverride != null && underlayOverride != TileOverride.NONE) + tileOverrideIndices[offset + TILE_OVERRIDE_UNDERLAY] = (char) (underlayOverride.index + 1); + + if(overlayOverride != null && overlayOverride != TileOverride.NONE) + tileOverrideIndices[offset + TILE_OVERRIDE_OVERLAY] = (char) (overlayOverride.index + 1); + } + + public boolean getTileOverrides(int plane, int x, int y, TileOverride[] result) { + final int offset = getTileIdx(plane, x, y) * TILE_OVERRIDE_COUNT; + + boolean hasOverrides = false; + for (int i = 0; i < TILE_OVERRIDE_COUNT; i++) { + final int idx = tileOverrideIndices[offset + i]; + result[i] = idx > 0 ? TileOverrideManager.OVERRIDES[idx - 1] : TileOverride.NONE; + if(idx > 0) + hasOverrides = true; + } + + return hasOverrides; + } + + public TileOverride getTileOverride(int plane, int x, int y, int type) { + final int offset = getTileIdx(plane, x, y) * TILE_OVERRIDE_COUNT; + final int idx = tileOverrideIndices[offset + type]; + return idx > 0 ? TileOverrideManager.OVERRIDES[idx - 1] : TileOverride.NONE; + } + + public int[] getTileIndices(int tileIdx, int[] indices) { + indices[0] = tileIdx % sizeX; + indices[1] = (tileIdx / sizeX) % sizeZ; + indices[2] = tileIdx / (sizeX * sizeZ); + return indices; + } + + public int getTileIdx(int plane, int x, int y) { + return x + y * sizeX + plane * sizeX * sizeZ; + } + /** * Transform local coordinates into world coordinates. * If the {@link LocalPoint} is not in the scene, this returns untranslated coordinates when in instances. diff --git a/src/main/java/rs117/hd/scene/TileOverrideManager.java b/src/main/java/rs117/hd/scene/TileOverrideManager.java index 349db12cfd..3824b33b7d 100644 --- a/src/main/java/rs117/hd/scene/TileOverrideManager.java +++ b/src/main/java/rs117/hd/scene/TileOverrideManager.java @@ -1,7 +1,5 @@ package rs117.hd.scene; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ListMultimap; import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; @@ -22,9 +20,11 @@ import rs117.hd.scene.areas.Area; import rs117.hd.scene.ground_materials.GroundMaterial; import rs117.hd.scene.tile_overrides.TileOverride; +import rs117.hd.scene.tile_overrides.TileOverrideVariables; import rs117.hd.utils.FileWatcher; import rs117.hd.utils.Props; import rs117.hd.utils.ResourcePath; +import rs117.hd.utils.collections.Int2ObjectHashMap; import static rs117.hd.scene.tile_overrides.TileOverride.OVERLAY_FLAG; import static rs117.hd.utils.HDUtils.localToWorld; @@ -38,6 +38,8 @@ public class TileOverrideManager { private static final ThreadLocal OVERLAY_UNDERLAY_IDS = ThreadLocal.withInitial(() -> new int[2]); + public static TileOverride[] OVERRIDES; + @Inject private Client client; @@ -53,7 +55,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))); @@ -62,6 +64,7 @@ public void startUp() { public void shutDown() { if (fileWatcher != null) fileWatcher.unregister(); + OVERRIDES = null; fileWatcher = null; anyMatchOverrides = null; idMatchOverrides = null; @@ -91,7 +94,7 @@ public void reload(boolean skipSceneReload) { checkForReplacementLoops(allOverrides); List anyMatch = new ArrayList<>(); - ListMultimap idMatch = ArrayListMultimap.create(); + Int2ObjectHashMap> idMatch = new Int2ObjectHashMap<>(); var tileOverrideVars = plugin.vars.aliases(Map.of( "textures", "groundTextures" @@ -112,8 +115,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); } @@ -121,6 +128,7 @@ public void reload(boolean skipSceneReload) { anyMatchOverrides = anyMatch; idMatchOverrides = idMatch; + OVERRIDES = allOverrides; log.debug("Loaded {} tile overrides", allOverrides.length); } catch (IOException ex) { @@ -232,13 +240,14 @@ public TileOverride getOverride(SceneContext sceneContext, @Nonnull Tile tile, @ ids[0] = overlayId; ids[1] = underlayId; } - var override = getOverrideBeforeReplacements(worldPos, ids); + + final TileOverride override = getOverrideBeforeReplacements(worldPos, ids); if (override.isConstant()) return override; - var vars = sceneContext.tileOverrideVars.get(); + final TileOverrideVariables vars = SceneContext.tileOverrideVars.get(); vars.setTile(tile); - var replacement = override.resolveReplacements(vars); + TileOverride replacement = override.resolveReplacements(vars); vars.setTile(null); // Avoid accidentally keeping the old scene in memory return replacement; } @@ -248,21 +257,25 @@ public TileOverride getOverrideBeforeReplacements(@Nonnull int[] worldPos, int.. var match = TileOverride.NONE; int index = match.index; - outer: - for (int id : ids) { + for (int i = 0; i < ids.length; i++) { + final int id = ids[i]; final var entries = idMatchOverrides.get(id); - for (int i = 0; i < entries.size(); i++) { - final var entry = entries.get(i); + if(entries == null) + continue; + for (int k = 0; k < entries.size(); k++) { + final var entry = entries.get(k); if (entry.area.containsPoint(worldPos)) { index = entry.index; match = entry.replacement; match.queriedAsOverlay = (id & OVERLAY_FLAG) != 0; - break outer; + i = ids.length; + break; } } } - for (var entry : anyMatchOverrides) { + for (int i = 0; i < anyMatchOverrides.size(); i++) { + final var entry = anyMatchOverrides.get(i); if (entry.index > index) break; if (entry.area.containsPoint(worldPos)) { diff --git a/src/main/java/rs117/hd/scene/areas/AABB.java b/src/main/java/rs117/hd/scene/areas/AABB.java index 30c72cedb3..8800c0b76b 100644 --- a/src/main/java/rs117/hd/scene/areas/AABB.java +++ b/src/main/java/rs117/hd/scene/areas/AABB.java @@ -191,6 +191,13 @@ public float[] getCenter() { }; } + public int distanceToPoint(int x, int y) { + int dx = Math.max(Math.max(minX - x, 0), x - maxX); + int dy = Math.max(Math.max(minY - y, 0), y - maxY); + + return dx * dx + dy * dy; + } + @Override public String toString() { if (hasZ()) diff --git a/src/main/java/rs117/hd/scene/areas/Area.java b/src/main/java/rs117/hd/scene/areas/Area.java index 5e32d81286..55fb13c418 100644 --- a/src/main/java/rs117/hd/scene/areas/Area.java +++ b/src/main/java/rs117/hd/scene/areas/Area.java @@ -3,10 +3,13 @@ import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import rs117.hd.scene.AreaManager; public class Area { + private static final ThreadLocal SORT_SCRATCH = ThreadLocal.withInitial(() -> new AABB[16]); + public static final Area NONE = new Area("NONE", 0, 0, 0, 0); public static final Area ALL = new Area("ALL", Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); public static Area OVERWORLD = NONE; @@ -17,15 +20,21 @@ public class Area { public String[] areas; public int[] regions; + @JsonAdapter(RegionBox.Adapter.class) public RegionBox[] regionBoxes; + @JsonAdapter(AABB.ArrayAdapter.class) @SerializedName("aabbs") public AABB[] rawAabbs; + @JsonAdapter(AABB.ArrayAdapter.class) public AABB[] unhideAreas = {}; public transient AABB[] aabbs; + public transient AABB areaBounds; + + private transient AABB[] sortedAabbs; private transient boolean normalized; public Area(String name) { @@ -42,41 +51,153 @@ public void normalize() { return; normalized = true; - ArrayList aabbs = new ArrayList<>(); + ArrayList list = new ArrayList<>(); + if (rawAabbs != null) - Collections.addAll(aabbs, rawAabbs); + Collections.addAll(list, rawAabbs); + if (regions != null) for (int regionId : regions) - aabbs.add(AABB.fromRegionId(regionId)); + list.add(AABB.fromRegionId(regionId)); + if (regionBoxes != null) for (var box : regionBoxes) - aabbs.add(box.toAabb()); + list.add(box.toAabb()); + if (areas != null) { - for (String area : areas) { + for (String areaName : areas) { for (Area other : AreaManager.AREAS) { - if (area.equals(other.name)) { + if (areaName.equals(other.name)) { other.normalize(); - Collections.addAll(aabbs, other.aabbs); + Collections.addAll(list, other.aabbs); break; } } } } - this.aabbs = aabbs.toArray(AABB[]::new); + this.aabbs = list.toArray(AABB[]::new); + + // Compute bounds + if (aabbs.length > 0) { + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + + for (AABB aabb : aabbs) { + minX = Math.min(minX, aabb.minX); + minY = Math.min(minY, aabb.minY); + minZ = Math.min(minZ, aabb.minZ); + maxX = Math.max(maxX, aabb.maxX); + maxY = Math.max(maxY, aabb.maxY); + maxZ = Math.max(maxZ, aabb.maxZ); + } + + areaBounds = new AABB(minX, minY, minZ, maxX, maxY, maxZ); + + if (aabbs.length > 4) { + sortedAabbs = new AABB[4 * aabbs.length]; + + buildCorner(0, areaBounds.minX, areaBounds.minY); + buildCorner(1, areaBounds.minX, areaBounds.maxY); + buildCorner(2, areaBounds.maxX, areaBounds.minY); + buildCorner(3, areaBounds.maxX, areaBounds.maxY); + } + } if (unhideAreas == null) unhideAreas = new AABB[0]; } + private void buildCorner(int c, int cx, int cy) { + AABB[] scratch = SORT_SCRATCH.get(); + if (scratch.length < aabbs.length) + SORT_SCRATCH.set(scratch = new AABB[aabbs.length]); + + System.arraycopy(aabbs, 0, scratch, 0, aabbs.length); + + Arrays.sort(scratch, 0, aabbs.length, (a1, a2) -> { + final float s1 = score(a1, cx, cy); + final float s2 = score(a2, cx, cy); + return Float.compare(s1, s2); + }); + + System.arraycopy(scratch, 0, sortedAabbs, c * aabbs.length, aabbs.length); + } + + private static float score(AABB aabb, int cx, int cy) { + float distance = aabb.distanceToPoint(cx, cy); + + int w = aabb.maxX - aabb.minX; + int h = aabb.maxY - aabb.minY; + int area = w * h; + + final float SIZE_WEIGHT = 1e-6F; + return distance - (area * SIZE_WEIGHT); + } + + private int getClosestCorner(int x, int y) { + int dx = x - areaBounds.minX; + int dy = y - areaBounds.minY; + int best = dx * dx + dy * dy; + int corner = 0; + + dx = x - areaBounds.minX; + dy = y - areaBounds.maxY; + int d = dx * dx + dy * dy; + if (d < best) { + best = d; + corner = 1; + } + + dx = x - areaBounds.maxX; + dy = y - areaBounds.minY; + d = dx * dx + dy * dy; + if (d < best) { + best = d; + corner = 2; + } + + dx = x - areaBounds.maxX; + dy = y - areaBounds.maxY; + d = dx * dx + dy * dy; + if (d < best) + corner = 3; + + return corner; + } + public boolean containsPoint(boolean includeUnhiding, int... worldPoint) { - for (var aabb : aabbs) - if (aabb.contains(worldPoint)) - return true; - if (includeUnhiding) - for (var aabb : unhideAreas) + if (areaBounds != null && !areaBounds.contains(worldPoint)) + return false; + + final int length = aabbs.length; + if (sortedAabbs != null && areaBounds != null) { + final int corner = getClosestCorner(worldPoint[0], worldPoint[1]); + final int offset = corner * length; + + for (int i = 0; i < length; i++) { + if (sortedAabbs[offset + i].contains(worldPoint)) + return true; + } + } else { + for (int i = 0; i < length; i++) { + if (aabbs[i].contains(worldPoint)) + return true; + } + } + + if (includeUnhiding) { + for (AABB aabb : unhideAreas) { if (aabb.contains(worldPoint)) return true; + } + } + return false; } @@ -88,10 +209,12 @@ public boolean intersects(boolean includeUnhiding, int minX, int minY, int maxX, for (AABB aabb : aabbs) if (aabb.intersects(minX, minY, maxX, maxY)) return true; + if (includeUnhiding) - for (var aabb : unhideAreas) + for (AABB aabb : unhideAreas) if (aabb.intersects(minX, minY, maxX, maxY)) return true; + return false; } diff --git a/src/main/java/rs117/hd/scene/ground_materials/GroundMaterial.java b/src/main/java/rs117/hd/scene/ground_materials/GroundMaterial.java index c2ee655895..c3b9b84598 100644 --- a/src/main/java/rs117/hd/scene/ground_materials/GroundMaterial.java +++ b/src/main/java/rs117/hd/scene/ground_materials/GroundMaterial.java @@ -33,13 +33,19 @@ public void normalize() { materials[j] = Material.NONE; } + + public Material getRandomMaterial(int[] worldPos) { + assert worldPos.length == 3; + return getRandomMaterial(worldPos[0], worldPos[1], worldPos[2]); + } + /** * Get a random material based on the given coordinates. */ - public Material getRandomMaterial(int... worldPos) { - long hash = 0; - for (int coord : worldPos) - hash = hash * 31 + coord; + public Material getRandomMaterial(int x, int y, int z) { + long hash = x; + hash = hash * 31 + y; + hash = hash * 31 + z; long seed = (hash ^ 0x5DEECE66DL) & ((1L << 48) - 1); seed = (seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1); int r = (int) (seed >>> (48 - 31)); 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 3b03fe3281..31947d345e 100644 --- a/src/main/java/rs117/hd/scene/model_overrides/ModelOverride.java +++ b/src/main/java/rs117/hd/scene/model_overrides/ModelOverride.java @@ -208,6 +208,19 @@ public void normalize(HdPlugin plugin) { shadowOpacityThreshold = 1; } + public void clearIds(){ + areas = null; + npcIds = null; + objectIds = null; + projectileIds = null; + graphicsObjectIds = null; + + if (colorOverrides != null) { + for (var override : colorOverrides) + override.clearIds(); + } + } + public ModelOverride copy() { return new ModelOverride( description, diff --git a/src/main/java/rs117/hd/utils/HDUtils.java b/src/main/java/rs117/hd/utils/HDUtils.java index 454645c72b..18e3345ef7 100644 --- a/src/main/java/rs117/hd/utils/HDUtils.java +++ b/src/main/java/rs117/hd/utils/HDUtils.java @@ -30,6 +30,8 @@ import java.io.BufferedReader; import java.io.FileReader; import java.lang.management.ManagementFactory; +import java.util.Comparator; +import java.util.List; import javax.annotation.Nullable; import javax.inject.Singleton; import javax.swing.JFrame; @@ -40,6 +42,7 @@ import rs117.hd.scene.areas.AABB; import rs117.hd.scene.areas.Area; import rs117.hd.scene.water_types.WaterType; +import rs117.hd.utils.collections.PooledArrayType; import static net.runelite.api.Constants.*; import static net.runelite.api.Constants.SCENE_SIZE; @@ -55,19 +58,30 @@ public final class HDUtils { public static final int HIDDEN_HSL = 12345678; public static final int UNDERWATER_HSL = 6676; + public static int fastVertex3Hash(int[] vPos) { + int hash = vPos[0]; + hash = 31 * hash + 44; + + hash = 31 * hash + vPos[1]; + hash = 31 * hash + 44; + + hash = 31 * hash + vPos[2]; + return 31 * hash + 44; + } + public static int fastVertexHash(int[] vPos) { int hash = 0; - for (int part : vPos) { - hash = 31 * hash + part; + for (int i = 0; i < vPos.length; i++) { + hash = 31 * hash + vPos[i]; hash = 31 * hash + ','; // preserve the comma separator effect } return hash; } - public static int[] calculateSurfaceNormals(int[] a, int[] b, int[] c) { + public static int[] calculateSurfaceNormals(int[] out, int[] a, int[] b, int[] c) { subtract(b, a, b); subtract(c, a, c); - return cross(b, c); + return cross(out, b, c); } public static int ceilPow2(int i) { @@ -494,6 +508,106 @@ public static boolean isBakedGroundShading(Model model, int face) { return heightA == heightB && heightA == heightC; } + public static String buildTable( + String header, + List columnNames, + List rowNames, + List> data + ) { + final int rows = data.size(); + final int cols = data.isEmpty() ? 0 : data.get(0).size(); + + // Compute column widths + final int[] colWidths = new int[cols]; + for (int c = 0; c < cols; c++) { + int max = 0; + + if (columnNames != null && c < columnNames.size()) + max = columnNames.get(c).length(); + + for (int r = 0; r < rows; r++) { + String cell = data.get(r).get(c); + if (cell != null) + max = Math.max(max, cell.length()); + } + + colWidths[c] = max; + } + + int rowNameWidth = 0; + if (rowNames != null) { + for (String name : rowNames) + rowNameWidth = Math.max(rowNameWidth, name.length()); + } + + // Calculate full table width + int totalWidth = 0; + + if (rowNames != null) + totalWidth += rowNameWidth + 3; // " | " + + for (int c = 0; c < cols; c++) { + totalWidth += colWidths[c]; + if (c < cols - 1) + totalWidth += 3; // " | " + } + + final StringBuilder sb = new StringBuilder(); + + if (header != null && !header.isEmpty()) { + sb.append(repeat("=", Math.max(header.length(), totalWidth))).append("\n"); + sb.append(header).append("\n"); + sb.append(repeat("=", Math.max(header.length(), totalWidth))).append("\n"); + } + + // Column headers + if (columnNames != null) { + if (rowNames != null) + sb.append(pad("", rowNameWidth)).append(" | "); + + for (int c = 0; c < cols; c++) { + sb.append(pad(columnNames.get(c), colWidths[c])); + if (c < cols - 1) sb.append(" | "); + } + sb.append("\n"); + } + + // Separator + if (columnNames != null) { + if (rowNames != null) + sb.append(repeat("-", rowNameWidth)).append("-+-"); + + for (int c = 0; c < cols; c++) { + sb.append(repeat("-", colWidths[c])); + if (c < cols - 1) sb.append("-+-"); + } + sb.append("\n"); + } + + // Data rows + for (int r = 0; r < rows; r++) { + if (rowNames != null) + sb.append(pad(rowNames.get(r), rowNameWidth)).append(" | "); + + for (int c = 0; c < cols; c++) { + sb.append(pad(data.get(r).get(c), colWidths[c])); + if (c < cols - 1) sb.append(" | "); + } + sb.append("\n"); + } + + return sb.toString(); + } + + private static String pad(String s, int width) { + if (s == null) s = ""; + return String.format("%-" + width + "s", s); + } + + private static String repeat(String s, int count) { + return s.repeat(Math.max(0, count)); + } + public static boolean isJFrameMinimized(@Nullable JFrame f) { return f != null && (f.getExtendedState() & Frame.ICONIFIED) != 0; } @@ -540,4 +654,82 @@ public static long getTotalSystemMemory() { return Long.MAX_VALUE; } } + + @SuppressWarnings("unchecked") + public static void quickSort(List list, Comparator comparator) { + final int size = list.size(); + final Object[] sortingBin = PooledArrayType.OBJECT.borrow(size); + try { + list.toArray(sortingBin); + quickSort(sortingBin, 0, size - 1, comparator); + + list.clear(); + for (int i = 0; i < size; i++) + list.add((T) sortingBin[i]); + } finally { + PooledArrayType.OBJECT.release(sortingBin); + } + } + + @SuppressWarnings({ "rawtypes" }) + public static void quickSort(Object[] a, Comparator comparator) { + quickSort(a, 0, a.length - 1, comparator); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static void quickSort( + Object[] a, + int left, + int right, + Comparator comparator + ) { + final int[] stack = PooledArrayType.INT.borrow(128); + try { + int top = 0; + + stack[top++] = left; + stack[top++] = right; + + while (top > 0) { + right = stack[--top]; + left = stack[--top]; + + while (left < right) { + int i = left; + int j = right; + + Object pivot = a[(left + right) >>> 1]; + + while (i <= j) { + while (comparator.compare(a[i], pivot) < 0) i++; + while (comparator.compare(a[j], pivot) > 0) j--; + + if (i <= j) { + Object tmp = a[i]; + a[i] = a[j]; + a[j] = tmp; + i++; + j--; + } + } + + if (j - left < right - i) { + if (i < right) { + stack[top++] = i; + stack[top++] = right; + } + right = j; + } else { + if (left < j) { + stack[top++] = left; + stack[top++] = j; + } + left = i; + } + } + } + } finally { + PooledArrayType.INT.release(stack); + } + } } diff --git a/src/main/java/rs117/hd/utils/MathUtils.java b/src/main/java/rs117/hd/utils/MathUtils.java index c72f2e1f48..3cfd6a9c7f 100644 --- a/src/main/java/rs117/hd/utils/MathUtils.java +++ b/src/main/java/rs117/hd/utils/MathUtils.java @@ -113,6 +113,10 @@ public static int[] copy(int[] v) { return Arrays.copyOf(v, v.length); } + public static short[] copy(short[] v) { + return Arrays.copyOf(v, v.length); + } + public static float[] copyTo(float[] out, @Nullable float[] in, int offset, int len) { if (in != null) { assert offset + len <= min(out.length, in.length); @@ -145,6 +149,25 @@ public static int[] ensureDefaults(@Nullable int[] in, int[] defaults) { return in != null && in.length == defaults.length ? in : copyTo(copy(defaults), in); } + public static boolean[] ensureCapacity(boolean[] in, int size) { + return in != null ? in.length >= size ? in : Arrays.copyOf(in, size) : new boolean[size]; + } + + public static float[] ensureCapacity(float[] in, int size) { + return in != null ? in.length >= size ? in : Arrays.copyOf(in, size) : new float[size]; + } + + public static int[] ensureCapacity(int[] in, int size) { + return in != null ? in.length >= size ? in : Arrays.copyOf(in, size) : new int[size]; + } + + @FunctionalInterface + public interface ArraySupplier { T[] get(int size); } + + public static T[] ensureCapacity(T[] in, int size, ArraySupplier supplier) { + return in != null ? in.length >= size ? in : Arrays.copyOf(in, size) : supplier.get(size); + } + public static int[] slice(int[] v, int offset) { return Arrays.copyOfRange(v, offset, v.length); } @@ -414,6 +437,22 @@ public static float dot(int... v) { return dot(v, v); } + public static float dot(short[] a, short[] b, int n) { + assert a.length >= n && b.length >= n; + float f = 0; + for (int i = 0; i < n; i++) + f += a[i] * b[i]; + return f; + } + + public static float dot(short[] a, short... b) { + return dot(a, b, min(a.length, b.length)); + } + + public static float dot(short... v) { + return dot(v, v); + } + public static int product(int... v) { int product = 1; for (int factor : v) @@ -835,6 +874,10 @@ public static float tan(float rad) { return (float) Math.tan(rad); } + public static short normShort(float f) { + return (short) round(clamp(f, -1, 1) * Short.MAX_VALUE); + } + public static int float16(float value) { if (value == 0) return 0; diff --git a/src/main/java/rs117/hd/utils/NpcDisplacementCache.java b/src/main/java/rs117/hd/utils/NpcDisplacementCache.java index aac087acd0..e1779d1027 100644 --- a/src/main/java/rs117/hd/utils/NpcDisplacementCache.java +++ b/src/main/java/rs117/hd/utils/NpcDisplacementCache.java @@ -38,12 +38,14 @@ public Entry reset() { private Set ANIM_ID_IGNORE_LIST = Collections.emptySet(); public void initialize() { - HashSet idsToIgnore = new HashSet<>(); - for (var substringToIgnore : ANIM_IGNORE_LIST) - for (var entry : gamevalManager.getAnims().entrySet()) - if (entry.getKey().contains(substringToIgnore)) - idsToIgnore.add(entry.getValue()); - ANIM_ID_IGNORE_LIST = Set.copyOf(idsToIgnore); + try(GamevalManager.Handle handle = gamevalManager.obtainHandle()) { + HashSet idsToIgnore = new HashSet<>(); + for (var substringToIgnore : ANIM_IGNORE_LIST) + for (var entry : handle.getAnims().entrySet()) + if (entry.getKey().contains(substringToIgnore)) + idsToIgnore.add(entry.getValue()); + ANIM_ID_IGNORE_LIST = Set.copyOf(idsToIgnore); + } } public void destroy() { diff --git a/src/main/java/rs117/hd/utils/buffer/GpuIntBuffer.java b/src/main/java/rs117/hd/utils/buffer/GpuIntBuffer.java index cdf5c6e79a..c7dec5569d 100644 --- a/src/main/java/rs117/hd/utils/buffer/GpuIntBuffer.java +++ b/src/main/java/rs117/hd/utils/buffer/GpuIntBuffer.java @@ -30,8 +30,6 @@ import org.lwjgl.system.MemoryUtil; import rs117.hd.HdPlugin; -import static rs117.hd.utils.MathUtils.*; - @Slf4j public class GpuIntBuffer { @Getter @@ -58,9 +56,8 @@ public GpuIntBuffer(IntBuffer buffer) { ownsBuffer = false; } - public GpuIntBuffer(GLMappedBuffer buffer) { - this.buffer = buffer.intView(); - ownsBuffer = false; + public GpuIntBuffer(boolean ownsBuffer) { + this.ownsBuffer = ownsBuffer; } public void destroy() { @@ -69,6 +66,12 @@ public void destroy() { buffer = null; } + public GpuIntBuffer setBuffer(GLMappedBuffer buffer) { + assert !ownsBuffer; + this.buffer = buffer.intView(); + return this; + } + @Override @SuppressWarnings("deprecation") protected void finalize() { @@ -106,60 +109,6 @@ public void put(IntBuffer buffer) { this.buffer.put(buffer); } - public static int normShort(float f) { - return round(clamp(f, -1, 1) * Short.MAX_VALUE); - } - - public int putFace( - int alphaBiasHslA, int alphaBiasHslB, int alphaBiasHslC, - int materialDataA, int materialDataB, int materialDataC, - int terrainDataA, int terrainDataB, int terrainDataC - ) { - return putFace( - buffer, - alphaBiasHslA, alphaBiasHslB, alphaBiasHslC, - materialDataA, materialDataB, materialDataC, - terrainDataA, terrainDataB, terrainDataC - ); - } - - public void putVertex( - int x, int y, int z, - float u, float v, float w, - int nx, int ny, int nz, - int textureFaceIdx - ) { - buffer.put((y & 0xFFFF) << 16 | x & 0xFFFF); - buffer.put(z & 0xFFFF); - buffer.put(float16(v) << 16 | float16(u)); - buffer.put(float16(w)); - // Unnormalized normals, assumed to be within short max - buffer.put((ny & 0xFFFF) << 16 | nx & 0xFFFF); - buffer.put(nz & 0xFFFF); - buffer.put(textureFaceIdx); - } - - public static int putFace( - IntBuffer buffer, - int alphaBiasHslA, int alphaBiasHslB, int alphaBiasHslC, - int materialDataA, int materialDataB, int materialDataC, - int terrainDataA, int terrainDataB, int terrainDataC - ) { - final int textureFaceIdx = buffer.position() / 3; - buffer.put(alphaBiasHslA); - buffer.put(alphaBiasHslB); - buffer.put(alphaBiasHslC); - - buffer.put(materialDataA); - buffer.put(materialDataB); - buffer.put(materialDataC); - - buffer.put(terrainDataA); // TODO: Remove? - buffer.put(terrainDataB); - buffer.put(terrainDataC); - return textureFaceIdx; - } - public int position() { return buffer.position(); } diff --git a/src/main/java/rs117/hd/utils/collections/Int2IntCache.java b/src/main/java/rs117/hd/utils/collections/Int2IntCache.java new file mode 100644 index 0000000000..5c30cab9f0 --- /dev/null +++ b/src/main/java/rs117/hd/utils/collections/Int2IntCache.java @@ -0,0 +1,235 @@ +package rs117.hd.utils.collections; + +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.collections.Util.DEFAULT_GROWTH; +import static rs117.hd.utils.collections.Util.EMPTY; +import static rs117.hd.utils.collections.Util.findIndex; +import static rs117.hd.utils.collections.Util.murmurHash3; + +public final class Int2IntCache { + private final int maxSize; + private final float growthFactor; + + private final StampedLock lock = new StampedLock(); + + 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); + + if (!lock.validate(stamp)) { + stamp = lock.readLock(); + try { + idx = findIndex(key, mask, keys, distances); + } 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 = 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/collections/Int2IntHashMap.java b/src/main/java/rs117/hd/utils/collections/Int2IntHashMap.java new file mode 100644 index 0000000000..874d995e9c --- /dev/null +++ b/src/main/java/rs117/hd/utils/collections/Int2IntHashMap.java @@ -0,0 +1,279 @@ +package rs117.hd.utils.collections; + +import java.util.Arrays; +import java.util.function.IntUnaryOperator; +import rs117.hd.utils.HDUtils; + +import static rs117.hd.utils.MathUtils.*; +import static rs117.hd.utils.collections.Util.DEFAULT_CAPACITY; +import static rs117.hd.utils.collections.Util.DEFAULT_GROWTH; +import static rs117.hd.utils.collections.Util.EMPTY; +import static rs117.hd.utils.collections.Util.LOAD_FACTOR; +import static rs117.hd.utils.collections.Util.findIndex; +import static rs117.hd.utils.collections.Util.murmurHash3; + +public final class Int2IntHashMap { + private final float growthFactor; + + private int[] keys; + private int[] values; + private int[] distances; + + private int lowTide = Integer.MAX_VALUE; + private int highTide; + 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); + } + + public int compute(int key, IntUnaryOperator op, int defaultValue) { + int idx = findIndex(key, mask, keys, distances); + if (idx >= 0) + return values[idx] = op.applyAsInt(values[idx]); + int newVal = op.applyAsInt(defaultValue); + put(key, newVal); + return newVal; + } + + public int or(int key, int maskBits, int defaultValue) { + int idx = findIndex(key, mask, keys, distances); + if (idx >= 0) + return values[idx] |= maskBits; + int newVal = defaultValue | maskBits; + put(key, newVal); + return newVal; + } + + public int and(int key, int maskBits, int defaultValue) { + int idx = findIndex(key, mask, keys, distances); + if (idx >= 0) + return values[idx] &= maskBits; + int newVal = defaultValue & maskBits; + put(key, newVal); + return newVal; + } + + public int xor(int key, int maskBits, int defaultValue) { + int idx = findIndex(key, mask, keys, distances); + if (idx >= 0) + return values[idx] ^= maskBits; + int newVal = defaultValue ^ maskBits; + put(key, newVal); + return newVal; + } + + public int setBits(int key, int valueBits, int maskBits, int defaultValue) { + int idx = findIndex(key, mask, keys, distances); + + if (idx >= 0) { + int v = values[idx]; + v = (v & ~maskBits) | (valueBits & maskBits); + values[idx] = v; + return v; + } + + int newVal = (defaultValue & ~maskBits) | (valueBits & maskBits); + put(key, newVal); + return newVal; + } + + public boolean test(int key, int maskBits) { + int idx = findIndex(key, mask, keys, distances); + return idx >= 0 && (values[idx] & maskBits) != 0; + } + + public int getBits(int key, int maskBits, int shift, int defaultValue) { + int idx = findIndex(key, mask, keys, distances); + return idx >= 0 ? (values[idx] >>> shift) & maskBits : defaultValue; + } + + 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++; + lowTide = min(idx, lowTide); + highTide = max(idx, highTide); + 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 find(Object key) { + return findIndex(key.hashCode(), mask, keys, distances); + } + + public int find(int key) { + return findIndex(key, mask, keys, distances); + } + + 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); + 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) >= 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); + 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() { + if(size == 0) + return; + Arrays.fill(keys, lowTide, highTide, EMPTY); + Arrays.fill(values, lowTide, highTide, 0); + Arrays.fill(distances, lowTide, highTide, 0); + lowTide = keys.length; + highTide = 0; + size = 0; + } + + public boolean isEmpty() { + return size == 0; + } + + public int size() { + return size; + } + + public int capacity() { return keys.length; } +} diff --git a/src/main/java/rs117/hd/utils/collections/Int2ObjectHashMap.java b/src/main/java/rs117/hd/utils/collections/Int2ObjectHashMap.java new file mode 100644 index 0000000000..c9315de900 --- /dev/null +++ b/src/main/java/rs117/hd/utils/collections/Int2ObjectHashMap.java @@ -0,0 +1,306 @@ +package rs117.hd.utils.collections; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.function.IntUnaryOperator; +import java.util.function.UnaryOperator; +import lombok.Getter; +import rs117.hd.utils.HDUtils; + +import static rs117.hd.utils.MathUtils.*; +import static rs117.hd.utils.collections.Util.DEFAULT_CAPACITY; +import static rs117.hd.utils.collections.Util.DEFAULT_GROWTH; +import static rs117.hd.utils.collections.Util.EMPTY; +import static rs117.hd.utils.collections.Util.LOAD_FACTOR; +import static rs117.hd.utils.collections.Util.findIndex; +import static rs117.hd.utils.collections.Util.murmurHash3; + +public final class Int2ObjectHashMap implements Iterable> { + public interface Supplier { T[] get(int capacity); } + + private final Supplier defaultValueSupplier; + private final float growthFactor; + + private int[] keys; + private T[] values; + private int[] distances; + + private int lowTide = Integer.MAX_VALUE; + private int highTide; + 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(Object key, T value) { return key != null && put(key.hashCode(), value); } + + public boolean put(int key, T value) { + return put(key, value, true); + } + + public boolean putIfAbsent(int key, T value) { + return put(key, value, false); + } + + public T compute(int key, UnaryOperator op, T defaultValue) { + int idx = findIndex(key, mask, keys, distances); + if (idx >= 0) + return values[idx] = op.apply(values[idx]); + T newVal = op.apply(defaultValue); + put(key, newVal); + return newVal; + } + + 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++; + lowTide = min(idx, lowTide); + highTide = max(idx, highTide); + 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); + 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); + 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) >= 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); + 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() { + if(size == 0) + return; + Arrays.fill(keys, lowTide, highTide, EMPTY); + Arrays.fill(values, lowTide, highTide, null); + Arrays.fill(distances, lowTide, highTide, 0); + lowTide = keys.length; + highTide = 0; + size = 0; + } + + public boolean isEmpty() { + return size == 0; + } + + public int size() { + return size; + } + + public int capacity() { return keys.length; } + + @Override + public Iterator> iterator() { + return new EntryIterator(); + } + + public static class Entry { + @Getter + private int key; + @Getter + private T value; + } + + private class EntryIterator implements Iterator> { + private int index = -1; + private int nextIndex = -1; + + private final Entry entry = new Entry<>(); + + 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(); + + entry.key = keys[index]; + entry.value = values[index]; + return entry; + } + + @Override + public void remove() { + if (index == -1) + throw new IllegalStateException(); + + removeIndex(index); + index = -1; + } + } +} diff --git a/src/main/java/rs117/hd/utils/collections/IntHashSet.java b/src/main/java/rs117/hd/utils/collections/IntHashSet.java new file mode 100644 index 0000000000..a043376755 --- /dev/null +++ b/src/main/java/rs117/hd/utils/collections/IntHashSet.java @@ -0,0 +1,247 @@ +package rs117.hd.utils.collections; + +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.collections.Util.DEFAULT_CAPACITY; +import static rs117.hd.utils.collections.Util.DEFAULT_GROWTH; +import static rs117.hd.utils.collections.Util.EMPTY; +import static rs117.hd.utils.collections.Util.LOAD_FACTOR; +import static rs117.hd.utils.collections.Util.findIndex; +import static rs117.hd.utils.collections.Util.murmurHash3; + +public final class IntHashSet implements Iterable { + private final float growthFactor; + + private int[] keys; + private int[] distances; + + private int lowTide = Integer.MAX_VALUE; + private int highTide; + 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); + } + } + } + + private void resizeTo(int newCapacity) { + if (newCapacity <= keys.length) + return; + + int cap = HDUtils.ceilPow2(newCapacity); + + final int[] oldKeys = keys; + keys = new int[cap]; + distances = new int[cap]; + Arrays.fill(keys, EMPTY); + + size = 0; + mask = cap - 1; + + for (int k : oldKeys) { + if (k != EMPTY) + add(k); + } + } + + public boolean addAll(Iterable collection) { + if (collection == null) + return false; + + boolean modified = false; + for (Integer val : collection) { + if (add(val)) + modified = true; + } + + return modified; + } + + public boolean addAll(IntHashSet other) { + if (other == null || other.size == 0) + return false; + + if (size + other.size >= keys.length * LOAD_FACTOR) + resizeTo(HDUtils.ceilPow2( (int) ((size + other.size) / LOAD_FACTOR))); + + boolean modified = false; + for (int i = 0; i < other.keys.length; i++) { + int k = other.keys[i]; + if (k != EMPTY && add(k)) + modified = true; + } + + return modified; + } + + 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++; + lowTide = min(idx, lowTide); + highTide = max(idx, highTide); + 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) >= 0; + } + + public boolean remove(Object key) { return key != null && remove(key.hashCode()); } + + public boolean remove(int key) { + int idx = findIndex(key, mask, keys, distances); + 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() { + if(size == 0) + return; + Arrays.fill(keys, lowTide, highTide, EMPTY); + Arrays.fill(distances, lowTide, highTide, 0); + size = 0; + } + + public boolean isEmpty() { + return size == 0; + } + + public int size() { return size; } + + public int capacity() { return keys.length; } + + @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/collections/PooledArrayType.java b/src/main/java/rs117/hd/utils/collections/PooledArrayType.java new file mode 100644 index 0000000000..27b4d8b8b9 --- /dev/null +++ b/src/main/java/rs117/hd/utils/collections/PooledArrayType.java @@ -0,0 +1,382 @@ +package rs117.hd.utils.collections; + +import java.lang.reflect.Array; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.StampedLock; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import rs117.hd.utils.HDUtils; +import rs117.hd.utils.Props; + +import static java.lang.Integer.numberOfLeadingZeros; +import static rs117.hd.utils.MathUtils.*; + +@Slf4j +public enum PooledArrayType { + BOOL(boolean[]::new, 1), + BYTE(byte[]::new, 1), + CHAR(char[]::new, 2), + SHORT(short[]::new, 2), + INT(int[]::new, 4), + FLOAT(float[]::new, 4), + OBJECT(Object[]::new, 4); + + public static final PooledArrayType[] VALUES = values(); + + private static final double MAX_HEAP_FRACTION = 0.05; // 768 MB * 0.05 = 38.4 MB + private static final long MAX_POOL_BYTES = (long) (Runtime.getRuntime().maxMemory() * MAX_HEAP_FRACTION); + + private static final int MAX_BUCKET = 30; + private static final int STRIPES = 8; + private static final int STRIPES_MASK = STRIPES - 1; + private static final int CLEANUP_INTERVAL = 64; + private static final long SHRINK_DELAY_MS = 60_000; + private static final double ALPHA = 0.1; + + private static final AtomicLong CURRENT_POOL_BYTES = new AtomicLong(); + + public final ArraySupplier supplier; + public final int stride; + + private final Bucket[][] buckets = new Bucket[MAX_BUCKET + 1][STRIPES]; + + PooledArrayType(ArraySupplier supplier, int stride) { + this.supplier = supplier; + this.stride = stride; + + for (int i = 0; i < buckets.length; i++) { + int size = 1 << i; + for (int s = 0; s < STRIPES; s++) + buckets[i][s] = new Bucket(size); + } + } + + private static int ceilPow2(int x) { + if (x <= 1) return 1; + if (x > (1 << 30)) return Integer.MAX_VALUE; + return 1 << (32 - numberOfLeadingZeros(x - 1)); + } + + private static int bucket(int size) { + if (size <= 1) return 0; + int b = 32 - numberOfLeadingZeros(size - 1); + return min(b, MAX_BUCKET); + } + + private static int stripeIndex() { + final int hash = Thread.currentThread().hashCode(); + return (hash ^ (hash >>> 16)) & STRIPES_MASK; + } + + private static boolean isPoolFull(long additionalBytes) { + return CURRENT_POOL_BYTES.get() + additionalBytes > MAX_POOL_BYTES; + } + + public static void forceCleanup() { + long startCacheSize = getCurrentTotalCacheSize(); + for(int v = 0; v < VALUES.length; v++) { + final PooledArrayType type = VALUES[v]; + for (int b = 0; b < type.buckets.length; b++) { + for (int s = 0; s < STRIPES; s++) { + final Bucket bucket = type.buckets[b][s]; + final long stamp = bucket.lock.writeLock(); + try { + bucket.opCounter = 0; + type.maybeCleanup(b, s, bucket, true); + } finally { + bucket.lock.unlockWrite(stamp); + } + } + } + } + long endCacheSize = getCurrentTotalCacheSize(); + long diff = endCacheSize - startCacheSize; + log.debug("PooledArrayType - Prev: {} New: {} Diff: {}", formatBytes(startCacheSize), formatBytes(endCacheSize), (diff < 0 ? "-" : "") + formatBytes(abs(diff))); + } + + public static void printDetailedStats() { + final List columnNames = new ArrayList<>(); + final List> data = new ArrayList<>(); + + columnNames.add("Type"); + columnNames.add("Items"); + columnNames.add("Allocated"); + + for (int v = 0; v < VALUES.length; v++) { + final PooledArrayType type = VALUES[v]; + final List row = new ArrayList<>(); + row.add(type.name()); + row.add(String.valueOf(type.getElementCount())); + row.add(formatBytes(type.getCurrentCacheSize())); + data.add(row); + } + + log.debug("\n{}", + HDUtils.buildTable( + "PooledArrayType stats (" + formatBytes(getCurrentTotalCacheSize()) + ")", + columnNames, + null, + data + ) + ); + } + + public static void shutdown() { + CURRENT_POOL_BYTES.set(0); + + for (int v = 0; v < VALUES.length; v++) { + final PooledArrayType type = VALUES[v]; + for (int b = 0; b < VALUES[v].buckets.length; b++) { + for (int s = 0; s < STRIPES; s++) { + final Bucket bucket = type.buckets[b][s]; + bucket.stack.clear(); + bucket.isEmpty = true; + + bucket.inUse = 0; + bucket.peakInUse = 0; + bucket.avgDemand = 0; + bucket.lastOverTargetTime = 0; + } + } + } + } + + public static long getCurrentTotalCacheSize() { + return CURRENT_POOL_BYTES.get(); + } + + public int getElementCount() { + int size = 0; + for (int b = 0; b < buckets.length; b++) { + for (int s = 0; s < STRIPES; s++) + size += buckets[b][s].stack.size(); + } + return size; + } + + public long getCurrentCacheSize() { + long size = 0; + for (int b = 0; b < buckets.length; b++) { + for (int s = 0; s < STRIPES; s++) { + Bucket bucket = buckets[b][s]; + size += (long) bucket.stack.size() * bucket.size; + } + } + return size * stride; + } + + private void maybeCleanup(int b, int s, Bucket bucket) { + maybeCleanup(b, s, bucket, false); + } + + private void maybeCleanup(int b, int s, Bucket bucket, boolean forced) { + if (!forced && (++bucket.opCounter & (CLEANUP_INTERVAL - 1)) != 0) + return; + + bucket.avgDemand = (float) (ALPHA * bucket.peakInUse + (1 - ALPHA) * bucket.avgDemand); + bucket.peakInUse = bucket.inUse; + + if (bucket.stack.size() <= bucket.avgDemand) { + bucket.lastOverTargetTime = 0; + return; + } + + final long now = System.currentTimeMillis(); + if (bucket.lastOverTargetTime == 0) { + bucket.lastOverTargetTime = now; + return; + } + + if (!forced && now - bucket.lastOverTargetTime <= SHRINK_DELAY_MS) + return; + + final int target = max((int) (bucket.avgDemand * 0.5f), 1); + int excess = bucket.stack.size() - target; + + while (excess-- > 0) { + Object arr = bucket.poll(bytesFor(bucket.size)); + if (arr == null) + break; + + spill(b, s, arr); + } + + bucket.lastOverTargetTime = now; + } + + private boolean spill(int b, int fromStripe, Object array) { + final Bucket[] stripes = buckets[b]; + + final long bytes = bytesFor(Array.getLength(array)); + + for (int i = 1; i < STRIPES; i++) { + final int s = (fromStripe + i) & STRIPES_MASK; + final Bucket other = stripes[s]; + + if (other.stack.size() > other.avgDemand) + continue; + + final long stamp = other.lock.tryWriteLock(); + if (stamp == 0) + continue; + + try { + if (other.stack.size() <= other.avgDemand) { + other.add(array, bytes); + return true; + } + } finally { + other.lock.unlockWrite(stamp); + } + } + + return false; + } + + private long bytesFor(int len) { + return (long) len * stride; + } + + @SuppressWarnings("unchecked") + public T ensureCapacity(Object array, int requestedSize) { + final int len = array != null ? Array.getLength(array) : 0; + if (len >= requestedSize) + return (T) array; + + release(array); + return borrow(requestedSize); + } + + @SuppressWarnings("SuspiciousSystemArraycopy") + public T cache(Object array, int offset, int size) { + final T cached = borrow(size); + System.arraycopy(array, offset, cached, 0, size); + return cached; + } + + @SuppressWarnings("unchecked") + public T borrow(int requestedSize) { + final int roundedSize = ceilPow2(requestedSize); + final int b = bucket(roundedSize); + + if (b < 0 || b >= buckets.length) + return (T) supplier.get(requestedSize); + + final long bytes = bytesFor(roundedSize); + final Bucket[] bucketStripes = buckets[b]; + final int startStripe = stripeIndex(); + + for (int i = 0; i < STRIPES * 2; i++) { + final int s = (startStripe + i) & STRIPES_MASK; + final Bucket bucket = bucketStripes[s]; + if (bucket.isEmpty) + continue; + + final long stamp = + i < STRIPES + ? bucket.lock.tryWriteLock() + : bucket.lock.writeLock(); + + if (stamp == 0) + continue; + + try { + final T arr = (T) bucket.poll(bytes); + if (arr != null) { + bucket.inUse++; + bucket.peakInUse = Math.max(bucket.peakInUse, bucket.inUse); + maybeCleanup(b, s, bucket); + return arr; + } + } finally { + bucket.lock.unlockWrite(stamp); + } + } + + return (T) supplier.get(roundedSize); + } + + public void release(Object array) { + if (array == null) + return; + + final int len = Array.getLength(array); + if (len != ceilPow2(len)) + return; + + final int b = bucket(len); + if (b < 0 || b >= buckets.length) + return; + + final long bytes = bytesFor(len); + if (isPoolFull(bytes)) + return; + + final int startStripe = stripeIndex(); + + final Bucket[] bucketStripes = buckets[b]; + + for (int i = 0; i < STRIPES * 2; i++) { + final int s = (startStripe + i) & STRIPES_MASK; + final Bucket bucket = bucketStripes[s]; + + final long stamp = + i < STRIPES + ? bucket.lock.tryWriteLock() + : bucket.lock.writeLock(); + if (stamp == 0) + continue; + + try { + if (isPoolFull(bytes)) + return; + + bucket.inUse = max(0, bucket.inUse - 1); + bucket.add(array, bytes); + maybeCleanup(b, s, bucket); + return; + } finally { + bucket.lock.unlockWrite(stamp); + } + } + } + + @FunctionalInterface + public interface ArraySupplier { + T get(int capacity); + } + + @RequiredArgsConstructor + private static final class Bucket { + private final ArrayDeque stack = new ArrayDeque<>(); + private final StampedLock lock = new StampedLock(); + + private final int size; + private int opCounter; + private int inUse; + private int peakInUse; + private float avgDemand; + private long lastOverTargetTime; + + private volatile boolean isEmpty = true; + + public void add(Object array, long bytes) { + if (Props.DEVELOPMENT && !isEmpty && stack.contains(array)) + throw new IllegalStateException("Duplicate array: " + array); + stack.add(array); + CURRENT_POOL_BYTES.addAndGet(bytes); + isEmpty = false; + } + + public Object poll(long bytes) { + Object arr = stack.poll(); + if (arr != null) + CURRENT_POOL_BYTES.addAndGet(-bytes); + isEmpty = stack.isEmpty(); + return arr; + } + } +} \ No newline at end of file diff --git a/src/main/java/rs117/hd/utils/collections/PooledObjectArray.java b/src/main/java/rs117/hd/utils/collections/PooledObjectArray.java new file mode 100644 index 0000000000..6faa149dc1 --- /dev/null +++ b/src/main/java/rs117/hd/utils/collections/PooledObjectArray.java @@ -0,0 +1,20 @@ +package rs117.hd.utils.collections; + +@SuppressWarnings("unchecked") +public final class PooledObjectArray { + public Object[] array; + + public T get(int idx) { return (T) array[idx]; } + + public void set(int idx, T value) { array[idx] = value; } + + public void ensureCapacity(int size) { + array = PooledArrayType.OBJECT.ensureCapacity(array, size); + } + + public void release() { + if(array != null) + PooledArrayType.OBJECT.release(array); + array = null; + } +} diff --git a/src/main/java/rs117/hd/utils/collections/PrimitiveCharArray.java b/src/main/java/rs117/hd/utils/collections/PrimitiveCharArray.java new file mode 100644 index 0000000000..41acccacdd --- /dev/null +++ b/src/main/java/rs117/hd/utils/collections/PrimitiveCharArray.java @@ -0,0 +1,46 @@ +package rs117.hd.utils.collections; + +import java.util.Arrays; + +import static java.lang.System.arraycopy; +import static rs117.hd.utils.HDUtils.ceilPow2; + +public final class PrimitiveCharArray { + public char[] array = new char[16]; + public int length; + + public PrimitiveCharArray reset() { + length = 0; + return this; + } + + public PrimitiveCharArray ensureCapacity(int count) { + if (length + count > array.length) + array = Arrays.copyOf(array, ceilPow2(length + count)); + return this; + } + + public void put(char i) { + if (length < array.length) + array[length++] = i; + } + + public void put(char[] ints, int offset, int count) { + ensureCapacity(count); + arraycopy(ints, offset, array, length, count); + length += count; + } + + public void removeAt(int idx) { + if (idx < 0 || idx >= length) + return; + arraycopy(array, idx + 1, array, idx, length - idx - 1); + length--; + } + + public void removeAtSwap(int idx) { + if (idx < 0 || idx >= length) + return; + array[idx] = array[--length]; + } +} diff --git a/src/main/java/rs117/hd/utils/collections/Util.java b/src/main/java/rs117/hd/utils/collections/Util.java new file mode 100644 index 0000000000..ed7c7d4009 --- /dev/null +++ b/src/main/java/rs117/hd/utils/collections/Util.java @@ -0,0 +1,43 @@ +package rs117.hd.utils.collections; + +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 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) { + 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) + return idx; + + idx = (idx + 1) & mask; + } + + return -1; + } +}