Skip to content

Commit 9aeff56

Browse files
riccardoblcodex128
andcommitted
fix LWJGL3 Canvas Frame Rate
Co-authored-by: gpt-5.6-sol <103840984+codex128@users.noreply.github.com> Co-authored-by: gpt-5.6-luna <103840984+codex128@users.noreply.github.com>
1 parent 0574c2f commit 9aeff56

6 files changed

Lines changed: 363 additions & 14 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright (c) 2009-2026 jMonkeyEngine
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are
7+
* met:
8+
*
9+
* * Redistributions of source code must retain the above copyright
10+
* notice, this list of conditions and the following disclaimer.
11+
*
12+
* * Redistributions in binary form must reproduce the above copyright
13+
* notice, this list of conditions and the following disclaimer in the
14+
* documentation and/or other materials provided with the distribution.
15+
*
16+
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17+
* may be used to endorse or promote products derived from this software
18+
* without specific prior written permission.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22+
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23+
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24+
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25+
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26+
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27+
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28+
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29+
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31+
*/
32+
package com.jme3.system.lwjgl;
33+
34+
import java.awt.EventQueue;
35+
import java.util.concurrent.TimeUnit;
36+
import java.util.concurrent.atomic.AtomicBoolean;
37+
import java.util.concurrent.locks.LockSupport;
38+
import java.util.function.Consumer;
39+
import java.util.function.LongConsumer;
40+
import java.util.function.LongSupplier;
41+
42+
/**
43+
* Prevents an unbounded AWT canvas render loop from starving Swing's event
44+
* dispatch thread without imposing a fixed frame-rate limit.
45+
*/
46+
final class AwtEventQueueFairness {
47+
48+
static final long PROBE_INTERVAL_NANOS = TimeUnit.MILLISECONDS.toNanos(4);
49+
static final long EDT_STALL_NANOS = TimeUnit.MILLISECONDS.toNanos(2);
50+
static final long BACKOFF_NANOS = TimeUnit.MICROSECONDS.toNanos(100);
51+
52+
private final LongSupplier nanoTime;
53+
private final Consumer<Runnable> edtExecutor;
54+
private final LongConsumer parker;
55+
private final AtomicBoolean probePending = new AtomicBoolean();
56+
57+
private long lastProbeNanos = Long.MIN_VALUE;
58+
private volatile long pendingSinceNanos;
59+
60+
AwtEventQueueFairness() {
61+
this(System::nanoTime, EventQueue::invokeLater, LockSupport::parkNanos);
62+
}
63+
64+
AwtEventQueueFairness(LongSupplier nanoTime, Consumer<Runnable> edtExecutor,
65+
LongConsumer parker) {
66+
this.nanoTime = nanoTime;
67+
this.edtExecutor = edtExecutor;
68+
this.parker = parker;
69+
}
70+
71+
static boolean isRequired(boolean vsyncPacedFrame, int frameRateLimit) {
72+
return !vsyncPacedFrame && frameRateLimit <= 0;
73+
}
74+
75+
void afterFrame() {
76+
long now = nanoTime.getAsLong();
77+
if (probePending.get()) {
78+
if (now - pendingSinceNanos >= EDT_STALL_NANOS) {
79+
parker.accept(BACKOFF_NANOS);
80+
}
81+
return;
82+
}
83+
84+
if (lastProbeNanos != Long.MIN_VALUE
85+
&& now - lastProbeNanos < PROBE_INTERVAL_NANOS) {
86+
return;
87+
}
88+
89+
if (probePending.compareAndSet(false, true)) {
90+
lastProbeNanos = now;
91+
pendingSinceNanos = now;
92+
try {
93+
edtExecutor.accept(() -> probePending.set(false));
94+
} catch (RuntimeException exception) {
95+
probePending.set(false);
96+
}
97+
}
98+
}
99+
}

jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import java.util.HashMap;
6363
import java.util.Map;
6464
import java.util.concurrent.atomic.AtomicBoolean;
65+
import java.util.function.BooleanSupplier;
6566
import java.util.function.Consumer;
6667
import java.util.logging.Level;
6768
import java.util.logging.Logger;
@@ -113,6 +114,18 @@ public class LwjglCanvas extends LwjglWindow implements JmeCanvasContext, Runnab
113114
/** Logger class. */
114115
private static final Logger LOGGER = Logger.getLogger(LwjglCanvas.class.getName());
115116

