From 663db311e3e591ca905b60602ab2e22a79a52af9 Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:39:01 +0100 Subject: [PATCH 01/10] Client & Memory Allocation Tracking * Added Client Timer to track time spent outside of the plugin * Added Allocation Tracking (Not completely accurate since GCs can occur) --- src/main/java/rs117/hd/HdPlugin.java | 2 + .../java/rs117/hd/overlays/FrameTimer.java | 19 +++- .../rs117/hd/overlays/FrameTimerOverlay.java | 103 +++++++++++++++--- .../java/rs117/hd/overlays/FrameTimings.java | 4 +- src/main/java/rs117/hd/overlays/Timer.java | 1 + .../renderer/zone/ModelStreamingManager.java | 14 ++- .../rs117/hd/renderer/zone/ZoneRenderer.java | 11 +- src/main/java/rs117/hd/utils/HDUtils.java | 5 + src/main/java/rs117/hd/utils/MathUtils.java | 25 ++++- 9 files changed, 155 insertions(+), 29 deletions(-) diff --git a/src/main/java/rs117/hd/HdPlugin.java b/src/main/java/rs117/hd/HdPlugin.java index 9db332e72d..4bb0bad9a6 100644 --- a/src/main/java/rs117/hd/HdPlugin.java +++ b/src/main/java/rs117/hd/HdPlugin.java @@ -1991,6 +1991,8 @@ public void onBeforeRender(BeforeRender beforeRender) { return; } + frameTimer.end(Timer.CLIENT); + if (lastFrameTimeMillis > 0) { deltaTime = (float) ((System.currentTimeMillis() - lastFrameTimeMillis) / 1000.); diff --git a/src/main/java/rs117/hd/overlays/FrameTimer.java b/src/main/java/rs117/hd/overlays/FrameTimer.java index 6e1ea4a3cd..de6669f943 100644 --- a/src/main/java/rs117/hd/overlays/FrameTimer.java +++ b/src/main/java/rs117/hd/overlays/FrameTimer.java @@ -14,6 +14,7 @@ import net.runelite.client.callback.ClientThread; import org.lwjgl.opengl.*; import rs117.hd.HdPlugin; +import rs117.hd.utils.HDUtils; import static org.lwjgl.opengl.GL33C.*; @@ -40,6 +41,8 @@ public class FrameTimer { private final AutoTimer[] autoTimers = new AutoTimer[NUM_TIMERS]; private final boolean[] activeTimers = new boolean[NUM_TIMERS]; private final long[] timings = new long[NUM_TIMERS]; + private final long[] memoryUsage = new long[NUM_TIMERS]; + private final long[] allocations = new long[NUM_TIMERS]; private final int[] gpuQueries = new int[NUM_TIMERS * 2]; private final ArrayDeque glDebugGroupStack = new ArrayDeque<>(NUM_GPU_DEBUG_GROUPS); private final ArrayDeque listeners = new ArrayDeque<>(); @@ -136,6 +139,7 @@ public void removeAllListeners() { public void reset() { Arrays.fill(timings, 0); + Arrays.fill(allocations, 0); Arrays.fill(activeTimers, false); cumulativeError = 0; } @@ -161,6 +165,7 @@ public AutoTimer begin(Timer timer) { } else if (!activeTimers[index]) { cumulativeError += errorCompensation + 1 >> 1; timings[index] -= System.nanoTime() - cumulativeError; + memoryUsage[index] = HDUtils.getUsedMemory(); } activeTimers[index] = true; @@ -185,15 +190,25 @@ public void end(Timer timer) { glQueryCounter(gpuQueries[timer.ordinal() * 2 + 1], GL_TIMESTAMP); // leave the GPU timer active, since it needs to be gathered at a later point } else { + long allocated = HDUtils.getUsedMemory() - memoryUsage[timer.ordinal()]; cumulativeError += errorCompensation >> 1; timings[timer.ordinal()] += System.nanoTime() - cumulativeError; activeTimers[timer.ordinal()] = false; + allocations[timer.ordinal()] += allocated > 0 ? allocated : 0; } } public void add(Timer timer, long nanos) { - if (isActive) + if (isActive) { timings[timer.ordinal()] += nanos; + } + } + + public void add(Timer timer, long nanos, long allocation) { + if (isActive) { + timings[timer.ordinal()] += nanos; + allocations[timer.ordinal()] += allocation > 0 ? allocation : 0; + } } public void endFrameAndReset() { @@ -234,7 +249,7 @@ public void endFrameAndReset() { } final float cpuLoad = (float) osBean.getSystemLoadAverage() / osBean.getAvailableProcessors(); - var frameTimings = new FrameTimings(frameEndTimestamp, timings, cpuLoad); + var frameTimings = new FrameTimings(frameEndTimestamp, timings, allocations, cpuLoad); for (var listener : listeners) listener.onFrameCompletion(frameTimings); diff --git a/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java b/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java index d596fc9efb..1d6a5be0de 100644 --- a/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java +++ b/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java @@ -3,6 +3,7 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import java.awt.Dimension; +import java.awt.FontMetrics; import java.awt.Graphics2D; import java.util.ArrayDeque; import java.util.Arrays; @@ -21,6 +22,7 @@ import rs117.hd.renderer.zone.WorldViewContext; import rs117.hd.renderer.zone.ZoneRenderer; import rs117.hd.utils.FrameTimingsRecorder; +import rs117.hd.utils.MathUtils; import rs117.hd.utils.NpcDisplacementCache; import rs117.hd.utils.jobs.JobSystem; @@ -52,11 +54,16 @@ public class FrameTimerOverlay extends OverlayPanel implements FrameTimer.Listen private final ArrayDeque frames = new ArrayDeque<>(); private final long[] timings = new long[Timer.TIMERS.length]; - private float cpuLoad; - private final Map componentMap = new HashMap<>(); + private final long[] allocations = new long[Timer.TIMERS.length]; + private final Map timerComponentMap = new HashMap<>(); private final StringBuilder sb = new StringBuilder(); private final Formatter formatter = new Formatter(sb); + private char[] strChars = new char[128]; + private String longestTimerName; + private float cpuLoad; + private Graphics2D graphics; + @Inject public FrameTimerOverlay(HdPlugin plugin) { super(plugin); @@ -95,6 +102,8 @@ public void onFrameCompletion(FrameTimings timings) { @Override public Dimension render(Graphics2D g) { + this.graphics = g; + long time = System.nanoTime(); var boldFont = FontManager.getRunescapeBoldFont(); @@ -105,19 +114,26 @@ public Dimension render(Graphics2D g) { .build()); } else { long cpuTime = timings[Timer.DRAW_FRAME.ordinal()]; + long cpuAlloc = allocations[Timer.DRAW_FRAME.ordinal()]; long asyncCpuTime = 0; - addTiming("CPU", cpuTime, true); + addTiming("CPU", cpuTime, cpuAlloc, true); for (var t : Timer.TIMERS) { + if(t == Timer.CLIENT) + continue; if (t.isCpuTimer() && t != Timer.DRAW_FRAME) - addTiming(t, timings); + addTiming(t, timings, allocations); if (t.isAsyncCpuTimer()) asyncCpuTime += timings[t.ordinal()]; } + long clientTime = timings[Timer.CLIENT.ordinal()]; + long clientAlloc = allocations[Timer.CLIENT.ordinal()]; + addTiming("Client", clientTime, clientAlloc, true); addTiming("Async", asyncCpuTime, true); for (var t : Timer.TIMERS) if (t.isAsyncCpuTimer()) - addTiming(t, timings); + addTiming(t, timings, allocations); + if (cpuLoad > 0) { children.add(LineComponent.builder() @@ -130,7 +146,7 @@ public Dimension render(Graphics2D g) { addTiming("GPU", gpuTime, true); for (var t : Timer.TIMERS) if (t.isGpuTimer() && t != Timer.RENDER_FRAME) - addTiming(t, timings); + addTiming(t, timings, null); children.add(LineComponent.builder() .leftFont(boldFont) @@ -267,48 +283,99 @@ private boolean getAverageTimings() { if (frames.isEmpty()) return false; + if(longestTimerName == null) { + longestTimerName = Timer.CLIENT.name; + for (var timer : Timer.TIMERS) { + if(timer.name.length() > longestTimerName.length()) + longestTimerName = timer.name; + } + } + Arrays.fill(timings, 0); + Arrays.fill(allocations, 0); cpuLoad = 0; for (var frame : frames) { - for (int i = 0; i < frame.timers.length; i++) + for (int i = 0; i < frame.timers.length; i++) { timings[i] += frame.timers[i]; + allocations[i] += frame.allocations[i]; + } cpuLoad += frame.cpuLoad; } - for (int i = 0; i < timings.length; i++) + for (int i = 0; i < timings.length; i++) { timings[i] = max(0, timings[i] / frames.size()); + allocations[i] = max(0, allocations[i] / frames.size()); + } cpuLoad /= frames.size(); return true; } - private void addTiming(Timer timer, long[] timings) { - addTiming(timer.name, timings[timer.ordinal()], false); + private void addTiming(Timer timer, long[] timings, long[] allocations) { + addTiming(timer.name, timings[timer.ordinal()], allocations != null ? allocations[timer.ordinal()] : -1, false); } + private void addTiming(String name, long nanos, boolean bold) { + addTiming(name, nanos, -1, bold); + } + + private int stringWidth(String str, FontMetrics fmt) { + int len = str.length(); + if(strChars == null || strChars.length < len) + strChars = new char[len]; + str.getChars(0, len, strChars, 0); + return fmt.charsWidth(strChars, 0, len); + } + + private void addTiming(String name, long nanos, long allocation, boolean bold) { if (nanos == 0) return; + final var font = bold ? FontManager.getRunescapeBoldFont() : FontManager.getRunescapeFont(); + final var metrics = graphics.getFontMetrics(font); // Round timers to zero if they are less than a microsecond off - String result = "~0 ms"; + sb.setLength(0); + + String value = "~0 ms"; if (abs(nanos) > 1e3) { sb.setLength(0); - result = sb.append(round(nanos / 1e3) / 1e3).append(" ms").toString(); + value = sb.append(round(nanos / 1e3) / 1e3).append(" ms").toString(); + } + + String title = name; + if(allocation >= 0) { + sb.setLength(0); + sb.append(name).append(": "); + + int spaceWidth = stringWidth(" ", metrics); + int lineWidth = stringWidth(name, metrics); + int padding = (stringWidth(longestTimerName, metrics) - lineWidth) / spaceWidth; + + // This is 2:15 AM Coding... Very hacky this certainly can be done better. But the core idea is here :D + padding += Math.max(0, ((getBounds().width / 6) / spaceWidth)); + if(bold) + padding = (int) (padding * 0.915f); + + for(int i = 0; i < padding; i++) + sb.append(" "); + + sb.append(" ("); + title = MathUtils.formatBytes(allocation, sb).append(") ").toString(); } - LineComponent component = componentMap.get(name); + LineComponent component = timerComponentMap.get(name); if (component == null) { - var font = bold ? FontManager.getRunescapeBoldFont() : FontManager.getRunescapeFont(); component = LineComponent.builder() - .left(name + ":") + .left(title) .leftFont(font) - .right(result) + .right(value) .rightFont(font) .build(); - componentMap.put(name, component); + timerComponentMap.put(name, component); } else { - component.setRight(result); + component.setLeft(title); + component.setRight(value); } panelComponent.getChildren().add(component); diff --git a/src/main/java/rs117/hd/overlays/FrameTimings.java b/src/main/java/rs117/hd/overlays/FrameTimings.java index c56aa0f55b..e831983fe9 100644 --- a/src/main/java/rs117/hd/overlays/FrameTimings.java +++ b/src/main/java/rs117/hd/overlays/FrameTimings.java @@ -5,11 +5,13 @@ public class FrameTimings { public final long frameTimestamp; public final long[] timers; + public final long[] allocations; public final float cpuLoad; - public FrameTimings(long frameTimestamp, long[] timers, float cpuLoad) { + public FrameTimings(long frameTimestamp, long[] timers, long[] allocations, float cpuLoad) { this.frameTimestamp = frameTimestamp; this.timers = Arrays.copyOf(timers, timers.length); + this.allocations = Arrays.copyOf(allocations, allocations.length); this.cpuLoad = cpuLoad; } } diff --git a/src/main/java/rs117/hd/overlays/Timer.java b/src/main/java/rs117/hd/overlays/Timer.java index 5f998a8651..7acee6b223 100644 --- a/src/main/java/rs117/hd/overlays/Timer.java +++ b/src/main/java/rs117/hd/overlays/Timer.java @@ -27,6 +27,7 @@ public enum Timer { DRAW_SUBMIT, // Miscellaneous + CLIENT, SWAP_BUFFERS, EXECUTE_COMMAND_BUFFER, MAP_UI_BUFFER("Map UI Buffer"), diff --git a/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java b/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java index cf745b83b3..5f9bab931d 100644 --- a/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java +++ b/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java @@ -261,6 +261,7 @@ private void uploadTempModelAsync( int x, int y, int z ) { final long asyncStart = System.nanoTime(); + final long asyncUsed = HDUtils.getUsedMemory(); uploadTempModel( projection, ctx, @@ -275,7 +276,11 @@ private void uploadTempModelAsync( orientation, x, y, z ); - frameTimer.add(Timer.DRAW_TEMP_ASYNC, System.nanoTime() - asyncStart); + frameTimer.add( + Timer.DRAW_TEMP_ASYNC, + System.nanoTime() - asyncStart, + HDUtils.getUsedMemory() - asyncUsed + ); } private void uploadTempModel( @@ -526,6 +531,7 @@ private void uploadDynamicModelAsync( int x, int y, int z ) { final long asyncStart = System.nanoTime(); + final long asyncUsed = HDUtils.getUsedMemory(); uploadDynamicModel( ctx, projection, @@ -540,7 +546,11 @@ private void uploadDynamicModelAsync( orientation, x, y, z ); - frameTimer.add(Timer.DRAW_DYNAMIC_ASYNC, System.nanoTime() - asyncStart); + frameTimer.add( + Timer.DRAW_DYNAMIC_ASYNC, + System.nanoTime() - asyncStart, + HDUtils.getUsedMemory() - asyncUsed + ); } private void uploadDynamicModel( diff --git a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java index 13d42c32a6..dc47ea20b1 100644 --- a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java +++ b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java @@ -1019,12 +1019,17 @@ public void drawDynamic( return; final long start = System.nanoTime(); + final long startUsed = HDUtils.getUsedMemory(); try { modelStreamingManager.drawDynamic(renderThreadId, projection, scene, tileObject, r, m, orient, x, y, z); } catch (Exception ex) { log.error("Error in drawDynamic:", ex); } finally { - frameTimer.add(renderThreadId == -1 ? Timer.DRAW_DYNAMIC : Timer.DRAW_DYNAMIC_ASYNC, System.nanoTime() - start); + frameTimer.add( + renderThreadId == -1 ? Timer.DRAW_DYNAMIC : Timer.DRAW_DYNAMIC_ASYNC, + System.nanoTime() - start, + HDUtils.getUsedMemory() - startUsed + ); } } @@ -1055,6 +1060,9 @@ public void draw(int overlayColor) { return; } + if(!shouldRenderScene) + frameTimer.end(Timer.CLIENT); + try { plugin.prepareInterfaceTexture(); } catch (Exception ex) { @@ -1131,6 +1139,7 @@ public void draw(int overlayColor) { glBindFramebuffer(GL_FRAMEBUFFER, plugin.awtContext.getFramebuffer(false)); frameTimer.endFrameAndReset(); + frameTimer.begin(Timer.CLIENT); checkGLErrors(); shouldRenderScene = false; diff --git a/src/main/java/rs117/hd/utils/HDUtils.java b/src/main/java/rs117/hd/utils/HDUtils.java index 5b7487294e..b3f106b4ee 100644 --- a/src/main/java/rs117/hd/utils/HDUtils.java +++ b/src/main/java/rs117/hd/utils/HDUtils.java @@ -532,6 +532,11 @@ public static String getCpuName() { return "Unknown"; } + public static long getUsedMemory() { + final Runtime runtime = Runtime.getRuntime(); + return runtime.totalMemory() - runtime.freeMemory(); + } + public static long getTotalSystemMemory() { try { var bean = ManagementFactory.getOperatingSystemMXBean(); diff --git a/src/main/java/rs117/hd/utils/MathUtils.java b/src/main/java/rs117/hd/utils/MathUtils.java index c72f2e1f48..1eeab8a7f2 100644 --- a/src/main/java/rs117/hd/utils/MathUtils.java +++ b/src/main/java/rs117/hd/utils/MathUtils.java @@ -880,14 +880,29 @@ public static int float16(float value) { } public static String formatBytes(long bytes) { - if (bytes < 0) - return "-" + formatBytes(bytes == Long.MIN_VALUE ? Long.MAX_VALUE : -bytes); + StringBuilder sb = new StringBuilder(); + formatBytes(bytes, sb); + return sb.toString(); + } + + public static StringBuilder formatBytes(long bytes, StringBuilder sb) { + if (bytes < 0) { + sb.append('-'); + return formatBytes(bytes == Long.MIN_VALUE ? Long.MAX_VALUE : -bytes, sb); + } + if (bytes == Long.MAX_VALUE) - return "infinity"; + return sb.append("infinity"); + if (bytes < 1024) - return bytes + " B"; + return sb.append(bytes).append(" B"); + int i = (63 - Long.numberOfLeadingZeros(bytes)) / 10 - 1; int decimal = (10 * (int) (bytes >> i * 10) / 1024) % 10; - return String.format("%d%s %siB", bytes >> (i + 1) * 10, decimal == 0 ? "" : "." + decimal, "KMGTPE".charAt(i)); + + sb.append(bytes >> (i + 1) * 10); + if(decimal > 0) + sb.append('.').append(decimal); + return sb.append(' ').append("KMGTPE".charAt(i)); } } From 6ae3230137efd6bec9be66c36472841e4726d112 Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:02:08 +0100 Subject: [PATCH 02/10] Use Thread Memory Tracking instead of Runtime, otherwise Async execution bleeds into tracking --- src/main/java/rs117/hd/HdPlugin.java | 2 ++ src/main/java/rs117/hd/utils/HDUtils.java | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/main/java/rs117/hd/HdPlugin.java b/src/main/java/rs117/hd/HdPlugin.java index 4bb0bad9a6..7c7add606a 100644 --- a/src/main/java/rs117/hd/HdPlugin.java +++ b/src/main/java/rs117/hd/HdPlugin.java @@ -676,6 +676,8 @@ protected void startUp() { } } + HDUtils.setupThreadAllocatedBytesMonitoring(); + updateCachedConfigs(); developerTools.activate(); diff --git a/src/main/java/rs117/hd/utils/HDUtils.java b/src/main/java/rs117/hd/utils/HDUtils.java index b3f106b4ee..00a576e6b2 100644 --- a/src/main/java/rs117/hd/utils/HDUtils.java +++ b/src/main/java/rs117/hd/utils/HDUtils.java @@ -532,7 +532,28 @@ public static String getCpuName() { return "Unknown"; } + private static boolean THREAD_ALLOCATED_BYTES_SUPPORTED = true; + private static com.sun.management.ThreadMXBean threadMXBean; + + public static void setupThreadAllocatedBytesMonitoring() { + if(ManagementFactory.getThreadMXBean() instanceof com.sun.management.ThreadMXBean) { + threadMXBean = (com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean(); + THREAD_ALLOCATED_BYTES_SUPPORTED = threadMXBean.isThreadAllocatedMemorySupported(); + if(THREAD_ALLOCATED_BYTES_SUPPORTED) { + threadMXBean.setThreadAllocatedMemoryEnabled(true); + THREAD_ALLOCATED_BYTES_SUPPORTED = threadMXBean.isThreadAllocatedMemoryEnabled(); + } + } else { + THREAD_ALLOCATED_BYTES_SUPPORTED = false; + } + + log.debug("Thread allocated bytes monitoring: {}", THREAD_ALLOCATED_BYTES_SUPPORTED); + } + public static long getUsedMemory() { + if(THREAD_ALLOCATED_BYTES_SUPPORTED) + return threadMXBean.getThreadAllocatedBytes(Thread.currentThread().getId()); + final Runtime runtime = Runtime.getRuntime(); return runtime.totalMemory() - runtime.freeMemory(); } From 13abf17f1f76b1d9a040c64dd4dd3ead6de8d5a1 Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:23:52 +0100 Subject: [PATCH 03/10] Add getTimeStamp & getUsedMemory, which are gaurded behind isActive. This prevents unnecessary execution when the frameTimer isn't enabled --- src/main/java/rs117/hd/HdPlugin.java | 4 ++-- .../java/rs117/hd/overlays/FrameTimer.java | 19 +++++++++++++++---- .../renderer/zone/ModelStreamingManager.java | 18 +++++++----------- .../renderer/zone/StaticAlphaSortingJob.java | 5 +++-- .../rs117/hd/renderer/zone/ZoneRenderer.java | 8 ++++---- 5 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/main/java/rs117/hd/HdPlugin.java b/src/main/java/rs117/hd/HdPlugin.java index 7c7add606a..d614e16708 100644 --- a/src/main/java/rs117/hd/HdPlugin.java +++ b/src/main/java/rs117/hd/HdPlugin.java @@ -1520,9 +1520,9 @@ public void prepareInterfaceTexture() { .build( "AsyncUICopy", t -> { - long start = System.nanoTime(); + long start = frameTimer.getTimeStamp(); pbo.mapped().intView().put(pixels, 0, uiWidth * uiHeight); - frameTimer.add(Timer.COPY_UI_ASYNC, System.nanoTime() - start); + frameTimer.add(Timer.COPY_UI_ASYNC, start); } ) .setExecuteAsync(!isPowerSaving) diff --git a/src/main/java/rs117/hd/overlays/FrameTimer.java b/src/main/java/rs117/hd/overlays/FrameTimer.java index de6669f943..913c9b6459 100644 --- a/src/main/java/rs117/hd/overlays/FrameTimer.java +++ b/src/main/java/rs117/hd/overlays/FrameTimer.java @@ -198,15 +198,26 @@ public void end(Timer timer) { } } - public void add(Timer timer, long nanos) { + public long getTimeStamp() { return isActive ? System.nanoTime() : 0; } + + public long getUsedMemory() { return isActive ? HDUtils.getUsedMemory() : 0; } + + public void addDuration(Timer timer, long nanos) { if (isActive) { timings[timer.ordinal()] += nanos; } } - public void add(Timer timer, long nanos, long allocation) { + public void add(Timer timer, long startNanos) { if (isActive) { - timings[timer.ordinal()] += nanos; + timings[timer.ordinal()] += System.nanoTime() - startNanos; + } + } + + public void add(Timer timer, long startNanos, long startMemory) { + if (isActive) { + long allocation = HDUtils.getUsedMemory() - startMemory; + timings[timer.ordinal()] += System.nanoTime() - startNanos; allocations[timer.ordinal()] += allocation > 0 ? allocation : 0; } } @@ -274,6 +285,6 @@ private void trackGarbageCollection() { plugin.garbageCollectionCount += gc.getCollectionCount(); } - add(Timer.GARBAGE_COLLECTION, elapsedDuration * 1_000_000L); + addDuration(Timer.GARBAGE_COLLECTION, elapsedDuration * 1_000_000L); } } diff --git a/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java b/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java index 5f9bab931d..4663550a8e 100644 --- a/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java +++ b/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java @@ -260,8 +260,8 @@ private void uploadTempModelAsync( int orientation, int x, int y, int z ) { - final long asyncStart = System.nanoTime(); - final long asyncUsed = HDUtils.getUsedMemory(); + final long asyncStart = frameTimer.getTimeStamp(); + final long asyncUsed = frameTimer.getUsedMemory(); uploadTempModel( projection, ctx, @@ -276,11 +276,7 @@ private void uploadTempModelAsync( orientation, x, y, z ); - frameTimer.add( - Timer.DRAW_TEMP_ASYNC, - System.nanoTime() - asyncStart, - HDUtils.getUsedMemory() - asyncUsed - ); + frameTimer.add(Timer.DRAW_TEMP_ASYNC, asyncStart, asyncUsed); } private void uploadTempModel( @@ -530,8 +526,8 @@ private void uploadDynamicModelAsync( int orientation, int x, int y, int z ) { - final long asyncStart = System.nanoTime(); - final long asyncUsed = HDUtils.getUsedMemory(); + final long asyncStart = frameTimer.getTimeStamp(); + final long asyncUsed = frameTimer.getUsedMemory(); uploadDynamicModel( ctx, projection, @@ -548,8 +544,8 @@ private void uploadDynamicModelAsync( ); frameTimer.add( Timer.DRAW_DYNAMIC_ASYNC, - System.nanoTime() - asyncStart, - HDUtils.getUsedMemory() - asyncUsed + asyncStart, + asyncUsed ); } diff --git a/src/main/java/rs117/hd/renderer/zone/StaticAlphaSortingJob.java b/src/main/java/rs117/hd/renderer/zone/StaticAlphaSortingJob.java index fb885b14c3..56fdcaadc0 100644 --- a/src/main/java/rs117/hd/renderer/zone/StaticAlphaSortingJob.java +++ b/src/main/java/rs117/hd/renderer/zone/StaticAlphaSortingJob.java @@ -58,7 +58,8 @@ public void reset() { @Override protected void onRun() { - long start = System.nanoTime(); + long start = frameTimer.getTimeStamp(); + long used = frameTimer.getUsedMemory(); try (FacePrioritySorter sorter = FacePrioritySorter.POOL.acquire()) { for (int i = 0; i < size; i++) { if (!states.compareAndSet(i, 0, 1)) @@ -66,7 +67,7 @@ protected void onRun() { processModel(sorter, models[i]); } } - frameTimer.add(Timer.STATIC_ALPHA_SORT, System.nanoTime() - start); + frameTimer.add(Timer.STATIC_ALPHA_SORT, start, used); } private void processModel(FacePrioritySorter sorter, AlphaModel m) { diff --git a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java index dc47ea20b1..89eda83f31 100644 --- a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java +++ b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java @@ -1018,8 +1018,8 @@ public void drawDynamic( if (plugin.isPluginStopPending()) return; - final long start = System.nanoTime(); - final long startUsed = HDUtils.getUsedMemory(); + final long start = frameTimer.getTimeStamp(); + final long used = frameTimer.getUsedMemory(); try { modelStreamingManager.drawDynamic(renderThreadId, projection, scene, tileObject, r, m, orient, x, y, z); } catch (Exception ex) { @@ -1027,8 +1027,8 @@ public void drawDynamic( } finally { frameTimer.add( renderThreadId == -1 ? Timer.DRAW_DYNAMIC : Timer.DRAW_DYNAMIC_ASYNC, - System.nanoTime() - start, - HDUtils.getUsedMemory() - startUsed + start, + used ); } } From 60ae34529a68dd224d1173056fa95a15bedd3020 Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:42:31 +0100 Subject: [PATCH 04/10] Client shouldn't use Thread memory tracking, it should track total memory usage --- src/main/java/rs117/hd/HdPlugin.java | 12 +++++++++--- src/main/java/rs117/hd/overlays/FrameTimer.java | 8 ++++---- src/main/java/rs117/hd/utils/HDUtils.java | 4 ++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/rs117/hd/HdPlugin.java b/src/main/java/rs117/hd/HdPlugin.java index d614e16708..5d80eb64c7 100644 --- a/src/main/java/rs117/hd/HdPlugin.java +++ b/src/main/java/rs117/hd/HdPlugin.java @@ -393,6 +393,7 @@ public class HdPlugin extends Plugin { public UBOLights uboLightsCulling; // Configs used frequently enough to be worth caching + public boolean configLegacyBrightness; public boolean configGroundTextures; public boolean configGroundBlending; public boolean configModelTextures; @@ -418,6 +419,8 @@ public class HdPlugin extends Plugin { public boolean configTiledLighting; public boolean configTiledLightingImageLoadStore; public int configDetailDrawDistance; + public int configDrawDistance; + public int configBrightness; public DynamicLights configDynamicLights; public ShadowMode configShadowMode; public SeasonalTheme configSeasonalTheme; @@ -1646,6 +1649,8 @@ private void updateCachedConfigs() { configShadowMode = config.shadowMode(); configShadowsEnabled = configShadowMode != ShadowMode.OFF; configRoofShadows = config.roofShadows(); + configLegacyBrightness = config.useLegacyBrightness(); + configBrightness = config.brightness(); configGroundTextures = config.groundTextures(); configGroundBlending = config.groundBlending(); configModelTextures = config.modelTextures(); @@ -1661,6 +1666,7 @@ private void updateCachedConfigs() { configTiledLighting = config.tiledLighting(); configTiledLightingImageLoadStore = config.tiledLightingImageLoadStore(); configDetailDrawDistance = config.detailDrawDistance(); + configDrawDistance = config.drawDistance(); configExpandShadowDraw = config.expandShadowDraw(); configUseFasterModelHashing = config.fasterModelHashing(); configZoneStreaming = config.zoneStreaming(); @@ -1965,13 +1971,13 @@ private float[] getDpiScaling() { } public int getDrawDistance() { - return clamp(config.drawDistance(), 0, MAX_DISTANCE); + return clamp(configDrawDistance, 0, MAX_DISTANCE); } public float getGammaCorrection() { - if (config.useLegacyBrightness()) + if (configLegacyBrightness) return 1; - return 100f / config.brightness(); + return 100f / configBrightness; } public int getExpandedMapLoadingChunks() { diff --git a/src/main/java/rs117/hd/overlays/FrameTimer.java b/src/main/java/rs117/hd/overlays/FrameTimer.java index 913c9b6459..17185b8e12 100644 --- a/src/main/java/rs117/hd/overlays/FrameTimer.java +++ b/src/main/java/rs117/hd/overlays/FrameTimer.java @@ -165,7 +165,7 @@ public AutoTimer begin(Timer timer) { } else if (!activeTimers[index]) { cumulativeError += errorCompensation + 1 >> 1; timings[index] -= System.nanoTime() - cumulativeError; - memoryUsage[index] = HDUtils.getUsedMemory(); + memoryUsage[index] = HDUtils.getUsedMemory(timer != Timer.CLIENT); } activeTimers[index] = true; @@ -190,7 +190,7 @@ public void end(Timer timer) { glQueryCounter(gpuQueries[timer.ordinal() * 2 + 1], GL_TIMESTAMP); // leave the GPU timer active, since it needs to be gathered at a later point } else { - long allocated = HDUtils.getUsedMemory() - memoryUsage[timer.ordinal()]; + long allocated = HDUtils.getUsedMemory(timer != Timer.CLIENT) - memoryUsage[timer.ordinal()]; cumulativeError += errorCompensation >> 1; timings[timer.ordinal()] += System.nanoTime() - cumulativeError; activeTimers[timer.ordinal()] = false; @@ -200,7 +200,7 @@ public void end(Timer timer) { public long getTimeStamp() { return isActive ? System.nanoTime() : 0; } - public long getUsedMemory() { return isActive ? HDUtils.getUsedMemory() : 0; } + public long getUsedMemory() { return isActive ? HDUtils.getUsedMemory(true) : 0; } public void addDuration(Timer timer, long nanos) { if (isActive) { @@ -216,7 +216,7 @@ public void add(Timer timer, long startNanos) { public void add(Timer timer, long startNanos, long startMemory) { if (isActive) { - long allocation = HDUtils.getUsedMemory() - startMemory; + long allocation = HDUtils.getUsedMemory(true) - startMemory; timings[timer.ordinal()] += System.nanoTime() - startNanos; allocations[timer.ordinal()] += allocation > 0 ? allocation : 0; } diff --git a/src/main/java/rs117/hd/utils/HDUtils.java b/src/main/java/rs117/hd/utils/HDUtils.java index 00a576e6b2..5cadd7f911 100644 --- a/src/main/java/rs117/hd/utils/HDUtils.java +++ b/src/main/java/rs117/hd/utils/HDUtils.java @@ -550,8 +550,8 @@ public static void setupThreadAllocatedBytesMonitoring() { log.debug("Thread allocated bytes monitoring: {}", THREAD_ALLOCATED_BYTES_SUPPORTED); } - public static long getUsedMemory() { - if(THREAD_ALLOCATED_BYTES_SUPPORTED) + public static long getUsedMemory(boolean useThreadTracking) { + if(THREAD_ALLOCATED_BYTES_SUPPORTED && useThreadTracking) return threadMXBean.getThreadAllocatedBytes(Thread.currentThread().getId()); final Runtime runtime = Runtime.getRuntime(); From b7a7d4311e9490e0348f39f44569167cd479d46a Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:48:32 +0100 Subject: [PATCH 05/10] Fix Are hiding allocating due to new Int[] --- src/main/java/rs117/hd/renderer/zone/SceneManager.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/rs117/hd/renderer/zone/SceneManager.java b/src/main/java/rs117/hd/renderer/zone/SceneManager.java index f7258f3620..76cfea461c 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneManager.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneManager.java @@ -103,6 +103,7 @@ public class SceneManager { private ZoneSceneContext nextSceneContext; private Zone[][] nextZones; private final List sortedZones = new ArrayList<>(); + private final int[] playerWorldPos = new int[3]; private boolean reloadRequested; public boolean isZoneStreamingEnabled() { @@ -250,15 +251,16 @@ private void updateAreaHiding() { if (!isTopLevelValid() || localPlayer == null || root.isLoading) return; - var lp = localPlayer.getLocalLocation(); if (root.sceneContext.enableAreaHiding) { var base = root.sceneContext.sceneBase; assert base != null; - int[] worldPos = { + var lp = localPlayer.getLocalLocation(); + final var worldPos = ivec3( + playerWorldPos, base[0] + lp.getSceneX(), base[1] + lp.getSceneY(), base[2] + client.getTopLevelWorldView().getPlane() - }; + ); // We need to check all areas contained in the scene in the order they appear in the list, // in order to ensure lower floors can take precedence over higher floors which include tiny From 97fc2263322500168202d5363135e5ff0f8c346a Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:57:00 +0100 Subject: [PATCH 06/10] Avoid casting Integer -> Int -> Integer --- src/main/java/rs117/hd/renderer/zone/SceneManager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/rs117/hd/renderer/zone/SceneManager.java b/src/main/java/rs117/hd/renderer/zone/SceneManager.java index 76cfea461c..31ea8c32d3 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneManager.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneManager.java @@ -232,15 +232,15 @@ public void update() { } } - for (int objectId : root.sceneContext.animatedDynamicObjectIds) { - int impostorId = objectId; + for (Integer objectId : root.sceneContext.animatedDynamicObjectIds) { + Integer impostorId = objectId; var def = client.getObjectDefinition(objectId); if (def != null && def.getImpostorIds() != null) { var impostor = def.getImpostor(); if (impostor != null) impostorId = impostor.getId(); } - root.sceneContext.animatedDynamicObjectImpostors.put(objectId, impostorId); + root.sceneContext.animatedDynamicObjectImpostors.putIfAbsent(objectId, impostorId); } root.completeInvalidation(); From 46dd1de37806d6793fedc0dd7c87e499ef0d1117 Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:57:17 +0100 Subject: [PATCH 07/10] Disbale garbage collection tracking when frameTimers are disabled --- src/main/java/rs117/hd/overlays/FrameTimer.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/rs117/hd/overlays/FrameTimer.java b/src/main/java/rs117/hd/overlays/FrameTimer.java index 17185b8e12..be2ff5432d 100644 --- a/src/main/java/rs117/hd/overlays/FrameTimer.java +++ b/src/main/java/rs117/hd/overlays/FrameTimer.java @@ -268,6 +268,9 @@ public void endFrameAndReset() { } private void trackGarbageCollection() { + if(!isActive) + return; + List garbageCollectors = ManagementFactory.getGarbageCollectorMXBeans(); if (lastGCTimes == null || lastGCTimes.length != garbageCollectors.size()) lastGCTimes = new long[garbageCollectors.size()]; From f5cb2f37db4bedd432793e11380df7fd0be2acd0 Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:20:20 +0100 Subject: [PATCH 08/10] VecCache to avoid allocating Int[]/Float[] Vecs --- src/main/java/rs117/hd/opengl/GLState.java | 42 ++++ .../rs117/hd/renderer/zone/SceneManager.java | 23 +- .../rs117/hd/renderer/zone/ZoneRenderer.java | 233 ++++++++++-------- .../rs117/hd/scene/EnvironmentManager.java | 39 +-- src/main/java/rs117/hd/utils/MathUtils.java | 106 ++++++++ 5 files changed, 312 insertions(+), 131 deletions(-) diff --git a/src/main/java/rs117/hd/opengl/GLState.java b/src/main/java/rs117/hd/opengl/GLState.java index 12d531589d..1a59e10827 100644 --- a/src/main/java/rs117/hd/opengl/GLState.java +++ b/src/main/java/rs117/hd/opengl/GLState.java @@ -107,6 +107,27 @@ public final void set(int... v) { System.arraycopy(v, 0, value, 0, v.length); } + public final void set(int a, int b) { + hasValue = true; + value[0] = a; + value[1] = b; + } + + public final void set(int a, int b, int c) { + hasValue = true; + value[0] = a; + value[1] = b; + value[2] = c; + } + + public final void set(int a, int b, int c, int d) { + hasValue = true; + value[0] = a; + value[1] = b; + value[2] = c; + value[3] = d; + } + @Override protected void internalApply() { if (!hasApplied || !Arrays.equals(value, appliedValue)) { @@ -133,6 +154,27 @@ public final void set(boolean... v) { System.arraycopy(v, 0, value, 0, v.length); } + public final void set(boolean a, boolean b) { + hasValue = true; + value[0] = a; + value[1] = b; + } + + public final void set(boolean a, boolean b, boolean c) { + hasValue = true; + value[0] = a; + value[1] = b; + value[2] = c; + } + + public final void set(boolean a, boolean b, boolean c, boolean d) { + hasValue = true; + value[0] = a; + value[1] = b; + value[2] = c; + value[3] = d; + } + @Override protected void internalApply() { if (!hasApplied || !Arrays.equals(value, appliedValue)) { diff --git a/src/main/java/rs117/hd/renderer/zone/SceneManager.java b/src/main/java/rs117/hd/renderer/zone/SceneManager.java index 31ea8c32d3..2f6d5e78e0 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneManager.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneManager.java @@ -103,7 +103,6 @@ public class SceneManager { private ZoneSceneContext nextSceneContext; private Zone[][] nextZones; private final List sortedZones = new ArrayList<>(); - private final int[] playerWorldPos = new int[3]; private boolean reloadRequested; public boolean isZoneStreamingEnabled() { @@ -248,20 +247,22 @@ public void update() { private void updateAreaHiding() { Player localPlayer = client.getLocalPlayer(); - if (!isTopLevelValid() || localPlayer == null || root.isLoading) + if (!isTopLevelValid() || localPlayer == null || root.isLoading || !root.sceneContext.enableAreaHiding) { + plugin.justChangedArea = false; return; + } + + var base = root.sceneContext.sceneBase; + assert base != null; + var lp = localPlayer.getLocalLocation(); - if (root.sceneContext.enableAreaHiding) { - var base = root.sceneContext.sceneBase; - assert base != null; - var lp = localPlayer.getLocalLocation(); - final var worldPos = ivec3( - playerWorldPos, + try ( + var worldPosHandle = ivec3( base[0] + lp.getSceneX(), base[1] + lp.getSceneY(), base[2] + client.getTopLevelWorldView().getPlane() - ); - + )) { + final int[] worldPos = worldPosHandle.data(); // We need to check all areas contained in the scene in the order they appear in the list, // in order to ensure lower floors can take precedence over higher floors which include tiny // portions of the floor beneath around stairs and ladders @@ -294,8 +295,6 @@ private void updateAreaHiding() { } else { plugin.justChangedArea = false; } - } else { - plugin.justChangedArea = false; } } diff --git a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java index 89eda83f31..f6e7658f10 100644 --- a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java +++ b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java @@ -343,10 +343,15 @@ private void preSceneDrawTopLevel( plugin.drawnTempRenderableCount = 0; plugin.drawnDynamicRenderableCount = 0; - copyTo(plugin.cameraPosition, vec(cameraX, cameraY, cameraZ)); - copyTo(plugin.cameraOrientation, vec(cameraYaw, cameraPitch)); - - copyTo(plugin.cameraFocalPoint, ivec((int) client.getCameraFocalPointX(), (int) client.getCameraFocalPointZ())); + try( + var cameraPosition = vec3(cameraX, cameraY, cameraZ); + var cameraOrientation = vec2(cameraYaw, cameraPitch); + var cameraFocalPoint = ivec2((int) client.getCameraFocalPointX(), (int) client.getCameraFocalPointZ()); + ) { + copyTo(plugin.cameraPosition, cameraPosition.data()); + copyTo(plugin.cameraOrientation, cameraOrientation.data()); + copyTo(plugin.cameraFocalPoint, cameraFocalPoint.data()); + } Arrays.fill(plugin.cameraShift, 0); float zoom = client.get3dZoom(); @@ -399,70 +404,75 @@ private void preSceneDrawTopLevel( (hasSceneCameraChanged || hasDirectionalCameraChanged) && !sceneCamera.isOrthographic() ) { - int shadowDrawDistance = 90 * LOCAL_TILE_SIZE; + try ( + var sceneCenterHandle = vec3(); + var directionalFwdHandle = vec3(); + ) { + int shadowDrawDistance = 90 * LOCAL_TILE_SIZE; - final float[][] volumeCorners = directionalShadowCasterVolume - .build(sceneCamera, drawDistance * LOCAL_TILE_SIZE, shadowDrawDistance); + final float[][] volumeCorners = directionalShadowCasterVolume + .build(sceneCamera, drawDistance * LOCAL_TILE_SIZE, shadowDrawDistance); - final float[] sceneCenter = new float[3]; - for (float[] corner : volumeCorners) - add(sceneCenter, sceneCenter, corner); - divide(sceneCenter, sceneCenter, (float) volumeCorners.length); + final float[] sceneCenter = sceneCenterHandle.data(); + for (float[] corner : volumeCorners) + add(sceneCenter, sceneCenter, corner); + divide(sceneCenter, sceneCenter, (float) volumeCorners.length); - // Reset position before transforming points - directionalCamera.setPosition(0, 0, 0); + // Reset position before transforming points + directionalCamera.setPosition(0, 0, 0); - float minX = Float.POSITIVE_INFINITY, maxX = Float.NEGATIVE_INFINITY; - float minY = Float.POSITIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY; - float minZ = Float.POSITIVE_INFINITY, maxZ = Float.NEGATIVE_INFINITY; - float radius = 0f; - for (float[] corner : volumeCorners) { - radius = max(radius, distance(sceneCenter, corner)); + float minX = Float.POSITIVE_INFINITY, maxX = Float.NEGATIVE_INFINITY; + float minY = Float.POSITIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY; + float minZ = Float.POSITIVE_INFINITY, maxZ = Float.NEGATIVE_INFINITY; + float radius = 0f; + for (float[] corner : volumeCorners) { + radius = max(radius, distance(sceneCenter, corner)); - directionalCamera.transformPoint(corner, corner); + directionalCamera.transformPoint(corner, corner); - minX = min(minX, corner[0]); - maxX = max(maxX, corner[0]); + minX = min(minX, corner[0]); + maxX = max(maxX, corner[0]); - minY = min(minY, corner[1]); - maxY = max(maxY, corner[1]); + minY = min(minY, corner[1]); + maxY = max(maxY, corner[1]); - minZ = min(minZ, corner[2]); - maxZ = max(maxZ, corner[2]); - } + minZ = min(minZ, corner[2]); + maxZ = max(maxZ, corner[2]); + } - // Offset the Directional Camera by the radius of the scene - float[] directionalFwd = directionalCamera.getForwardDirection(); - multiply(directionalFwd, directionalFwd, radius); - add(sceneCenter, sceneCenter, directionalFwd); - - // Calculate directional size from the AABB of the scene frustum corners - // Then snap to the nearest multiple of `LOCAL_HALF_TILE_SIZE` to prevent shimmering - int directionalSize = (int) max(abs(maxY - minY), abs(maxX - minX), abs(maxZ - minZ)); - directionalSize = Math.round(directionalSize / (float) LOCAL_HALF_TILE_SIZE) * LOCAL_HALF_TILE_SIZE; - directionalSize = max(8000, directionalSize); // Clamp the size to prevent going too small at reduced draw distances - - // Ignore directional size changes below the change threshold to avoid inducing shimmering - int previousDirectionalSize = directionalCamera.getViewportWidth(); - float changeThreshold = previousDirectionalSize * 0.05f; // 10% of the previous directional size - if (abs(directionalSize - previousDirectionalSize) < changeThreshold) - directionalSize = previousDirectionalSize; - - // Snap Position to Shadow Texel Grid to prevent shimmering - directionalCamera.transformPoint(sceneCenter, sceneCenter); - - float texelSize = (float) directionalSize / plugin.shadowMapResolution; - sceneCenter[0] = (float) floor(sceneCenter[0] / texelSize + 0.5f) * texelSize; - sceneCenter[1] = (float) floor(sceneCenter[1] / texelSize + 0.5f) * texelSize; - - directionalCamera.setPosition(directionalCamera.inverseTransformPoint(sceneCenter, sceneCenter)); - directionalCamera.setNearPlane(Math.max(0.1f, radius * 0.05f)); - directionalCamera.setFarPlane(radius * 2.0f); - directionalCamera.setZoom(1.0f); - directionalCamera.setViewportWidth(directionalSize); - directionalCamera.setViewportHeight(directionalSize); - - plugin.uboGlobal.lightProjectionMatrix.set(directionalCamera.getViewProjMatrix()); + // Offset the Directional Camera by the radius of the scene + final float[] directionalFwd = directionalCamera.getForwardDirection(directionalFwdHandle.data()); + multiply(directionalFwd, directionalFwd, radius); + add(sceneCenter, sceneCenter, directionalFwd); + + // Calculate directional size from the AABB of the scene frustum corners + // Then snap to the nearest multiple of `LOCAL_HALF_TILE_SIZE` to prevent shimmering + int directionalSize = (int) max(abs(maxY - minY), abs(maxX - minX), abs(maxZ - minZ)); + directionalSize = Math.round(directionalSize / (float) LOCAL_HALF_TILE_SIZE) * LOCAL_HALF_TILE_SIZE; + directionalSize = max(8000, directionalSize); // Clamp the size to prevent going too small at reduced draw distances + + // Ignore directional size changes below the change threshold to avoid inducing shimmering + int previousDirectionalSize = directionalCamera.getViewportWidth(); + float changeThreshold = previousDirectionalSize * 0.05f; // 10% of the previous directional size + if (abs(directionalSize - previousDirectionalSize) < changeThreshold) + directionalSize = previousDirectionalSize; + + // Snap Position to Shadow Texel Grid to prevent shimmering + directionalCamera.transformPoint(sceneCenter, sceneCenter); + + float texelSize = (float) directionalSize / plugin.shadowMapResolution; + sceneCenter[0] = (float) floor(sceneCenter[0] / texelSize + 0.5f) * texelSize; + sceneCenter[1] = (float) floor(sceneCenter[1] / texelSize + 0.5f) * texelSize; + + directionalCamera.setPosition(directionalCamera.inverseTransformPoint(sceneCenter, sceneCenter)); + directionalCamera.setNearPlane(Math.max(0.1f, radius * 0.05f)); + directionalCamera.setFarPlane(radius * 2.0f); + directionalCamera.setZoom(1.0f); + directionalCamera.setViewportWidth(directionalSize); + directionalCamera.setViewportHeight(directionalSize); + + plugin.uboGlobal.lightProjectionMatrix.set(directionalCamera.getViewProjMatrix()); + } } shouldDrawRoofShadows = @@ -481,29 +491,35 @@ private void preSceneDrawTopLevel( assert ctx.sceneContext.numVisibleLights <= UBOLights.MAX_LIGHTS; frameTimer.begin(Timer.UPDATE_LIGHTS); - final float[] lightPosition = new float[4]; - final float[] lightColor = new float[4]; - for (int i = 0; i < ctx.sceneContext.numVisibleLights; i++) { - final Light light = ctx.sceneContext.lights.get(i); - final float lightRadiusSq = light.radius * light.radius; - lightPosition[0] = light.pos[0] + plugin.cameraShift[0]; - lightPosition[1] = light.pos[1]; - lightPosition[2] = light.pos[2] + plugin.cameraShift[1]; - lightPosition[3] = lightRadiusSq; - - lightColor[0] = light.color[0] * light.strength; - lightColor[1] = light.color[1] * light.strength; - lightColor[2] = light.color[2] * light.strength; - lightColor[3] = 0.0f; - - plugin.uboLights.setLight(i, lightPosition, lightColor); - - if (plugin.configTiledLighting) { - // Pre-calculate the view space position of the light, to save having to do the multiplication in the culling shader - lightPosition[3] = 1.0f; - Mat4.mulVec(lightPosition, plugin.viewMatrix, lightPosition); - lightPosition[3] = lightRadiusSq; // Restore lightRadiusSq - plugin.uboLightsCulling.setLight(i, lightPosition, lightColor); + try ( + var lightPositionHandle = vec4(); + var lightColorHandle = vec4() + ) { + final float[] lightPosition = lightPositionHandle.data(); + final float[] lightColor = lightColorHandle.data(); + + for (int i = 0; i < ctx.sceneContext.numVisibleLights; i++) { + final Light light = ctx.sceneContext.lights.get(i); + final float lightRadiusSq = light.radius * light.radius; + lightPosition[0] = light.pos[0] + plugin.cameraShift[0]; + lightPosition[1] = light.pos[1]; + lightPosition[2] = light.pos[2] + plugin.cameraShift[1]; + lightPosition[3] = lightRadiusSq; + + lightColor[0] = light.color[0] * light.strength; + lightColor[1] = light.color[1] * light.strength; + lightColor[2] = light.color[2] * light.strength; + lightColor[3] = 0.0f; + + plugin.uboLights.setLight(i, lightPosition, lightColor); + + if (plugin.configTiledLighting) { + // Pre-calculate the view space position of the light, to save having to do the multiplication in the culling shader + lightPosition[3] = 1.0f; + Mat4.mulVec(lightPosition, plugin.viewMatrix, lightPosition); + lightPosition[3] = lightRadiusSq; // Restore lightRadiusSq + plugin.uboLightsCulling.setLight(i, lightPosition, lightColor); + } } } @@ -536,28 +552,37 @@ private void preSceneDrawTopLevel( plugin.uboGlobal.expandedMapLoadingChunks.set(ctx.sceneContext.expandedMapLoadingChunks); plugin.uboGlobal.colorBlindnessIntensity.set(config.colorBlindnessIntensity() / 100.f); - float[] waterColorHsv = ColorUtils.srgbToHsv(environmentManager.currentWaterColor); - float lightBrightnessMultiplier = 0.8f; - float midBrightnessMultiplier = 0.45f; - float darkBrightnessMultiplier = 0.05f; - float[] waterColorLight = ColorUtils.linearToSrgb(ColorUtils.hsvToSrgb(new float[] { - waterColorHsv[0], - waterColorHsv[1], - waterColorHsv[2] * lightBrightnessMultiplier - })); - float[] waterColorMid = ColorUtils.linearToSrgb(ColorUtils.hsvToSrgb(new float[] { - waterColorHsv[0], - waterColorHsv[1], - waterColorHsv[2] * midBrightnessMultiplier - })); - float[] waterColorDark = ColorUtils.linearToSrgb(ColorUtils.hsvToSrgb(new float[] { - waterColorHsv[0], - waterColorHsv[1], - waterColorHsv[2] * darkBrightnessMultiplier - })); - plugin.uboGlobal.waterColorLight.set(waterColorLight); - plugin.uboGlobal.waterColorMid.set(waterColorMid); - plugin.uboGlobal.waterColorDark.set(waterColorDark); + try ( + var waterColorLightHandle = vec3(); + var waterColorMidHandle = vec3(); + var waterColorDarkHandle = vec3(); + ) { + float[] waterColorHsv = ColorUtils.srgbToHsv(environmentManager.currentWaterColor); // TODO: Pass in a vec3 + float lightBrightnessMultiplier = 0.8f; + float midBrightnessMultiplier = 0.45f; + float darkBrightnessMultiplier = 0.05f; + float[] waterColorLight = ColorUtils.linearToSrgb(ColorUtils.hsvToSrgb(vec3( + waterColorLightHandle.data(), + waterColorHsv[0], + waterColorHsv[1], + waterColorHsv[2] * lightBrightnessMultiplier + ))); + float[] waterColorMid = ColorUtils.linearToSrgb(ColorUtils.hsvToSrgb(vec3( + waterColorMidHandle.data(), + waterColorHsv[0], + waterColorHsv[1], + waterColorHsv[2] * midBrightnessMultiplier + ))); + float[] waterColorDark = ColorUtils.linearToSrgb(ColorUtils.hsvToSrgb(vec3( + waterColorDarkHandle.data(), + waterColorHsv[0], + waterColorHsv[1], + waterColorHsv[2] * darkBrightnessMultiplier + ))); + plugin.uboGlobal.waterColorLight.set(waterColorLight); + plugin.uboGlobal.waterColorMid.set(waterColorMid); + plugin.uboGlobal.waterColorDark.set(waterColorDark); + } plugin.uboGlobal.gammaCorrection.set(plugin.getGammaCorrection()); float ambientStrength = environmentManager.currentAmbientStrength; diff --git a/src/main/java/rs117/hd/scene/EnvironmentManager.java b/src/main/java/rs117/hd/scene/EnvironmentManager.java index b19eb18f64..c9d57272df 100644 --- a/src/main/java/rs117/hd/scene/EnvironmentManager.java +++ b/src/main/java/rs117/hd/scene/EnvironmentManager.java @@ -73,7 +73,7 @@ public class EnvironmentManager { // when the current transition began, relative to plugin startup private boolean transitionComplete = true; private double transitionStartTime = 0; - private int[] previousPosition = new int[3]; + private final int[] previousPosition = new int[3]; private float[] startFogColor = new float[] { 0, 0, 0 }; public float[] currentFogColor = new float[] { 0, 0, 0 }; @@ -235,23 +235,32 @@ public void reload() { public void update(SceneContext sceneContext) { assert client.isClientThread(); - int[] focalPoint = sceneContext.localToWorld( - plugin.cameraFocalPoint[0], - plugin.cameraFocalPoint[1], - client.getPlane() - ); - // skip the transitional fade if the player has moved too far // since the previous frame. results in an instant transition when // teleporting, entering dungeons, etc. - int tileChange = (int) max(abs(subtract(vec(focalPoint), vec(previousPosition)))); - previousPosition = focalPoint; - - boolean skipTransition = tileChange >= SKIP_TRANSITION_DISTANCE; - for (var environment : sceneContext.environments) { - if (environment.area.containsPoint(focalPoint)) { - changeEnvironment(environment, skipTransition); - break; + try (var focalPointHandle = ivec3()) + { + final var focalPoint = sceneContext.localToWorld( + plugin.cameraFocalPoint[0], + plugin.cameraFocalPoint[1], + client.getPlane(), + focalPointHandle.data() + ); + + try ( + var vecFocalPointHandle = vec3(focalPoint); + var vecPreviousPosition = vec3(previousPosition); + ) { + int tileChange = (int) max(abs(vecFocalPointHandle.data(), subtract(vecFocalPointHandle.data(), vecPreviousPosition.data()))); + copyTo(previousPosition, focalPoint); + + boolean skipTransition = tileChange >= SKIP_TRANSITION_DISTANCE; + for (var environment : sceneContext.environments) { + if (environment.area.containsPoint(focalPoint)) { + changeEnvironment(environment, skipTransition); + break; + } + } } } diff --git a/src/main/java/rs117/hd/utils/MathUtils.java b/src/main/java/rs117/hd/utils/MathUtils.java index 1eeab8a7f2..7b1fcc2f4c 100644 --- a/src/main/java/rs117/hd/utils/MathUtils.java +++ b/src/main/java/rs117/hd/utils/MathUtils.java @@ -8,9 +8,12 @@ */ package rs117.hd.utils; +import java.util.ArrayDeque; import java.util.Arrays; import java.util.Random; +import java.util.function.Supplier; import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; /** * Math utility functions similar to GLSL, including vector operations on raw float arrays. @@ -41,6 +44,109 @@ public final class MathUtils { public static final float JAU_TO_RAD = TWO_PI / 2048; public static final float RAD_TO_JAU = 1 / JAU_TO_RAD; + @RequiredArgsConstructor + public static final class VecHandle implements AutoCloseable { + private final VecCache owner; + private final T vec; + private boolean cached = true; + + public T data() { + assert !cached; + return vec; + } + + @Override + public void close() { + final ArrayDeque> deque = owner.cache.get(); + cached = true; + deque.add(this); + } + } + + @RequiredArgsConstructor + static final class VecCache { + final ThreadLocal>> cache = ThreadLocal.withInitial(ArrayDeque::new); + final Supplier supplier; + + VecHandle get() { + final ArrayDeque> deque = cache.get(); + VecHandle handle = deque.poll(); + if (handle == null) + handle = new VecHandle<>(this, supplier.get()); + assert handle.cached; + handle.cached = false; + return handle; + } + } + + private static final VecCache iVec2Cache = new VecCache<>(() -> new int[2]); + private static final VecCache iVec3Cache = new VecCache<>(() -> new int[3]); + private static final VecCache iVec4Cache = new VecCache<>(() -> new int[4]); + + private static final VecCache vec2Cache = new VecCache<>(() -> new float[2]); + private static final VecCache vec3Cache = new VecCache<>(() -> new float[3]); + private static final VecCache vec4Cache = new VecCache<>(() -> new float[4]); + + public static VecHandle ivec2() {return iVec2Cache.get(); } + public static VecHandle ivec2(int x, int y) { + final VecHandle out = iVec2Cache.get(); + out.vec[0] = x; + out.vec[1] = y; + return out; + } + + public static VecHandle ivec3() {return iVec3Cache.get(); } + public static VecHandle ivec3(int x, int y, int z) { + final VecHandle out = iVec3Cache.get(); + out.vec[0] = x; + out.vec[1] = y; + out.vec[2] = z; + return out; + } + + public static VecHandle ivec4() {return iVec4Cache.get(); } + public static VecHandle ivec4(int x, int y, int z, int w) { + final VecHandle out = iVec4Cache.get(); + out.vec[0] = x; + out.vec[1] = y; + out.vec[2] = z; + out.vec[3] = w; + return out; + } + + public static VecHandle vec2() { return vec2Cache.get(); } + public static VecHandle vec2(int[] ivec) { return vec2(ivec[0], ivec[1]); } + public static VecHandle vec2(int x, int y) { return vec2((float) x, (float) y); } + public static VecHandle vec2(float x, float y) { + final VecHandle out = vec2Cache.get(); + out.vec[0] = x; + out.vec[1] = y; + return out; + } + + public static VecHandle vec3() { return vec3Cache.get(); } + public static VecHandle vec3(int[] ivec) { return vec3(ivec[0], ivec[1], ivec[2]); } + public static VecHandle vec3(int x, int y, int z) { return vec3((float) x, (float) y, (float) z); } + public static VecHandle vec3(float x, float y, float z) { + final VecHandle out = vec3Cache.get(); + out.vec[0] = x; + out.vec[1] = y; + out.vec[2] = z; + return out; + } + + public static VecHandle vec4() { return vec4Cache.get(); } + public static VecHandle vec4(int[] ivec) { return vec4(ivec[0], ivec[1], ivec[2], ivec[3]); } + public static VecHandle vec4(int x, int y, int z, int w) { return vec4((float) x, (float) y, (float) z, (float) w); } + public static VecHandle vec4(float x, float y, float z, float w) { + final VecHandle out = vec4Cache.get(); + out.vec[0] = x; + out.vec[1] = y; + out.vec[2] = z; + out.vec[3] = w; + return out; + } + public static float[] vec(float... vec) { return vec; } From 1d08d156a5751e2e52f9191d11003b4b69ea1ade Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:31:39 +0100 Subject: [PATCH 09/10] Switch GLState IntSet to PrimitiveIntArray instead of using a HashSet --- src/main/java/rs117/hd/opengl/GLState.java | 14 ++++----- .../utils/collections/PrimitiveIntArray.java | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/main/java/rs117/hd/opengl/GLState.java b/src/main/java/rs117/hd/opengl/GLState.java index 1a59e10827..cfadae13c3 100644 --- a/src/main/java/rs117/hd/opengl/GLState.java +++ b/src/main/java/rs117/hd/opengl/GLState.java @@ -1,10 +1,9 @@ package rs117.hd.opengl; import java.util.Arrays; -import java.util.HashSet; import java.util.Objects; -import java.util.Set; import lombok.Getter; +import rs117.hd.utils.collections.PrimitiveIntArray; public abstract class GLState { protected boolean hasValue; @@ -187,11 +186,11 @@ protected void internalApply() { } public abstract static class IntSet extends GLState { - private final Set targets = new HashSet<>(); + private final PrimitiveIntArray targets = new PrimitiveIntArray(); public void add(int target) { hasValue = true; - targets.add(target); + targets.addUnique(target); } public void remove(int target) { @@ -201,14 +200,15 @@ public void remove(int target) { @Override protected void internalApply() { - for (int t : targets) applyTarget(t); - targets.clear(); + for (int i = 0; i < targets.length; i++) + applyTarget(targets.array[i]); + targets.reset(); } @Override public void reset() { super.reset(); - targets.clear(); + targets.reset(); } protected abstract void applyTarget(int target); diff --git a/src/main/java/rs117/hd/utils/collections/PrimitiveIntArray.java b/src/main/java/rs117/hd/utils/collections/PrimitiveIntArray.java index a680dadb35..8799bfe64b 100644 --- a/src/main/java/rs117/hd/utils/collections/PrimitiveIntArray.java +++ b/src/main/java/rs117/hd/utils/collections/PrimitiveIntArray.java @@ -20,6 +20,37 @@ public PrimitiveIntArray ensureCapacity(int count) { return this; } + public void add(int val) { + ensureCapacity(1); + array[length++] = val; + } + + public boolean contains(int val) { + for(int i = 0; i < length; i++){ + if(array[i] == val) return true; + } + return false; + } + + public boolean isEmpty() { + return length == 0; + } + + public void addUnique(int val) { + if(contains(val)) return; + ensureCapacity(1); + array[length++] = val; + } + + public void remove(int val) { + for(int i = 0; i < length; i++){ + if(array[i] == val) { + removeAtSwap(i); + return; + } + } + } + public void put(int i) { if (length < array.length) array[length++] = i; From 954472b1e1aa72eedda2968cff304b393d2912d2 Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:15:11 +0100 Subject: [PATCH 10/10] Fixes to base ivec/vec getter --- src/main/java/rs117/hd/utils/MathUtils.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/rs117/hd/utils/MathUtils.java b/src/main/java/rs117/hd/utils/MathUtils.java index 7b1fcc2f4c..eb3d46fdd4 100644 --- a/src/main/java/rs117/hd/utils/MathUtils.java +++ b/src/main/java/rs117/hd/utils/MathUtils.java @@ -87,7 +87,7 @@ VecHandle get() { private static final VecCache vec3Cache = new VecCache<>(() -> new float[3]); private static final VecCache vec4Cache = new VecCache<>(() -> new float[4]); - public static VecHandle ivec2() {return iVec2Cache.get(); } + public static VecHandle ivec2() {return ivec2(0, 0); } public static VecHandle ivec2(int x, int y) { final VecHandle out = iVec2Cache.get(); out.vec[0] = x; @@ -95,7 +95,7 @@ public static VecHandle ivec2(int x, int y) { return out; } - public static VecHandle ivec3() {return iVec3Cache.get(); } + public static VecHandle ivec3() {return ivec3(0, 0, 0); } public static VecHandle ivec3(int x, int y, int z) { final VecHandle out = iVec3Cache.get(); out.vec[0] = x; @@ -104,7 +104,7 @@ public static VecHandle ivec3(int x, int y, int z) { return out; } - public static VecHandle ivec4() {return iVec4Cache.get(); } + public static VecHandle ivec4() {return ivec4(0, 0, 0, 0); } public static VecHandle ivec4(int x, int y, int z, int w) { final VecHandle out = iVec4Cache.get(); out.vec[0] = x; @@ -114,7 +114,7 @@ public static VecHandle ivec4(int x, int y, int z, int w) { return out; } - public static VecHandle vec2() { return vec2Cache.get(); } + public static VecHandle vec2() { return vec2(0, 0); } public static VecHandle vec2(int[] ivec) { return vec2(ivec[0], ivec[1]); } public static VecHandle vec2(int x, int y) { return vec2((float) x, (float) y); } public static VecHandle vec2(float x, float y) { @@ -124,7 +124,7 @@ public static VecHandle vec2(float x, float y) { return out; } - public static VecHandle vec3() { return vec3Cache.get(); } + public static VecHandle vec3() { return vec3(0, 0, 0); } public static VecHandle vec3(int[] ivec) { return vec3(ivec[0], ivec[1], ivec[2]); } public static VecHandle vec3(int x, int y, int z) { return vec3((float) x, (float) y, (float) z); } public static VecHandle vec3(float x, float y, float z) { @@ -135,7 +135,7 @@ public static VecHandle vec3(float x, float y, float z) { return out; } - public static VecHandle vec4() { return vec4Cache.get(); } + public static VecHandle vec4() { return vec4(0, 0, 0, 0); } public static VecHandle vec4(int[] ivec) { return vec4(ivec[0], ivec[1], ivec[2], ivec[3]); } public static VecHandle vec4(int x, int y, int z, int w) { return vec4((float) x, (float) y, (float) z, (float) w); } public static VecHandle vec4(float x, float y, float z, float w) {