FPS Plugin
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (c) 2017, Levi <me@levischuck.com>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (c) 2017, Levi <me@levischuck.com>
|
||||
* 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<BufferedImage>
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2017, Levi <me@levischuck.com>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (c) 2017, Levi <me@levischuck.com>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (c) 2017, Levi <me@levischuck.com>
|
||||
* 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user