117+
static boolean swapBuffersAndCheckNativeVsync(boolean nativeVsyncEnabled,
118+
BooleanSupplier swapBuffers) {
119+
boolean swapped = swapBuffers.getAsBoolean();
120+
return nativeVsyncEnabled && swapped;
121+
}
122+
123+
/** Gives Swing time to process events when rendering without any limiter. */
124+
private final AwtEventQueueFairness edtFairness = new AwtEventQueueFairness();
125+
126+
/** Whether the platform successfully enabled native vertical sync. */
127+
private volatile boolean nativeVsyncEnabled;
128+
116129
/** GL versions map. */
117130
private static final Map<String, Consumer<GLData>> RENDER_CONFIGS = new HashMap<>();
118131

@@ -272,12 +285,14 @@ public GLData getGLDataEffective() {
272285
* To start drawing on the AWT surface, the AWT threads must be locked to
273286
* avoid conflicts when drawing on the canvas.
274287
*/
275-
public void lock() {
288+
public boolean lock() {
276289
synchronized (lock) {
277290
try {
278291
platformCanvas.lock();// <- MUST lock on Linux
292+
return true;
279293
} catch (AWTException e) {
280294
listener.handleError("Failed to lock Canvas", e);
295+
return false;
281296
}
282297
}
283298
}
@@ -305,8 +320,8 @@ public void doDisposeCanvas() {
305320
/**
306321
* This is where you actually draw on the canvas (framebuffer).
307322
*/
308-
public void swapBuffers() {
309-
platformCanvas.swapBuffers();
323+
public boolean swapBuffers() {
324+
return platformCanvas.swapBuffers();
310325
}
311326

312327
/**
@@ -575,18 +590,22 @@ public void run() {
575590
// with demanding scenes.
576591
runLoop();
577592

593+
boolean nativeVsyncPacedFrame = false;
594+
578595
// All this does is call swapBuffers().
579596
// If the canvas is not active, there's no need to waste time
580597
// doing that.
581598
if (renderable.get() && canvas.hasContext() && canvas.isValid()) {
582599
try {
583600
if (allowSwapBuffers && autoFlush) {
584601
// calls swap buffers | lock, etc.
585-
try {
586-
canvas.lock();
587-
canvas.swapBuffers();
588-
} finally {
589-
canvas.unlock();
602+
if (canvas.lock()) {
603+
try {
604+
nativeVsyncPacedFrame = swapBuffersAndCheckNativeVsync(
605+
nativeVsyncEnabled, canvas::swapBuffers);
606+
} finally {
607+
canvas.unlock();
608+
}
590609
}
591610

592611
// Sync the display on some systems.
@@ -596,6 +615,12 @@ public void run() {
596615
listener.handleError("Error while swapping buffers", ex);
597616
}
598617
}
618+
619+
// With neither VSync nor a software FPS limit, keep rendering
620+
// unbounded but yield briefly if Swing's EDT stops progressing.
621+
if (AwtEventQueueFairness.isRequired(nativeVsyncPacedFrame, frameRateLimit)) {
622+
edtFairness.afterFrame();
623+
}
599624
} else {
600625
// HACK: If the GL context is not rendering, the thread will
601626
// enter a waiting state, thus avoiding CPU overload.
@@ -705,6 +730,8 @@ protected void destroyContext() {
705730
*/
706731
@Override
707732
protected void createContext(AppSettings settings) {
733+
nativeVsyncEnabled = false;
734+
708735
boolean linux = Platform.get() == Platform.LINUX
709736
|| Platform.get() == Platform.FREEBSD;
710737
if (!settings.isX11PlatformPreferred() && linux && JmeSystem.isWaylandSession()) {
@@ -763,6 +790,13 @@ protected void createContext(AppSettings settings) {
763790
canvas.createContext();
764791
canvas.makeCurrent();
765792

793+
nativeVsyncEnabled = settings.isVSync()
794+
&& Integer.valueOf(1).equals(canvas.getGLDataEffective().swapInterval);
795+
if (settings.isVSync() && !nativeVsyncEnabled) {
796+
LOGGER.warning("Native VSync is unavailable for the AWT canvas; "
797+
+ "EDT fairness will be used when no FPS limit is set.");
798+
}
799+
766800
SwingUtilities.invokeLater(() -> {
767801
canvas.validate();
768802
});

jme3-lwjgl3/src/main/java/com/jme3/system/lwjglx/MacOSXGLPlatform.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,16 @@
3131
*/
3232
package com.jme3.system.lwjglx;
3333

34+
import static org.lwjgl.opengl.CGL.CGLSetParameter;
35+
import static org.lwjgl.opengl.CGL.kCGLCPSwapInterval;
36+
import static org.lwjgl.opengl.CGL.kCGLNoError;
37+
import static org.lwjgl.system.jawt.JAWTFunctions.JAWT_FreeDrawingSurface;
38+
39+
import java.awt.AWTException;
40+
import java.awt.Canvas;
41+
42+
import org.lwjgl.opengl.awt.GLData;
3443
import org.lwjgl.opengl.awt.PlatformMacOSXGLCanvas;
35-
import static org.lwjgl.system.jawt.JAWTFunctions.*;
3644

3745
/**
3846
* <code>MacOSXGLPlatform</code> class that implements the {@link com.jme3.system.lwjglx.LwjglxGLPlatform}
@@ -42,6 +50,25 @@
4250
*/
4351
final class MacOSXGLPlatform extends PlatformMacOSXGLCanvas implements LwjglxGLPlatform {
4452

53+
@Override
54+
public long create(Canvas canvas, GLData data, GLData effective) throws AWTException {
55+
effective.swapInterval = null;
56+
if (data.swapInterval != null && data.swapInterval < 0) {
57+
throw new AWTException("Negative swap intervals are not supported by CGL");
58+
}
59+
60+
long context = super.create(canvas, data, effective);
61+
if (data.swapInterval == null) {
62+
return context;
63+
}
64+
65+
int result = CGLSetParameter(context, kCGLCPSwapInterval, data.swapInterval);
66+
if (result == kCGLNoError) {
67+
effective.swapInterval = data.swapInterval;
68+
}
69+
return context;
70+
}
71+
4572
/**
4673
* (non-Javadoc)
4774
* @see com.jme3.system.lwjglx.LwjglxGLPlatform#destroy()

jme3-lwjgl3/src/main/java/com/jme3/system/lwjglx/Win32GLPlatform.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,18 @@
3131
*/
3232
package com.jme3.system.lwjglx;
3333

34-
import static org.lwjgl.system.jawt.JAWTFunctions.*;
34+
import static org.lwjgl.opengl.WGL.wglGetCurrentContext;
35+
import static org.lwjgl.opengl.WGL.wglGetCurrentDC;
36+
import static org.lwjgl.opengl.WGL.wglGetProcAddress;
37+
import static org.lwjgl.opengl.WGL.wglMakeCurrent;
38+
import static org.lwjgl.system.JNI.callI;
39+
import static org.lwjgl.system.MemoryUtil.NULL;
40+
import static org.lwjgl.system.jawt.JAWTFunctions.JAWT_FreeDrawingSurface;
41+
42+
import java.awt.AWTException;
43+
import java.awt.Canvas;
44+
45+
import org.lwjgl.opengl.awt.GLData;
3546
import org.lwjgl.opengl.awt.PlatformWin32GLCanvas;
3647

3748
/**
@@ -42,6 +53,47 @@
4253
*/
4354
final class Win32GLPlatform extends PlatformWin32GLCanvas implements LwjglxGLPlatform {
4455

56+
@Override
57+
public long create(Canvas canvas, GLData data, GLData effective) throws AWTException {
58+
effective.swapInterval = null;
59+
Integer requestedSwapInterval = data.swapInterval;
60+
long context;
61+
try {
62+
// The dependency treats an unavailable WGL swap-control extension
63+
// as a fatal context-creation error. Create first, then apply VSync
64+
// here so jME can fall back to EDT fairness instead.
65+
data.swapInterval = null;
66+
context = super.create(canvas, data, effective);
67+
} finally {
68+
data.swapInterval = requestedSwapInterval;
69+
}
70+
71+
if (requestedSwapInterval == null) {
72+
return context;
73+
}
74+
75+
long previousContext = wglGetCurrentContext(null);
76+
long previousDc = wglGetCurrentDC();
77+
try {
78+
if (!makeCurrent(context)) {
79+
return context;
80+
}
81+
82+
long setAddress = wglGetProcAddress(null, "wglSwapIntervalEXT");
83+
if (setAddress == NULL || callI(requestedSwapInterval, setAddress) == 0) {
84+
return context;
85+
}
86+
87+
long getAddress = wglGetProcAddress(null, "wglGetSwapIntervalEXT");
88+
if (getAddress != NULL && callI(getAddress) == requestedSwapInterval) {
89+
effective.swapInterval = requestedSwapInterval;
90+
}
91+
} finally {
92+
wglMakeCurrent(null, previousDc, previousContext);
93+
}
94+
return context;
95+
}
96+
4597
/* (non-Javadoc)
4698
* @see com.jme3.system.lwjglx.LwjglxGLPlatform#dispose()
4799
*/

0 commit comments

Comments
 (0)