From c1dad5d14922db268ff8f533acefaba4ba820438 Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:39:37 +0100 Subject: [PATCH 01/12] Profiler Upgrade Client Timer Tracking Refactored FrameTimer Graph to be generic * Added Memory Tracking Tweaked Memory Graph heights Ui Ui Improvements Graph Mouse Hovering support with Tooltips displaying series data Added Axis naming support & improved memory allocations Dont Draw Series which have no data Implement Drag Support to get the average of samples within a range Make Selection work when moving around overlay Track Allocations Renamed GPU Memory to System Memory Switch NPCDisplacementCache to use Int Collections Track Per Thread Allocations Allocation Improvements Fixes Build Background image since it doesn't change often Further optimisations Added Logging on startup for allocation tracking support Fixes --- src/main/java/rs117/hd/HdPlugin.java | 33 +- src/main/java/rs117/hd/HdPluginConfig.java | 60 + .../java/rs117/hd/overlays/FrameTimer.java | 61 +- .../hd/overlays/FrameTimerGraphOverlay.java | 421 +++++ .../rs117/hd/overlays/FrameTimerOverlay.java | 1602 +++++++++++++++-- .../java/rs117/hd/overlays/FrameTimerUI.java | 535 ++++++ .../java/rs117/hd/overlays/FrameTimings.java | 11 +- .../rs117/hd/overlays/FrameTimingsStore.java | 44 + src/main/java/rs117/hd/overlays/Timer.java | 3 + .../overlays/components/GraphComponent.java | 771 ++++++++ .../overlays/components/SwatchComponent.java | 46 + .../renderer/zone/ModelStreamingManager.java | 5 +- .../renderer/zone/StaticAlphaSortingJob.java | 4 +- .../rs117/hd/renderer/zone/ZoneRenderer.java | 38 +- .../java/rs117/hd/utils/DeveloperTools.java | 25 + .../rs117/hd/utils/FrameTimingsRecorder.java | 25 + src/main/java/rs117/hd/utils/HDUtils.java | 34 + .../rs117/hd/utils/NpcDisplacementCache.java | 35 +- 18 files changed, 3524 insertions(+), 229 deletions(-) create mode 100644 src/main/java/rs117/hd/overlays/FrameTimerGraphOverlay.java create mode 100644 src/main/java/rs117/hd/overlays/FrameTimerUI.java create mode 100644 src/main/java/rs117/hd/overlays/FrameTimingsStore.java create mode 100644 src/main/java/rs117/hd/overlays/components/GraphComponent.java create mode 100644 src/main/java/rs117/hd/overlays/components/SwatchComponent.java diff --git a/src/main/java/rs117/hd/HdPlugin.java b/src/main/java/rs117/hd/HdPlugin.java index cb715fd25a..7cad916e2e 100644 --- a/src/main/java/rs117/hd/HdPlugin.java +++ b/src/main/java/rs117/hd/HdPlugin.java @@ -572,6 +572,8 @@ protected void startUp() { String glVendor = Objects.requireNonNullElse(glGetString(GL_VENDOR), "Unknown"); var runtime = Runtime.getRuntime(); + boolean supportsThreadAllocationTracking = HDUtils.setupThreadAllocatedBytesMonitoring(); + APPLE = osType == OSType.MacOS; APPLE_ARM = APPLE && osArch.equals("aarch64"); AMD_GPU = glRenderer.contains("AMD") || glRenderer.contains("Radeon") || glVendor.contains("ATI"); @@ -582,19 +584,20 @@ protected void startUp() { SUPPORTS_STORAGE_BUFFERS = GL_CAPS.GL_ARB_buffer_storage && !DEBUG_MAC_OS && config.storageBuffers().get(!INTEL_GPU); log.info("Starting 117 HD... (count: {})", startupCount); - log.info("Renderer: {}", rendererClass.getSimpleName()); - log.info("rlawt version: {}", rlawtVersion); - log.info("LWJGL Version: {}", Version.getVersion()); - log.info("Java version: {} ({})", javaVmName, javaVersion); - log.info("Java memory limit: {} (free: {})", formatBytes(runtime.maxMemory()), formatBytes(runtime.freeMemory())); - log.info("Operating system: {} {} ({}-bit {})", osType, osVersion, wordSize, osArch); - log.info("CPU: {} ({} threads)", HDUtils.getCpuName(), runtime.availableProcessors()); - log.info("Memory: {}", formatBytes(HDUtils.getTotalSystemMemory())); - log.info("GPU: {} ({})", glRenderer, glVendor); - log.info("GPU driver: {}", glGetString(GL_VERSION)); - log.info("Indirect draw: {}", SUPPORTS_INDIRECT_DRAW); - log.info("Storage buffers: {}", SUPPORTS_STORAGE_BUFFERS); - log.info("Low memory mode: {}", useLowMemoryMode); + log.info("Renderer: {}", rendererClass.getSimpleName()); + log.info("rlawt version: {}", rlawtVersion); + log.info("LWJGL Version: {}", Version.getVersion()); + log.info("Java version: {} ({})", javaVmName, javaVersion); + log.info("Java memory limit: {} (free: {})", formatBytes(runtime.maxMemory()), formatBytes(runtime.freeMemory())); + log.info("Operating system: {} {} ({}-bit {})", osType, osVersion, wordSize, osArch); + log.info("CPU: {} ({} threads)", HDUtils.getCpuName(), runtime.availableProcessors()); + log.info("Memory: {}", formatBytes(HDUtils.getTotalSystemMemory())); + log.info("GPU: {} ({})", glRenderer, glVendor); + log.info("GPU driver: {}", glGetString(GL_VERSION)); + log.info("Indirect draw: {}", SUPPORTS_INDIRECT_DRAW); + log.info("Storage buffers: {}", SUPPORTS_STORAGE_BUFFERS); + log.info("Allocation Tracking: {}", supportsThreadAllocationTracking); + log.info("Low memory mode: {}", useLowMemoryMode); renderer = injector.getInstance(rendererClass); @@ -1520,9 +1523,9 @@ public void prepareInterfaceTexture() { .build( "AsyncUICopy", t -> { - long start = System.nanoTime(); + long timestamp = 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, timestamp); } ) .setExecuteAsync(!isPowerSaving) diff --git a/src/main/java/rs117/hd/HdPluginConfig.java b/src/main/java/rs117/hd/HdPluginConfig.java index f79b21b215..d147195742 100644 --- a/src/main/java/rs117/hd/HdPluginConfig.java +++ b/src/main/java/rs117/hd/HdPluginConfig.java @@ -1253,6 +1253,66 @@ default boolean multithreadedModelProcessing() { /*====== Internal settings ======*/ + String KEY_FRAME_TIMER_OVERLAY_ENABLED = "frameTimerOverlayEnabled"; + @ConfigItem(keyName = KEY_FRAME_TIMER_OVERLAY_ENABLED, hidden = true, name = "", description = "") + default boolean frameTimerOverlayEnabled() { + return false; + } + + String KEY_FRAME_TIMER_GRAPH_ENABLED = "frameTimerGraphEnabled"; + @ConfigItem(keyName = KEY_FRAME_TIMER_GRAPH_ENABLED, hidden = true, name = "", description = "") + default boolean frameTimerGraphEnabled() { + return false; + } + + String KEY_FRAME_TIMER_HIDDEN_GRAPHS = "frameTimerHiddenGraphs"; + @ConfigItem(keyName = KEY_FRAME_TIMER_HIDDEN_GRAPHS, hidden = true, name = "", description = "") + default String frameTimerHiddenGraphs() { + return ""; + } + + String KEY_FRAME_TIMER_SELECTED_TAB = "frameTimerSelectedTab"; + @ConfigItem(keyName = KEY_FRAME_TIMER_SELECTED_TAB, hidden = true, name = "", description = "") + default String frameTimerSelectedTab() { + return "ALL"; + } + + String KEY_FRAME_TIMER_HIDDEN_TABS = "frameTimerHiddenTabs"; + @ConfigItem(keyName = KEY_FRAME_TIMER_HIDDEN_TABS, hidden = true, name = "", description = "") + default String frameTimerHiddenTabs() { + return ""; + } + + String KEY_FRAME_TIMER_DETACHED_TABS = "frameTimerDetachedTabs"; + @ConfigItem(keyName = KEY_FRAME_TIMER_DETACHED_TABS, hidden = true, name = "", description = "") + default String frameTimerDetachedTabs() { + return ""; + } + + String KEY_FRAME_TIMER_SETTINGS_DETACHED = "frameTimerSettingsDetached"; + @ConfigItem(keyName = KEY_FRAME_TIMER_SETTINGS_DETACHED, hidden = true, name = "", description = "") + default boolean frameTimerSettingsDetached() { + return false; + } + + String KEY_FRAME_TIMER_GRAPH_WIDTH = "frameTimerGraphWidth"; + @ConfigItem(keyName = KEY_FRAME_TIMER_GRAPH_WIDTH, hidden = true, name = "", description = "") + default int frameTimerGraphWidth() { + return 800; + } + + String KEY_FRAME_TIMER_GRAPH_HEIGHT = "frameTimerGraphHeight"; + @ConfigItem(keyName = KEY_FRAME_TIMER_GRAPH_HEIGHT, hidden = true, name = "", description = "") + default int frameTimerGraphHeight() { + return 200; + } + + String KEY_FRAME_TIMER_MEMORY_GRAPH_HEIGHT = "frameTimerMemoryGraphHeight"; + @ConfigItem(keyName = KEY_FRAME_TIMER_MEMORY_GRAPH_HEIGHT, hidden = true, name = "", description = "") + default int frameTimerMemoryGraphHeight() { + return 75; + } + @ConfigItem(keyName = "pluginUpdateMessage", hidden = true, name = "", description = "") void setPluginUpdateMessage(int version); @ConfigItem(keyName = "pluginUpdateMessage", hidden = true, name = "", description = "") diff --git a/src/main/java/rs117/hd/overlays/FrameTimer.java b/src/main/java/rs117/hd/overlays/FrameTimer.java index edd523043f..4294781fcd 100644 --- a/src/main/java/rs117/hd/overlays/FrameTimer.java +++ b/src/main/java/rs117/hd/overlays/FrameTimer.java @@ -15,8 +15,13 @@ import net.runelite.client.callback.ClientThread; import org.lwjgl.opengl.*; import rs117.hd.HdPlugin; +import rs117.hd.utils.HDUtils; +import static org.lwjgl.opengl.GL11.glGetInteger; import static org.lwjgl.opengl.GL33C.*; +import static org.lwjgl.opengl.NVXGPUMemoryInfo.GL_GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX; +import static org.lwjgl.opengl.NVXGPUMemoryInfo.GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX; +import static rs117.hd.HdPlugin.GL_CAPS; @Slf4j @Singleton @@ -38,9 +43,13 @@ public class FrameTimer { private static final int NUM_GPU_TIMERS = (int) Arrays.stream(Timer.TIMERS).filter(Timer::isGpuTimer).count(); private static final int NUM_GPU_DEBUG_GROUPS = (int) Arrays.stream(Timer.TIMERS).filter(Timer::hasGpuDebugGroup).count(); + private static final boolean TRACK_SYSTEM_MEMORY = HDUtils.getFreeSystemMemory() != Long.MAX_VALUE; + 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[] heap = 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<>(); @@ -137,10 +146,15 @@ public void removeAllListeners() { public void reset() { Arrays.fill(timings, 0); + Arrays.fill(allocations, 0); Arrays.fill(activeTimers, false); cumulativeError = 0; } + public long getTimeStamp() { return isActive ? System.nanoTime() : 0; } + + public long getUsedMemory() { return isActive ? HDUtils.getUsedMemory(true) : 0; } + public AutoTimer begin(Timer timer) { int index = timer.ordinal(); if (log.isDebugEnabled() && timer.hasGpuDebugGroup() && HdPlugin.GL_CAPS.OpenGL43) { @@ -162,6 +176,7 @@ public AutoTimer begin(Timer timer) { } else if (!activeTimers[index]) { cumulativeError += errorCompensation + 1 >> 1; timings[index] -= System.nanoTime() - cumulativeError; + heap[index] = HDUtils.getUsedMemory(true); } activeTimers[index] = true; @@ -179,24 +194,44 @@ public void end(Timer timer) { } } - if (!isActive || !activeTimers[timer.ordinal()]) + int index = timer.ordinal(); + if (!isActive || !activeTimers[index]) return; if (timer.isGpuTimer()) { - glQueryCounter(gpuQueries[timer.ordinal() * 2 + 1], GL_TIMESTAMP); + glQueryCounter(gpuQueries[index * 2 + 1], GL_TIMESTAMP); // leave the GPU timer active, since it needs to be gathered at a later point } else { + final long originalHeap = heap[index]; + final long newHeap = HDUtils.getUsedMemory(true); + final long allocated = newHeap - originalHeap; + cumulativeError += errorCompensation >> 1; - timings[timer.ordinal()] += System.nanoTime() - cumulativeError; - activeTimers[timer.ordinal()] = false; + timings[index] += System.nanoTime() - cumulativeError; + allocations[index] += allocated > 0 ? allocated : 0; + activeTimers[index] = false; + heap[index] = 0; } } - public void add(Timer timer, long nanos) { + public void addDuration(Timer timer, long nanos) { if (isActive) timings[timer.ordinal()] += nanos; } + public void add(Timer timer, long startNanos) { + if (isActive) + timings[timer.ordinal()] += System.nanoTime() - startNanos; + } + + public void add(Timer timer, long startNanos, long startMemory) { + if (isActive) { + long allocation = HDUtils.getUsedMemory(true) - startMemory; + timings[timer.ordinal()] += System.nanoTime() - startNanos; + allocations[timer.ordinal()] += allocation > 0 ? allocation : 0; + } + } + public void add(Timer timer, long duration, TimeUnit unit) { if (isActive) timings[timer.ordinal()] += TimeUnit.NANOSECONDS.convert(duration, unit); @@ -240,7 +275,19 @@ public void endFrameAndReset() { } final float cpuLoad = (float) osBean.getSystemLoadAverage() / osBean.getAvailableProcessors(); - var frameTimings = new FrameTimings(frameEndTimestamp, timings, cpuLoad); + final long heapUsageKB = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1024L; + final long freeSystemMemory = TRACK_SYSTEM_MEMORY ? HDUtils.getFreeSystemMemory() / 1024L : 0; + + final long gpuUsageKB; + if (GL_CAPS.GL_NVX_gpu_memory_info) { + int totalKB = glGetInteger(GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX); + int availableKB = glGetInteger(GL_GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX); + gpuUsageKB = totalKB - availableKB; + } else { + gpuUsageKB = -1; + } + + var frameTimings = new FrameTimings(frameEndTimestamp, timings, allocations, cpuLoad, heapUsageKB, freeSystemMemory, gpuUsageKB); for (var listener : listeners) listener.onFrameCompletion(frameTimings); @@ -265,6 +312,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/overlays/FrameTimerGraphOverlay.java b/src/main/java/rs117/hd/overlays/FrameTimerGraphOverlay.java new file mode 100644 index 0000000000..0144b8e044 --- /dev/null +++ b/src/main/java/rs117/hd/overlays/FrameTimerGraphOverlay.java @@ -0,0 +1,421 @@ +package rs117.hd.overlays; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.*; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.input.MouseListener; +import net.runelite.client.input.MouseManager; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.OverlayPosition; +import rs117.hd.HdPlugin; +import rs117.hd.overlays.components.GraphComponent; + +import static rs117.hd.HdPlugin.GL_CAPS; + +@Slf4j +@Singleton +public class FrameTimerGraphOverlay extends OverlayPanel implements MouseListener { + private static final Color HEAP_COLOR = new Color(80, 200, 255); + private static final Color GPU_COLOR = new Color(80, 255, 95); + private static final Color MEMORY_COLOR = new Color(255, 80, 179); + + private static final long BYTES_PER_KB = 1024L; + private static final long KB_PER_MB = 1024L; + private static final int SCREEN_MARGIN = 24; + private static final int PANEL_BORDER = 4; + private static final double DEFAULT_WIDTH_RATIO = 0.55; + private static final double DEFAULT_HEIGHT_RATIO = 0.70; + + @Inject + private OverlayManager overlayManager; + + @Inject + private Client client; + + @Inject + private FrameTimerOverlay frameTimerOverlay; + + @Inject + private FrameTimingsStore frameTimingsStore; + + @Inject + private FrameTimerUI ui; + + @Inject + private EventBus eventBus; + + @Inject + private MouseManager mouseManager; + + @Inject + private FrameTimer frameTimer; + + private final List timerGraphs = new ArrayList<>(); + private final List memoryGraphs = new ArrayList<>(); + private final List frames = new ArrayList<>(); + + @Getter + private boolean active; + + private GraphComponent dragGraph; + private Point dragStartPoint; + + private static final class GraphEntry { + final FrameTimerUI.Graph id; + final GraphComponent component; + + GraphEntry(FrameTimerUI.Graph id, GraphComponent component) { + this.id = id; + this.component = component; + } + } + + @Inject + public FrameTimerGraphOverlay(HdPlugin plugin) { + super(plugin); + setLayer(OverlayLayer.ABOVE_SCENE); + setPosition(OverlayPosition.TOP_RIGHT); + setPreferredLocation(new Point(50, 10)); + setMinimumSize(GraphComponent.MIN_GRAPH_WIDTH / 2); + } + + boolean hasSelection() { + for(int i = 0; i < timerGraphs.size(); i++){ + if(timerGraphs.get(i).component.isSelectionActive()) + return true; + } + for(int i = 0; i < memoryGraphs.size(); i++){ + if(memoryGraphs.get(i).component.isSelectionActive()) + return true; + } + + return false; + } + + void createGraphs() { + Supplier> frames = () -> this.frames; + Supplier hoveredTimer = () -> frameTimerOverlay.getHoveredTimer(); + Supplier mousePosition = () -> { + var p = client.getMouseCanvasPosition(); + return p == null ? null : new Point(p.getX(), p.getY()); + }; + + timerGraphs.clear(); + memoryGraphs.clear(); + + var cpuGpuGraph = setupFrameTimerGraph(new GraphComponent<>("CPU/GPU", frames, hoveredTimer, mousePosition), FrameTimerUI.Graph.CPU_GPU); + var cpuGraph = setupFrameTimerGraph(new GraphComponent<>("CPU", frames, hoveredTimer, mousePosition), FrameTimerUI.Graph.CPU); + var asyncGraph = setupFrameTimerGraph(new GraphComponent<>("ASYNC", frames, hoveredTimer, mousePosition), FrameTimerUI.Graph.ASYNC); + var gpuGraph = setupFrameTimerGraph(new GraphComponent<>("GPU", frames, hoveredTimer, mousePosition), FrameTimerUI.Graph.GPU); + var allocationGraph = setupMemoryGraph(new GraphComponent<>("Allocations", frames, hoveredTimer, mousePosition), true, FrameTimerUI.Graph.ALLOCATIONS); + var heapMemoryGraph = setupMemoryGraph(new GraphComponent<>("Heap Memory", frames, () -> null, mousePosition), false, FrameTimerUI.Graph.HEAP); + var systemMemoryGraph = setupMemoryGraph(new GraphComponent<>("System Memory", frames, () -> null, mousePosition), false, FrameTimerUI.Graph.SYSTEM_MEMORY); + + if (GL_CAPS.GL_NVX_gpu_memory_info) + systemMemoryGraph.addSeries("GPU Memory", GPU_COLOR, f -> f.gpuUsageKB / (double) KB_PER_MB, false); + systemMemoryGraph.addSeries("Avail System Memory", MEMORY_COLOR, f -> f.freeSystemMemoryKB / (double) KB_PER_MB, false); + + heapMemoryGraph.addSeries("Heap Memory", HEAP_COLOR, f -> (f.heapUsageKB) / (double) KB_PER_MB, false); + + addMemorySeries(allocationGraph, Timer.CLIENT); + addTimerSeries(cpuGpuGraph, Timer.CLIENT); + addTimerSeries(cpuGpuGraph, Timer.DRAW_FRAME); + addTimerSeries(cpuGpuGraph, Timer.RENDER_FRAME); + + for (Timer t : Timer.TIMERS) { + if (t.isCpuTimer() && !isInGraph(cpuGpuGraph, t)) { + addTimerSeries(cpuGraph, t); + addMemorySeries(allocationGraph, t); + } + + if (t.isAsyncCpuTimer()) { + addTimerSeries(asyncGraph, t); + addMemorySeries(allocationGraph, t); + } + + if (t.isGpuTimer()) + addTimerSeries(gpuGraph, t); + } + + applyGraphSizes(); + } + + private GraphComponent setupFrameTimerGraph(GraphComponent graph, FrameTimerUI.Graph graphId) { + graph + .setYAxisName("ms") + .setAxisFormat("%.3f") + .setAppendAxisNameToTooltip(true); + timerGraphs.add(new GraphEntry(graphId, graph)); + return graph; + } + + private GraphComponent setupMemoryGraph(GraphComponent graph, boolean isKB, FrameTimerUI.Graph graphId) { + graph + .setRoundStep(50.0) + .setYAxisName(isKB ? "KB" : "MB") + .setAppendAxisNameToTooltip(true); + memoryGraphs.add(new GraphEntry(graphId, graph)); + return graph; + } + + private void applyGraphSizes() { + if (timerGraphs.isEmpty() && memoryGraphs.isEmpty()) + return; + + int visibleTimers = countVisible(timerGraphs); + int visibleMemory = countVisible(memoryGraphs); + int visibleTotal = visibleTimers + visibleMemory; + if (visibleTotal == 0) + return; + + int canvasW = Math.max(1, client.getCanvasWidth()); + int canvasH = Math.max(1, client.getCanvasHeight()); + + int maxPlotWidth = Math.max( + GraphComponent.MIN_GRAPH_WIDTH, + canvasW - SCREEN_MARGIN - GraphComponent.MARGIN_LEFT - GraphComponent.MARGIN_RIGHT - PANEL_BORDER * 2 + ); + int maxContentHeight = Math.max( + GraphComponent.MIN_GRAPH_HEIGHT, + canvasH - SCREEN_MARGIN - PANEL_BORDER * 2 + ); + int chromeHeight = visibleTotal * (GraphComponent.MARGIN_TOP + GraphComponent.MARGIN_BOTTOM); + int maxTotalPlotHeight = Math.max(GraphComponent.MIN_GRAPH_HEIGHT, maxContentHeight - chromeHeight); + + Dimension preferred = getPreferredSize(); + int plotWidth; + int timerHeight; + int memoryHeight; + + if (preferred != null && preferred.width > 0 && preferred.height > 0) { + plotWidth = preferred.width - PANEL_BORDER * 2 - GraphComponent.MARGIN_LEFT - GraphComponent.MARGIN_RIGHT; + int totalPlotHeight = preferred.height - PANEL_BORDER * 2 - chromeHeight; + + plotWidth = clamp(plotWidth, GraphComponent.MIN_GRAPH_WIDTH, Math.min(GraphComponent.MAX_GRAPH_WIDTH, maxPlotWidth)); + totalPlotHeight = clamp(totalPlotHeight, GraphComponent.MIN_GRAPH_HEIGHT, maxTotalPlotHeight); + + int weight = visibleTimers * 3 + visibleMemory; + if (weight <= 0) + weight = 1; + + timerHeight = visibleTimers > 0 + ? clamp((totalPlotHeight * 3) / weight, GraphComponent.MIN_GRAPH_HEIGHT, GraphComponent.MAX_GRAPH_HEIGHT) + : GraphComponent.DEFAULT_GRAPH_HEIGHT; + memoryHeight = visibleMemory > 0 + ? clamp(totalPlotHeight / weight, GraphComponent.MIN_MEMORY_GRAPH_HEIGHT, GraphComponent.MAX_MEMORY_GRAPH_HEIGHT) + : GraphComponent.DEFAULT_MEMORY_GRAPH_HEIGHT; + } else { + plotWidth = clamp( + (int) (canvasW * DEFAULT_WIDTH_RATIO), + GraphComponent.MIN_GRAPH_WIDTH, + Math.min(GraphComponent.MAX_GRAPH_WIDTH, maxPlotWidth) + ); + + int preferredTimerHeight = defaultTimerHeight(canvasH, visibleTimers, visibleMemory); + int preferredMemoryHeight = defaultMemoryHeight(canvasH, visibleTimers, visibleMemory); + int preferredTotal = visibleTimers * preferredTimerHeight + visibleMemory * preferredMemoryHeight; + + if (preferredTotal > maxTotalPlotHeight && preferredTotal > 0) { + double scale = (double) maxTotalPlotHeight / preferredTotal; + timerHeight = clamp( + (int) Math.round(preferredTimerHeight * scale), + GraphComponent.MIN_GRAPH_HEIGHT, + GraphComponent.MAX_GRAPH_HEIGHT + ); + memoryHeight = clamp( + (int) Math.round(preferredMemoryHeight * scale), + GraphComponent.MIN_MEMORY_GRAPH_HEIGHT, + GraphComponent.MAX_MEMORY_GRAPH_HEIGHT + ); + } else { + timerHeight = preferredTimerHeight; + memoryHeight = preferredMemoryHeight; + } + } + + for (var entry : timerGraphs) + entry.component.setGraphSize(plotWidth, timerHeight); + + for (var entry : memoryGraphs) + entry.component.setGraphSize(plotWidth, memoryHeight); + } + + private int defaultTimerHeight(int canvasH, int visibleTimers, int visibleMemory) { + int usable = (int) (canvasH * DEFAULT_HEIGHT_RATIO); + int chrome = (visibleTimers + visibleMemory) * (GraphComponent.MARGIN_TOP + GraphComponent.MARGIN_BOTTOM); + int plotBudget = Math.max(GraphComponent.MIN_GRAPH_HEIGHT, usable - chrome); + int weight = visibleTimers * 3 + visibleMemory; + if (weight <= 0) + return GraphComponent.DEFAULT_GRAPH_HEIGHT; + return Math.max(GraphComponent.MIN_GRAPH_HEIGHT, (plotBudget * 3) / weight); + } + + private int defaultMemoryHeight(int canvasH, int visibleTimers, int visibleMemory) { + int usable = (int) (canvasH * DEFAULT_HEIGHT_RATIO); + int chrome = (visibleTimers + visibleMemory) * (GraphComponent.MARGIN_TOP + GraphComponent.MARGIN_BOTTOM); + int plotBudget = Math.max(GraphComponent.MIN_MEMORY_GRAPH_HEIGHT, usable - chrome); + int weight = visibleTimers * 3 + visibleMemory; + if (weight <= 0) + return GraphComponent.DEFAULT_MEMORY_GRAPH_HEIGHT; + return Math.max(GraphComponent.MIN_MEMORY_GRAPH_HEIGHT, plotBudget / weight); + } + + private int countVisible(List graphs) { + int count = 0; + for (var entry : graphs) { + if (ui.isGraphVisible(entry.id)) + count++; + } + return count; + } + + private void addTimerSeries(GraphComponent graph, Timer t) { + graph.addSeries(t.name(), t.color, f -> f.timers[t.ordinal()] / 1e6, false, t); + } + + private void addMemorySeries(GraphComponent graph, Timer t) { + graph.addSeries(t.name(), t.color, f -> f.allocations[t.ordinal()] / (double) BYTES_PER_KB, false, t); + } + + private boolean isInGraph(GraphComponent graph, Timer t) { + return graph.getSeries(t) != null; + } + + public boolean hasGpuMemoryGraph() { + return GL_CAPS.GL_NVX_gpu_memory_info; + } + + public void setActive(boolean activate) { + active = activate; + if (activate) { + overlayManager.add(this); + eventBus.register(this); + mouseManager.registerMouseListener(this); + } else { + overlayManager.remove(this); + eventBus.unregister(this); + mouseManager.unregisterMouseListener(this); + dragGraph = null; + dragStartPoint = null; + } + } + + @Override + public Dimension render(Graphics2D g) { + if (timerGraphs.isEmpty() && memoryGraphs.isEmpty()) + createGraphs(); + else + applyGraphSizes(); + + if(!hasSelection()) { + frames.clear(); + frames.addAll(frameTimingsStore.getFrames()); + } + + var children = panelComponent.getChildren(); + for (var entry : timerGraphs) { + if (ui.isGraphVisible(entry.id)) + children.add(entry.component); + } + for (var entry : memoryGraphs) { + if (ui.isGraphVisible(entry.id)) + children.add(entry.component); + } + + Dimension dimension = super.render(g); + + for (var entry : timerGraphs) { + if (ui.isGraphVisible(entry.id)) + entry.component.renderTooltip(g); + } + for (var entry : memoryGraphs) { + if (ui.isGraphVisible(entry.id)) + entry.component.renderTooltip(g); + } + + return dimension; + } + + private static int clamp(int value, int min, int max) { + return Math.max(min, Math.min(max, value)); + } + + private GraphComponent findGraphAt(Point canvasPoint) { + var panelBounds = getBounds(); + if (panelBounds == null || panelBounds.width <= 0 || panelBounds.height <= 0) + return null; + + int localX = canvasPoint.x - panelBounds.x; + int localY = canvasPoint.y - panelBounds.y; + + for (var entry : timerGraphs) + if (ui.isGraphVisible(entry.id) && entry.component.getBounds().contains(localX, localY)) + return entry.component; + for (var entry : memoryGraphs) + if (ui.isGraphVisible(entry.id) && entry.component.getBounds().contains(localX, localY)) + return entry.component; + return null; + } + + @Override + public MouseEvent mousePressed(MouseEvent event) { + if (active && event.getButton() == MouseEvent.BUTTON1) { + GraphComponent hit = findGraphAt(event.getPoint()); + if (hit != null) { + dragGraph = hit; + dragStartPoint = event.getPoint(); + event.consume(); + } + } + return event; + } + + @Override + public MouseEvent mouseDragged(MouseEvent event) { + if (dragGraph != null) { + dragGraph.setSelectionFromPoints(dragStartPoint, event.getPoint()); + event.consume(); + } + return event; + } + + @Override + public MouseEvent mouseReleased(MouseEvent event) { + if(dragGraph != null) { + if(dragStartPoint.distance(event.getPoint()) <= 0.01) { + dragGraph.clearSelection(); + } else { + dragGraph.setSelectionFromPoints(dragStartPoint, event.getPoint()); + } + dragGraph = null; + dragStartPoint = null; + event.consume(); + } + return event; + } + + @Override + public MouseEvent mouseClicked(MouseEvent event) { return event; } + + @Override + public MouseEvent mouseEntered(MouseEvent event) { return event; } + + @Override + public MouseEvent mouseExited(MouseEvent event) { return event; } + + @Override + public MouseEvent mouseMoved(MouseEvent event) { return event; } +} \ No newline at end of file diff --git a/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java b/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java index 452f3ea6e9..84be4848bd 100644 --- a/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java +++ b/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java @@ -2,21 +2,43 @@ import com.google.inject.Inject; import com.google.inject.Singleton; +import java.awt.Color; import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; import java.awt.Graphics2D; -import java.util.ArrayDeque; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.MouseEvent; +import java.util.ArrayList; import java.util.Arrays; +import java.util.EnumMap; import java.util.Formatter; -import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; import java.util.Map; +import java.util.function.Consumer; +import javax.annotation.Nullable; +import javax.swing.SwingUtilities; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import net.runelite.api.*; +import net.runelite.client.input.MouseListener; +import net.runelite.client.input.MouseManager; import net.runelite.client.ui.FontManager; import net.runelite.client.ui.overlay.OverlayLayer; import net.runelite.client.ui.overlay.OverlayManager; import net.runelite.client.ui.overlay.OverlayPanel; import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.components.ComponentOrientation; +import net.runelite.client.ui.overlay.components.LayoutableRenderableEntity; import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.PanelComponent; +import net.runelite.client.ui.overlay.components.SplitComponent; import net.runelite.client.ui.overlay.components.TitleComponent; import rs117.hd.HdPlugin; +import rs117.hd.overlays.components.SwatchComponent; import rs117.hd.renderer.zone.SceneManager; import rs117.hd.renderer.zone.WorldViewContext; import rs117.hd.renderer.zone.ZoneRenderer; @@ -29,16 +51,29 @@ import static rs117.hd.utils.MathUtils.*; @Singleton -public class FrameTimerOverlay extends OverlayPanel implements FrameTimer.Listener { +public class FrameTimerOverlay extends OverlayPanel implements FrameTimer.Listener, MouseListener { + private static final int PANEL_HORIZONTAL_PADDING = 8; + private static final int SWATCH_WIDTH = 10; + private static final int SWATCH_GAP = 4; + @Inject private OverlayManager overlayManager; @Inject - private HdPlugin plugin; + private MouseManager mouseManager; + + @Inject + private Client client; @Inject private FrameTimer frameTimer; + @Inject + private FrameTimingsStore frameTimingsStore; + + @Inject + private FrameTimerUI ui; + @Inject private FrameTimingsRecorder frameTimingsRecorder; @@ -51,272 +86,1469 @@ public class FrameTimerOverlay extends OverlayPanel implements FrameTimer.Listen @Inject private SceneManager sceneManager; - private final ArrayDeque frames = new ArrayDeque<>(); + @Inject + private FrameTimerGraphOverlay frameTimerGraphOverlay; + + private final HdPlugin plugin; private final long[] timings = new long[Timer.TIMERS.length]; private float cpuLoad; - private final Map componentMap = new HashMap<>(); + private final LineCache lineCache = new LineCache(); + private Header header; + private Header settingsHeader; + private DetachedSettingsPanel detachedSettingsPanel; + private final Map detachedPanels = new EnumMap<>(FrameTimerUI.Tab.class); + private final StringBuilder sb = new StringBuilder(); private final Formatter formatter = new Formatter(sb); + @Getter + @Nullable + private Timer hoveredTimer; + + private boolean overlayActive; + private int mainPanelWidth; + private int mainContentLineWidth; + private int mainPanelHeight; + @Inject public FrameTimerOverlay(HdPlugin plugin) { super(plugin); + this.plugin = plugin; setLayer(OverlayLayer.ABOVE_SCENE); setPosition(OverlayPosition.TOP_RIGHT); - panelComponent.setPreferredSize(new Dimension(215, 200)); + panelComponent.setPreferredSize(new Dimension(280, 200)); + hoveredTimer = null; + } + + private Header getHeader() { + if (header == null) { + header = new Header( + ui, + frameTimingsRecorder, + frameTimingsStore, + false, + ui::selectTab, + ui::toggleSettings, + ui::detachSettings, + () -> ui.setSettingsDetached(false), + tab -> ui.setDetached(tab, true), + ui::toggleGraph, + ui::toggleHidden, + tab -> ui.setDetached(tab, false) + ); + } + return header; + } + + private Header getSettingsHeader() { + if (settingsHeader == null) { + settingsHeader = new Header( + ui, + frameTimingsRecorder, + frameTimingsStore, + true, + ui::selectTab, + ui::toggleSettings, + ui::detachSettings, + () -> ui.setSettingsDetached(false), + tab -> ui.setDetached(tab, true), + ui::toggleGraph, + ui::toggleHidden, + tab -> ui.setDetached(tab, false) + ); + } + return settingsHeader; } public void setActive(boolean activate) { + if (overlayActive == activate) + return; + + overlayActive = activate; if (activate) { - frameTimer.addTimingsListener(this); + frameTimer.addTimingsListener(frameTimingsStore); overlayManager.add(this); + mouseManager.registerMouseListener(0, this); + ui.setChangeListener(u -> syncDetachedPanels()); + syncDetachedPanels(); } else { - frameTimer.removeTimingsListener(this); + frameTimer.removeTimingsListener(frameTimingsStore); overlayManager.remove(this); - frames.clear(); + mouseManager.unregisterMouseListener(this); + frameTimingsStore.clear(); + deactivateDetachedPanels(); } } - private String format(String format, Object... args) { - sb.setLength(0); - formatter.format(format, args); - return sb.toString(); + private void syncDetachedPanels() { + int detachedIndex = 0; + for (FrameTimerUI.Tab tab : FrameTimerUI.Tab.values()) { + if (!tab.isDetachable()) + continue; + + if (ui.isDetached(tab)) { + DetachedPanel panel = detachedPanels.computeIfAbsent(tab, DetachedPanel::new); + if (!panel.isActive()) { + panel.setLocationOffset(detachedIndex); + panel.setActive(true); + } + detachedIndex++; + } else { + DetachedPanel panel = detachedPanels.get(tab); + if (panel != null) + panel.setActive(false); + } + } + + if (ui.isSettingsDetached()) { + if (detachedSettingsPanel == null) + detachedSettingsPanel = new DetachedSettingsPanel(); + if (!detachedSettingsPanel.isActive()) { + detachedSettingsPanel.setLocationOffset(detachedIndex); + detachedSettingsPanel.setActive(true); + } + } else if (detachedSettingsPanel != null) { + detachedSettingsPanel.setActive(false); + } + } + + private void deactivateDetachedPanels() { + for (DetachedPanel panel : detachedPanels.values()) + panel.setActive(false); + if (detachedSettingsPanel != null) + detachedSettingsPanel.setActive(false); } @Override public void onFrameCompletion(FrameTimings timings) { - long now = System.currentTimeMillis(); - while (!frames.isEmpty()) { - if (now - frames.peekFirst().frameTimestamp < 10e3) // remove older entries - break; - frames.removeFirst(); - } - frames.addLast(timings); + frameTimingsStore.onFrameCompletion(timings); } @Override public Dimension render(Graphics2D g) { long time = System.nanoTime(); - var boldFont = FontManager.getRunescapeBoldFont(); + + lineCache.syncGraphOverlayState(ui.isGraphEnabled()); + + int contentLineWidth = getHeader().computeContentLineWidth(g); + int panelWidth = getHeader().computePanelWidth(g) + PANEL_HORIZONTAL_PADDING; + ui.setLineWidth(contentLineWidth); + panelComponent.setPreferredSize(new Dimension(panelWidth, panelComponent.getPreferredSize().height)); + getHeader().setPreferredSize(new Dimension(panelWidth - PANEL_HORIZONTAL_PADDING, 0)); var children = panelComponent.getChildren(); + children.add(getHeader()); + if (!getAverageTimings()) { children.add(TitleComponent.builder() .text("Waiting for data...") .build()); - } else { - long cpuTime = timings[Timer.DRAW_FRAME.ordinal()]; - long asyncCpuTime = 0; - addTiming("CPU", cpuTime, true); - for (var t : Timer.TIMERS) { - if (t.isCpuTimer() && t != Timer.DRAW_FRAME) - addTiming(t, timings); - if (t.isAsyncCpuTimer()) - asyncCpuTime += timings[t.ordinal()]; - } + } else if (!ui.isInlineSettingsOpen()) { + renderTab(ui.getSelectedTab(), panelComponent, lineCache, timings, cpuLoad, contentLineWidth); + } - addTiming("Async", asyncCpuTime, true); - for (var t : Timer.TIMERS) - if (t.isAsyncCpuTimer()) - addTiming(t, timings); + var result = super.render(g); + mainPanelWidth = panelWidth; + mainContentLineWidth = contentLineWidth; + if (result.height > 0) + mainPanelHeight = result.height; + updateHoveredLine(); + frameTimer.cumulativeError += System.nanoTime() - time; + return result; + } - if (cpuLoad > 0) { - children.add(LineComponent.builder() - .left("CPU Load:") - .right((int) (cpuLoad * 100) + "%") - .build()); - } + private void updateHoveredLine() { + hoveredTimer = null; - long gpuTime = timings[Timer.RENDER_FRAME.ordinal()]; - addTiming("GPU", gpuTime, true); - for (var t : Timer.TIMERS) - if (t.isGpuTimer() && t != Timer.RENDER_FRAME) - addTiming(t, timings); - - children.add(LineComponent.builder() - .leftFont(boldFont) - .left("Estimated bottleneck:") - .rightFont(boldFont) - .right(cpuTime > gpuTime ? "CPU" : "GPU") - .build()); + var mouse = client.getMouseCanvasPosition(); + if (mouse.getX() < 0 || mouse.getY() < 0) + return; - children.add(LineComponent.builder() - .leftFont(boldFont) - .left("Estimated FPS:") - .rightFont(boldFont) - .right(format("%.1f FPS", 1e9 / max(cpuTime, gpuTime))) - .build()); + Rectangle panelBounds = getBounds(); + if (panelBounds.width <= 0 || panelBounds.height <= 0) + return; - children.add(LineComponent.builder() - .left("Error compensation:") - .right(format("%d ns", frameTimer.errorCompensation)) - .build()); + hoveredTimer = lineCache.getHoveredTimer( + panelComponent, + panelBounds.x, + panelBounds.y, + mouse.getX(), + mouse.getY() + ); + } - children.add(LineComponent.builder() - .left("Pooled array size:") - .right(formatBytes(PooledArrayType.getCurrentTotalCacheSize())) - .build()); + private boolean getAverageTimings() { + var frames = frameTimingsStore.getFrames(); + if (frames.isEmpty()) + return false; - children.add(LineComponent.builder() - .left("Garbage collection count:") - .right(String.valueOf(plugin.getGarbageCollectionCount())) - .build()); + Arrays.fill(timings, 0); + cpuLoad = 0; + for (var frame : frames) { + for (int i = 0; i < frame.timers.length; i++) + timings[i] += frame.timers[i]; + cpuLoad += frame.cpuLoad; + } - children.add(LineComponent.builder() - .left("Power saving mode:") - .right(plugin.isPowerSaving ? "ON" : "OFF") - .build()); + for (int i = 0; i < timings.length; i++) + timings[i] = Math.max(0, timings[i] / frames.size()); + cpuLoad /= frames.size(); - children.add(LineComponent.builder() - .leftFont(boldFont) - .left("Scene stats:") - .build()); + return true; + } - if (plugin.getSceneContext() != null) { - var sceneContext = plugin.getSceneContext(); - children.add(LineComponent.builder() - .left("Lights:") - .right(format("%d/%d", sceneContext.numVisibleLights, sceneContext.lights.size())) - .build()); - } + private void renderTab( + FrameTimerUI.Tab tab, + PanelComponent panel, + LineCache cache, + long[] timings, + float cpuLoad, + int lineWidth + ) { + cache.syncLineWidth(lineWidth); - if (plugin.renderer instanceof ZoneRenderer) { - children.add(LineComponent.builder() - .left("Dynamic renderables:") - .right(String.valueOf(plugin.getDrawnDynamicRenderableCount())) - .build()); + switch (tab) { + case ALL: + buildCpu(panel, cache, lineWidth, timings); + buildAsync(panel, cache, lineWidth, timings); + buildGpu(panel, cache, lineWidth, timings); + buildStats(panel, cache, lineWidth); + break; + case CPU: + buildCpu(panel, cache, lineWidth, timings); + break; + case ASYNC: + buildAsync(panel, cache, lineWidth, timings); + break; + case GPU: + buildGpu(panel, cache, lineWidth, timings); + break; + case STATS: + buildStats(panel, cache, lineWidth); + break; + } - children.add(LineComponent.builder() - .left("Temp renderables:") - .right(String.valueOf(plugin.getDrawnTempRenderableCount())) - .build()); - } else { - children.add(LineComponent.builder() - .left("Tiles:") - .right(String.valueOf(plugin.getDrawnTileCount())) - .build()); + panel.getChildren().add(LineComponent.builder() + .preferredSize(new Dimension(lineWidth, 6)) + .build()); - children.add(LineComponent.builder() - .left("Static renderables:") - .right(String.valueOf(plugin.getDrawnStaticRenderableCount())) - .build()); + buildSummary(panel, lineWidth, timings, cpuLoad); + buildSnapshot(panel, lineWidth); + } - children.add(LineComponent.builder() - .left("Dynamic renderables:") - .right(String.valueOf(plugin.getDrawnDynamicRenderableCount())) - .build()); + private void buildStats(PanelComponent panel, LineCache cache, int lineWidth) { + var boldFont = FontManager.getRunescapeBoldFont(); + addLine(panel, lineWidth, LineComponent.builder() + .leftFont(boldFont) + .left("Stats:")); - children.add(LineComponent.builder() - .left("NPC displacement cache size:") - .right(String.valueOf(npcDisplacementCache.size())) - .build()); - } + buildSceneContent(panel, lineWidth); + buildStreamingContent(panel, cache, lineWidth); + } - children.add(LineComponent.builder() - .leftFont(boldFont) - .left("Streaming Stats:") - .build()); + private void addLine(PanelComponent panel, int lineWidth, LineComponent.LineComponentBuilder builder) { + panel.getChildren().add(builder.preferredSize(new Dimension(lineWidth, 0)).build()); + } - WorldViewContext root = sceneManager.getRoot(); - addTiming("Root Scene Load", root.loadTime, false); - addTiming("Root Scene Upload", root.uploadTime, false); - addTiming("Root Scene Swap", root.sceneSwapTime, false); - - // TODO: Maybe this should be calculated somewhere else - int subSceneCount = 0; - long subSceneLoadTime = 0; - long subSceneUploadTime = 0; - long subSceneSwapTime = 0; - - for (int worldViewId = 0; worldViewId < MAX_WORLDVIEWS; worldViewId++) { - WorldViewContext subscene = sceneManager.getContext(worldViewId); - if (subscene != null) { - subSceneCount++; - subSceneLoadTime += subscene.loadTime; - subSceneUploadTime += subscene.uploadTime; - subSceneSwapTime += subscene.sceneSwapTime; - } - } + private void buildSummary(PanelComponent panel, int lineWidth, long[] timings, float cpuLoad) { + var boldFont = FontManager.getRunescapeBoldFont(); + long cpuTime = timings[Timer.DRAW_FRAME.ordinal()]; + long gpuTime = timings[Timer.RENDER_FRAME.ordinal()]; - if (subSceneCount > 0) { - addTiming("Avg SubScene Load", subSceneLoadTime / subSceneCount, false); - addTiming("Avg SubScene Upload", subSceneUploadTime / subSceneCount, false); - addTiming("Avg SubScene Swap", subSceneSwapTime / subSceneCount, false); - } + addLine(panel, lineWidth, LineComponent.builder() + .leftFont(boldFont) + .left("Estimated bottleneck:") + .rightFont(boldFont) + .right(cpuTime > gpuTime ? "CPU" : "GPU")); - children.add(LineComponent.builder() - .left("Sub Scene Count:") - .right(String.valueOf(subSceneCount)) - .build()); + addLine(panel, lineWidth, LineComponent.builder() + .leftFont(boldFont) + .left("Estimated FPS:") + .rightFont(boldFont) + .right(format("%.1f FPS", 1e9 / max(cpuTime, gpuTime)))); + addLine(panel, lineWidth, LineComponent.builder() + .left("Error compensation:") + .right(format("%d ns", frameTimer.errorCompensation))); - children.add(LineComponent.builder() - .left("Streaming Zones:") - .right(String.valueOf(jobSystem.getWorkQueueSize())) - .build()); + if (cpuLoad > 0) { + addLine(panel, lineWidth, LineComponent.builder() + .left("CPU Load:") + .right((int) (cpuLoad * 100) + "%")); + } - if (frameTimingsRecorder.isCapturingSnapshot()) - children.add(LineComponent.builder() - .leftFont(boldFont) - .left("Capturing Snapshot...") - .rightFont(boldFont) - .right(format("%d%%", frameTimingsRecorder.getProgressPercentage())) - .build()); + addLine(panel, lineWidth, LineComponent.builder() + .left("Pooled array size:") + .right(formatBytes(PooledArrayType.getCurrentTotalCacheSize()))); + + addLine(panel, lineWidth, LineComponent.builder() + .left("Garbage collection count:") + .right(String.valueOf(plugin.getGarbageCollectionCount()))); + + addLine(panel, lineWidth, LineComponent.builder() + .left("Power saving mode:") + .right(plugin.isPowerSaving ? "ON" : "OFF")); + + if (!frameTimingsStore.isCapturing()) { + var boldFont2 = FontManager.getRunescapeBoldFont(); + addLine(panel, lineWidth, LineComponent.builder() + .leftFont(boldFont2) + .left("Live capture:") + .rightFont(boldFont2) + .right("FROZEN")); } + } - var result = super.render(g); - frameTimer.cumulativeError += System.nanoTime() - time; - return result; + private void buildCpu(PanelComponent panel, LineCache cache, int lineWidth, long[] timings) { + long cpuTime = timings[Timer.DRAW_FRAME.ordinal()]; + addTiming(panel, cache, lineWidth, "CPU", cpuTime, true, Timer.DRAW_FRAME.color, Timer.DRAW_FRAME); + for (var t : Timer.TIMERS) { + if (t.isCpuTimer() && t != Timer.DRAW_FRAME) + addTiming(panel, cache, lineWidth, t, timings); + } } - private boolean getAverageTimings() { - if (frames.isEmpty()) - return false; + private void buildAsync(PanelComponent panel, LineCache cache, int lineWidth, long[] timings) { + long asyncCpuTime = 0; + for (var t : Timer.TIMERS) + if (t.isAsyncCpuTimer()) + asyncCpuTime += timings[t.ordinal()]; - Arrays.fill(timings, 0); - cpuLoad = 0; - for (var frame : frames) { - for (int i = 0; i < frame.timers.length; i++) - timings[i] += frame.timers[i]; - cpuLoad += frame.cpuLoad; + addTiming(panel, cache, lineWidth, "Async", asyncCpuTime, true, null, null); + for (var t : Timer.TIMERS) + if (t.isAsyncCpuTimer()) + addTiming(panel, cache, lineWidth, t, timings); + } + + private void buildGpu(PanelComponent panel, LineCache cache, int lineWidth, long[] timings) { + long gpuTime = timings[Timer.RENDER_FRAME.ordinal()]; + addTiming(panel, cache, lineWidth, "GPU", gpuTime, true, Timer.RENDER_FRAME.color, Timer.RENDER_FRAME); + for (var t : Timer.TIMERS) + if (t.isGpuTimer() && t != Timer.RENDER_FRAME) + addTiming(panel, cache, lineWidth, t, timings); + } + + private void buildSceneContent(PanelComponent panel, int lineWidth) { + if (plugin.getSceneContext() != null) { + var sceneContext = plugin.getSceneContext(); + addLine(panel, lineWidth, LineComponent.builder() + .left("Lights:") + .right(format("%d/%d", sceneContext.numVisibleLights, sceneContext.lights.size()))); } - for (int i = 0; i < timings.length; i++) - timings[i] = max(0, timings[i] / frames.size()); - cpuLoad /= frames.size(); + if (plugin.renderer instanceof ZoneRenderer) { + addLine(panel, lineWidth, LineComponent.builder() + .left("Dynamic renderables:") + .right(String.valueOf(plugin.getDrawnDynamicRenderableCount()))); - return true; + addLine(panel, lineWidth, LineComponent.builder() + .left("Temp renderables:") + .right(String.valueOf(plugin.getDrawnTempRenderableCount()))); + } else { + addLine(panel, lineWidth, LineComponent.builder() + .left("Tiles:") + .right(String.valueOf(plugin.getDrawnTileCount()))); + + addLine(panel, lineWidth, LineComponent.builder() + .left("Static renderables:") + .right(String.valueOf(plugin.getDrawnStaticRenderableCount()))); + + addLine(panel, lineWidth, LineComponent.builder() + .left("Dynamic renderables:") + .right(String.valueOf(plugin.getDrawnDynamicRenderableCount()))); + + addLine(panel, lineWidth, LineComponent.builder() + .left("NPC displacement cache size:") + .right(String.valueOf(npcDisplacementCache.size()))); + } } - private void addTiming(Timer timer, long[] timings) { - addTiming(timer.name, timings[timer.ordinal()], false); + private void buildStreamingContent(PanelComponent panel, LineCache cache, int lineWidth) { + WorldViewContext root = sceneManager.getRoot(); + addTiming(panel, cache, lineWidth, "Root Scene Load", root.loadTime, false, null, null); + addTiming(panel, cache, lineWidth, "Root Scene Upload", root.uploadTime, false, null, null); + addTiming(panel, cache, lineWidth, "Root Scene Swap", root.sceneSwapTime, false, null, null); + + int subSceneCount = 0; + long subSceneLoadTime = 0; + long subSceneUploadTime = 0; + long subSceneSwapTime = 0; + + for (int worldViewId = 0; worldViewId < MAX_WORLDVIEWS; worldViewId++) { + WorldViewContext subscene = sceneManager.getContext(worldViewId); + if (subscene != null) { + subSceneCount++; + subSceneLoadTime += subscene.loadTime; + subSceneUploadTime += subscene.uploadTime; + subSceneSwapTime += subscene.sceneSwapTime; + } + } + + if (subSceneCount > 0) { + addTiming(panel, cache, lineWidth, "Avg SubScene Load", subSceneLoadTime / subSceneCount, false, null, null); + addTiming(panel, cache, lineWidth, "Avg SubScene Upload", subSceneUploadTime / subSceneCount, false, null, null); + addTiming(panel, cache, lineWidth, "Avg SubScene Swap", subSceneSwapTime / subSceneCount, false, null, null); + } + + addLine(panel, lineWidth, LineComponent.builder() + .left("Sub Scene Count:") + .right(String.valueOf(subSceneCount))); + + addLine(panel, lineWidth, LineComponent.builder() + .left("Streaming Zones:") + .right(String.valueOf(jobSystem.getWorkQueueSize()))); + } + + private void buildSnapshot(PanelComponent panel, int lineWidth) { + if (!frameTimingsRecorder.isCapturingSnapshot()) + return; + + var boldFont = FontManager.getRunescapeBoldFont(); + addLine(panel, lineWidth, LineComponent.builder() + .leftFont(boldFont) + .left("Recording...") + .rightFont(boldFont) + .right(format( + "%ds left (%d%%)", + frameTimingsRecorder.getRemainingSeconds(), + frameTimingsRecorder.getProgressPercentage() + ))); } - private void addTiming(String name, long nanos, boolean bold) { + private void addTiming(PanelComponent panel, LineCache cache, int lineWidth, Timer timer, long[] timings) { + addTiming(panel, cache, lineWidth, timer.name, timings[timer.ordinal()], false, timer.color, timer); + } + + private void addTiming( + PanelComponent panel, + LineCache cache, + int lineWidth, + String name, + long nanos, + boolean bold, + @Nullable Color swatchColor, + @Nullable Timer timer + ) { if (nanos == 0) return; - // Round timers to zero if they are less than a microsecond off String result = "~0 ms"; if (abs(nanos) > 1e3) { sb.setLength(0); result = sb.append(round(nanos / 1e3) / 1e3).append(" ms").toString(); } - LineComponent component = componentMap.get(name); - if (component == null) { + boolean showSwatch = swatchColor != null && frameTimerGraphOverlay.isActive(); + int textLineWidth = showSwatch ? lineWidth - SWATCH_WIDTH - SWATCH_GAP : lineWidth; + + TimerLineEntry entry = cache.lineEntries.get(name); + if (entry == null || entry.hasSwatch != showSwatch) { var font = bold ? FontManager.getRunescapeBoldFont() : FontManager.getRunescapeFont(); - component = LineComponent.builder() - .left(name + ":") + LineComponent lineComponent = LineComponent.builder() .leftFont(font) .right(result) .rightFont(font) + .left(name + ":") + .preferredSize(new Dimension(textLineWidth, 0)) .build(); - componentMap.put(name, component); + + LayoutableRenderableEntity component = lineComponent; + + if (showSwatch) { + component = SplitComponent.builder() + .orientation(ComponentOrientation.HORIZONTAL) + .first(new SwatchComponent(swatchColor)) + .second(lineComponent) + .gap(new Point(SWATCH_GAP, 0)) + .preferredSize(new Dimension(lineWidth, 0)) + .build(); + } + + entry = new TimerLineEntry(timer, component, lineComponent, showSwatch, textLineWidth); + cache.lineEntries.put(name, entry); } else { - component.setRight(result); + entry.lineComponent.setRight(result); + entry.lineComponent.setPreferredSize(new Dimension(textLineWidth, 0)); } - panelComponent.getChildren().add(component); + panel.getChildren().add(entry.component); + } + + private String format(String format, Object... args) { + sb.setLength(0); + formatter.format(format, args); + return sb.toString(); + } + + @Override + public MouseEvent mouseClicked(MouseEvent e) { + return e; + } + + @Override + public MouseEvent mousePressed(MouseEvent e) { + if (!overlayActive) + return e; + + Rectangle panelBounds = getBounds(); + if (panelBounds.width <= 0 || panelBounds.height <= 0) + return e; + + var mouse = client.getMouseCanvasPosition(); + if (mouse.getX() < 0 || mouse.getY() < 0) + return e; + + int localX = mouse.getX() - panelBounds.x; + int localY = mouse.getY() - panelBounds.y; + + Rectangle headerBounds = getHeader().getBounds(); + if (headerBounds.width <= 0 || headerBounds.height <= 0) + return e; + + int headerX = localX - headerBounds.x; + int headerY = localY - headerBounds.y; + if (headerX < 0 || headerY < 0 || headerX >= headerBounds.width || headerY >= headerBounds.height) + return e; + + boolean middleClick = SwingUtilities.isMiddleMouseButton(e); + + if (getHeader().handleClick(headerX, headerY, middleClick)) + e.consume(); + + return e; + } + + @Override + public MouseEvent mouseReleased(MouseEvent e) { + return e; + } + + @Override + public MouseEvent mouseEntered(MouseEvent e) { + return e; + } + + @Override + public MouseEvent mouseExited(MouseEvent e) { + return e; + } + + @Override + public MouseEvent mouseDragged(MouseEvent e) { + return e; + } + + @Override + public MouseEvent mouseMoved(MouseEvent e) { + return e; + } + + static final class LineCache { + private final IdentityHashMap lineEntries = new IdentityHashMap<>(); + private boolean graphOverlayActive; + private int lineWidth; + + void syncLineWidth(int lineWidth) { + if (lineWidth != this.lineWidth) { + this.lineWidth = lineWidth; + lineEntries.clear(); + } + } + + void syncGraphOverlayState(boolean graphActive) { + if (graphActive != graphOverlayActive) { + lineEntries.clear(); + graphOverlayActive = graphActive; + } + } + + @Nullable + Timer getHoveredTimer(PanelComponent panel, int panelX, int panelY, int mouseX, int mouseY) { + for (var entry : lineEntries.values()) { + if (entry.timer == null) + continue; + + Rectangle lineBounds = entry.component.getBounds(); + if (lineBounds.width <= 0 || lineBounds.height <= 0) + continue; + + lineBounds = new Rectangle( + panelX + lineBounds.x, + panelY + lineBounds.y, + lineBounds.width, + lineBounds.height + ); + + if (lineBounds.contains(mouseX, mouseY)) + return entry.timer; + } + return null; + } + } + + @RequiredArgsConstructor + static final class TimerLineEntry { + final Timer timer; + final LayoutableRenderableEntity component; + final LineComponent lineComponent; + final boolean hasSwatch; + final int textLineWidth; + } + + private static class Header implements LayoutableRenderableEntity { + private static final int BUTTON_PADDING_X = 6; + private static final int TAB_GAP = 2; + private static final int SETTINGS_GAP = 4; + static final int SETTINGS_MENU_MIN_WIDTH = 230; + private static final int MENU_PADDING = 6; + private static final int SECTION_GAP = 4; + + static final Color SETTINGS_BACKGROUND = new Color(25, 25, 25, 230); + private static final Color SETTINGS_BORDER = new Color(100, 100, 100, 200); + private static final Color MENU_TEXT = Color.WHITE; + private static final Color MENU_MUTED = new Color(160, 160, 160); + private static final Color MENU_ON = new Color(120, 220, 120); + private static final Color MENU_OFF = new Color(220, 120, 120); + private static final Color MENU_ACTION = new Color(140, 190, 255); + + private final FrameTimerUI state; + private final FrameTimingsRecorder recorder; + private final FrameTimingsStore timingsStore; + private final boolean settingsOverlay; + private final Consumer onTabSelected; + private final Runnable onSettingsToggle; + private final Runnable onSettingsDetach; + private final Runnable onSettingsDock; + private final Consumer onTabDetached; + private final Runnable onGraphToggle; + private final Consumer onTabVisibilityToggle; + private final Consumer onTabAttach; + + private final Rectangle bounds = new Rectangle(); + private final List hitRegions = new ArrayList<>(); + + @Getter + private final List