diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsConfig.java new file mode 100644 index 0000000000..114c005fd7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsConfig.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2017, Levi + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.fps; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup( + keyName = FpsPlugin.CONFIG_GROUP_KEY, + name = "FPS Control", + description = "Lets you control what your game frame rate is, often helps keep CPU down too" +) +public interface FpsConfig extends Config +{ + @ConfigItem( + keyName = "limitMode", + name = "Limit Mode", + description = "Stay at or under the target frames per second even when in this mode", + position = 1 + ) + default FpsLimitMode limitMode() + { + return FpsLimitMode.NEVER; + } + + @ConfigItem( + keyName = "maxFps", + name = "FPS target", + description = "Desired max frames per second", + position = 2 + ) + default int maxFps() + { + return 50; + } + + @ConfigItem( + keyName = "drawFps", + name = "Draw FPS indicator", + description = "Show a number in the corner for the current FPS", + position = 3 + ) + default boolean drawFps() + { + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsDrawListener.java b/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsDrawListener.java new file mode 100644 index 0000000000..1deade0ea7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsDrawListener.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2017, Levi + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.fps; + +import java.awt.image.BufferedImage; +import java.util.function.Consumer; +import javax.inject.Inject; +import net.runelite.api.events.FocusChanged; + +/** + * FPS Draw Listener runs after the canvas has been painted with the buffered image (unused in this plugin) + * It specifically is designed only to pause when RS decides to paint the canvas, after the canvas has been painted. + * The RS client operates at 50 cycles happen per second in the RS Client with higher priority than draws per second. + * For high powered computers, drawing is limited by client cycles, so 50 is the max FPS observed. + * After running a game cycle and a draw operation, the RS client burns the rest of the time with a nano timer. + * For low powered computers, the RS client is often throttled by the hardware or OS and draws at 25-30 fps. + * The nano timer is not used in this scenario. + * Instead to catch up the RS client runs several cycles before drawing, thus maintaining 50 cycles / second. + * + * Enforcing FPS in the draw code does not impact the client engine's ability to run including its audio, + * even when forced to 1 FPS with this plugin. + */ +public class FpsDrawListener implements Consumer +{ + private static final int SAMPLE_SIZE = 4; + + private final FpsConfig config; + + private long targetDelay = 0; + + // Often changing values + private boolean isFocused = true; + + // Working set + private long lastMillis = 0; + private long[] lastDelays = new long[SAMPLE_SIZE]; + private int lastDelayIndex = 0; + private long sleepDelay = 0; + + @Inject + private FpsDrawListener(FpsConfig config) + { + this.config = config; + reloadConfig(); + } + + void reloadConfig() + { + lastMillis = System.currentTimeMillis(); + targetDelay = 1000 / Math.max(1, config.maxFps()); + sleepDelay = targetDelay; + + for (int i = 0; i < SAMPLE_SIZE; i++) + { + lastDelays[i] = targetDelay; + } + } + + void onFocusChanged(FocusChanged event) + { + this.isFocused = event.isFocused(); + } + + private boolean isEnforced() + { + return FpsLimitMode.ALWAYS == config.limitMode() + || (FpsLimitMode.UNFOCUSED == config.limitMode() && !isFocused); + } + + @Override + public void accept(BufferedImage bufferedImage) + { + + if (!isEnforced()) + { + return; + } + + // We can't trust client.getFPS to get frame-perfect FPS knowledge + // If we do try to use client.getFPS, we will end up oscillating + // So we rely on currentTimeMillis which is occasionally cached by the JVM unlike nanotime + // Its caching will not cause oscillation as it is granular enough for our uses here + final long before = lastMillis; + final long now = System.currentTimeMillis(); + + lastMillis = now; + lastDelayIndex = (lastDelayIndex + 1) % SAMPLE_SIZE; + lastDelays[lastDelayIndex] = now - before; + + // We take a sampling approach because sometimes the game client seems to repaint + // after only running 1 game cycle, and then runs repaint again after running 30 cycles + long averageDelay = 0; + for (int i = 0; i < SAMPLE_SIZE; i++) + { + averageDelay += lastDelays[i]; + } + averageDelay /= lastDelays.length; + + // Sleep delay is a moving target due to the nature of how and when the engine + // decides to run cycles. + // This will also keep us safe from time spent in plugins conditionally + // as some plugins and overlays are only appropriate in some game areas + if (averageDelay > targetDelay) + { + sleepDelay--; + } + else if (averageDelay < targetDelay) + { + sleepDelay++; + } + + if (sleepDelay > 0) + { + try + { + Thread.sleep(sleepDelay); + } + catch (InterruptedException e) + { + // Can happen on shutdown + } + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsLimitMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsLimitMode.java new file mode 100644 index 0000000000..8536f7a67a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsLimitMode.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2017, Levi + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.fps; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum FpsLimitMode +{ + NEVER("Never"), + UNFOCUSED("Unfocused"), + ALWAYS("Always"); + + private final String name; + + @Override + public String toString() + { + return name; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsOverlay.java new file mode 100644 index 0000000000..29c09a13e7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsOverlay.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2017, Levi + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.fps; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.Point; +import net.runelite.api.events.FocusChanged; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayPriority; +import net.runelite.client.ui.overlay.OverlayUtil; + +/** + * The built in FPS overlay has a few problems that this one does not have, most of all: it is distracting. + * 1. The built in one also shows memory, which constantly fluctuates and garbage collects. + * It is useful for devs profiling memory. + * 2. The built in one shifts around constantly because it is not monospace. + * This locks "FPS:" into one position (the far top right corner of the canvas), + * along with a locked position for the FPS value. + */ +public class FpsOverlay extends Overlay +{ + private static final int MAX_FPS = 50; + private static final int FPS_SIZE = MAX_FPS + 1; + private static final int Y_OFFSET = 14; + private static final int VALUE_X_OFFSET = 15; + + // Cache of FPS number strings from 00-50 + private final String[] fpsNums; + + // Local dependencies + private final FpsConfig config; + private final Client client; + + // Often changing values + private boolean isFocused = true; + + @Inject + private FpsOverlay(FpsConfig config, Client client) + { + this.config = config; + this.client = client; + setLayer(OverlayLayer.ABOVE_WIDGETS); + setPriority(OverlayPriority.HIGH); + setPosition(OverlayPosition.DYNAMIC); + + // Populate pre-allocated strings of FPS, these are constant and there's no reason + // to create additional garbage + // FPS should never exceed 50, we have 0-50 (51 entries) + String[] fpsNums = new String[FPS_SIZE]; + for (int i = 0; i < FPS_SIZE; i++) + { + fpsNums[i] = String.format("%02d", i); + } + this.fpsNums = fpsNums; + + } + + void onFocusChanged(FocusChanged event) + { + isFocused = event.isFocused(); + } + + private boolean isEnforced() + { + return FpsLimitMode.ALWAYS == config.limitMode() + || (FpsLimitMode.UNFOCUSED == config.limitMode() && !isFocused); + } + + private Color getFpsValueColor() + { + return isEnforced() ? Color.red : Color.yellow; + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (!config.drawFps()) + { + return null; + } + + final int fps = client.getFPS(); + if (fps < FPS_SIZE) + { + final int width = client.getCanvas().getWidth(); + final Point point = new Point(width - VALUE_X_OFFSET, Y_OFFSET); + + OverlayUtil.renderTextLocation(graphics, point, fpsNums[fps], getFpsValueColor()); + } + + return null; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsPlugin.java new file mode 100644 index 0000000000..cb2abf4b84 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/fps/FpsPlugin.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2017, Levi + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.fps; + +import com.google.common.eventbus.Subscribe; +import com.google.inject.Inject; +import com.google.inject.Provides; +import net.runelite.api.events.ConfigChanged; +import net.runelite.api.events.FocusChanged; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.DrawManager; +import net.runelite.client.ui.overlay.Overlay; + +/** + * FPS Control has two primary areas, this plugin class just keeps those areas up to date and handles setup / teardown. + * + *

Overlay paints the current FPS, the color depends on whether or not FPS is being enforced. + * The overlay is lightweight and is merely and indicator. + * + *

Draw Listener, sleeps a calculated amount after each canvas paint operation. + * This is the heart of the plugin, the amount of sleep taken is regularly adjusted to account varying + * game and system load, it usually finds the sweet spot in about two seconds. + */ +@PluginDescriptor( + name = "FPS Control", + enabledByDefault = false +) +public class FpsPlugin extends Plugin +{ + static final String CONFIG_GROUP_KEY = "fpscontrol"; + + @Inject + private FpsOverlay overlay; + + @Inject + private FpsDrawListener drawListener; + + @Inject + private DrawManager drawManager; + + @Provides + FpsConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(FpsConfig.class); + } + + @Override + public Overlay getOverlay() + { + return overlay; + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (event.getGroup().equals(CONFIG_GROUP_KEY)) + { + drawListener.reloadConfig(); + } + } + + @Subscribe + public void onFocusChanged(FocusChanged event) + { + drawListener.onFocusChanged(event); + overlay.onFocusChanged(event); + } + + @Override + protected void startUp() throws Exception + { + drawManager.registerEveryFrameListener(drawListener); + } + + @Override + protected void shutDown() throws Exception + { + drawManager.unregisterEveryFrameListener(drawListener); + } +}