FPS Plugin

This commit is contained in:
Levi
2018-05-04 20:45:33 -05:00
committed by Adam
parent 9e758fbded
commit 6129d36028
5 changed files with 486 additions and 0 deletions

View File

@@ -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;
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}