LWJGL 3 Limit refresh rate

Started by Maineri, August 15, 2017, 17:42:13

Previous topic - Next topic

Maineri

I am new to LWJGL 3 and I am having hard time limit the fps as I have no experience in glfw. Docs state the following:

QuoteGLFW_REFRESH_RATE specifies the desired refresh rate for full screen windows. If set to GLFW_DONT_CARE, the highest available refresh rate will be used. This hint is ignored for windowed mode windows.

So how do I limit the frame rate in windowed mode?

Kai

You cannot limit the monitor refresh rate in windowed mode. The monitor refresh rate will always be the desktop refresh rate (typically 59 or 60 Hz). You can only limit the rate at which you are swapping the back/front buffers to an integer fraction of the monitor refresh rate. So that your game will be swapping buffers also at 59 or 60Hz, or fractions of it, such as 30Hz or 15Hz. Check out glfwSwapInterval().

Maineri

Isn't glfwSwapInterval just to turn v-sync on/off. What if I wan't something like 120 or more. I thought I would use it to limit the updates of the whole engine. In LWJGL 2 limiting fps slowed down everything else too. Is this possible in with glfw or do I need to build my own system for it?

Kai

Oh, okay. You are referring to LWJGL 2 and specifically probably the Display.sync() method. Yeah, there is no such thing in LWJGL 3. You have to roll your own.

kappa

It can be a little tricky to roll your own especially if you are new to Java and not aware of some of the quirks with its Timers. LWJGL2's Display.sync() method just points to the Sync class which is mostly standalone from the rest of LWJGL2. It can pretty easily be adapted for use with LWJGL3 as follows:

/*
 * Copyright (c) 2002-2012 LWJGL Project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * 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.
 *
 * * Neither the name of 'LWJGL' nor the names of
 *   its contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * 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 org.mypackage;

import static org.lwjgl.glfw.GLFW.glfwGetTime;

/**
* A highly accurate sync method that continually adapts to the system 
* it runs on to provide reliable results.
*
* @author Riven
* @author kappaOne
*/
class Sync {
	
	/** number of nano seconds in a second */
	private static final long NANOS_IN_SECOND = 1000L * 1000L * 1000L;

	/** The time to sleep/yield until the next frame */
	private long nextFrame = 0;
	
	/** whether the initialisation code has run */
	private boolean initialised = false;
	
	/** for calculating the averages the previous sleep/yield times are stored */
	private RunningAvg sleepDurations = new RunningAvg(10);
	private RunningAvg yieldDurations = new RunningAvg(10);
	
	public Sync() {
		
	}
	
	/**
	 * An accurate sync method that will attempt to run at a constant frame rate.
	 * It should be called once every frame.
	 * 
	 * @param fps - the desired frame rate, in frames per second
	 */
	public void sync(int fps) {
		if (fps <= 0) return;
		if (!initialised) initialise();
		
		try {
			// sleep until the average sleep time is greater than the time remaining till nextFrame
			for (long t0 = getTime(), t1; (nextFrame - t0) > sleepDurations.avg(); t0 = t1) {
				Thread.sleep(1);
				sleepDurations.add((t1 = getTime()) - t0); // update average sleep time
			}
	
			// slowly dampen sleep average if too high to avoid yielding too much
			sleepDurations.dampenForLowResTicker();
	
			// yield until the average yield time is greater than the time remaining till nextFrame
			for (long t0 = getTime(), t1; (nextFrame - t0) > yieldDurations.avg(); t0 = t1) {
				Thread.yield();
				yieldDurations.add((t1 = getTime()) - t0); // update average yield time
			}
		} catch (InterruptedException e) {
			
		}
		
		// schedule next frame, drop frame(s) if already too late for next frame
		nextFrame = Math.max(nextFrame + NANOS_IN_SECOND / fps, getTime());
	}
	
	/**
	 * This method will initialise the sync method by setting initial
	 * values for sleepDurations/yieldDurations and nextFrame.
	 * 
	 * If running on windows it will start the sleep timer fix.
	 */
	private void initialise() {
		initialised = true;
		
		sleepDurations.init(1000 * 1000);
		yieldDurations.init((int) (-(getTime() - getTime()) * 1.333));
		
		nextFrame = getTime();
		
		String osName = System.getProperty("os.name");
		
		if (osName.startsWith("Win")) {
			// On windows the sleep functions can be highly inaccurate by 
			// over 10ms making in unusable. However it can be forced to 
			// be a bit more accurate by running a separate sleeping daemon
			// thread.
			Thread timerAccuracyThread = new Thread(new Runnable() {
				public void run() {
					try {
						Thread.sleep(Long.MAX_VALUE);
					} catch (Exception e) {}
				}
			});
			
			timerAccuracyThread.setName("LWJGL3 Timer");
			timerAccuracyThread.setDaemon(true);
			timerAccuracyThread.start();
		}
	}

	/**
	 * Get the system time in nano seconds
	 * 
	 * @return will return the current time in nano's
	 */
	private long getTime() {
		return (long)(glfwGetTime() * NANOS_IN_SECOND);
	}

	private class RunningAvg {
		private final long[] slots;
		private int offset;
		
		private static final long DAMPEN_THRESHOLD = 10 * 1000L * 1000L; // 10ms
		private static final float DAMPEN_FACTOR = 0.9f; // don't change: 0.9f is exactly right!

		public RunningAvg(int slotCount) {
			this.slots = new long[slotCount];
			this.offset = 0;
		}

		public void init(long value) {
			while (this.offset < this.slots.length) {
				this.slots[this.offset++] = value;
			}
		}

		public void add(long value) {
			this.slots[this.offset++ % this.slots.length] = value;
			this.offset %= this.slots.length;
		}

		public long avg() {
			long sum = 0;
			for (int i = 0; i < this.slots.length; i++) {
				sum += this.slots[i];
			}
			return sum / this.slots.length;
		}
		
		public void dampenForLowResTicker() {
			if (this.avg() > DAMPEN_THRESHOLD) {
				for (int i = 0; i < this.slots.length; i++) {
					this.slots[i] *= DAMPEN_FACTOR;
				}
			}
		}
	}
}


Just call Sync.sync(int fps) on your Sync object in your game loop and it should just function as Display.sync(int fps) did. You could also modify the above code into a static class (like LWJGL2) if you don't want to create an object of Sync every time and only have one main loop/window.

voidburn

I was curious as to why a sleeper deamonized thread was needed to accomplish this, and found the following information about it:

To try and alleviate the low granularity, the VM actually makes a special call to set the interrupt period to 1ms while any Java thread is sleeping, if it requests a sleep interval that is not a multiple of 10ms. This means you are actually making a system-wide change to interrupt behavior (although generally not such a problem).

Generally, the solution works well enough, but: there is also a bug in Windows (XP, 2000, 2003) whereby repeatedly altering the interrupt period (and hence, repeatedly sleeping in Java for periods that are not multiples of 10ms)can make the system clock run faster than normal. Hotspot assumes that the default interrupt period is 10ms, but on some hardware it is 15ms.

If timing is crucial to your application, then an inelegant but practical way to get round these bugs is to leave a daemon thread running throughout the duration of your application that simply sleeps for a large prime number of milliseconds (Long.MAX_VALUE for example). This way, the interrupt period will be set once per invocation of your application, minimizing the effect on the system clock, and setting the sleep granularity to 1ms even where the default interrupt period isn't 15ms.

REFERENCES:
1) http://javamex.com/tutorials/threads/sleep_issues.shtml
2) https://support.microsoft.com/en-us/help/821893/the-system-clock-may-run-fast-when-you-use-the-acpi-power-management-t