Hi Kevin,

On 23/9/25 01:02, Kevin Rushforth wrote:
Hi Michael,

Welcome to the openjfx-dev list. We've not ever been able to reproduce this on any of our systems. It seems likely that it is specific to certain graphics drivers. We do all our testing on Ubuntu Linux and Oracle Linux (which is based on RHEL), so it's also possible we are testing with different drivers even for the same card.


Thanks, and I've subscribed to the dev list.  It just seemed to be full of patch discussion so I wasn't sure where to post.

Indeed I saw on the bug nobody could recreate it.  Weird.  I tried Oracle Linux 10 on my laptop but I couldn't get it to load the modesetting driver and boot (incl the official amd driver).  With ubuntu 25 vanilla i'm getting the same unthrottled framerate as on my other systems.  I was hoping this wasn't the case so I could see why you weren't seeing this, so  I'll try the other desktops in-case it's related to the compositor but it's going to take a while as i'm using a usb stick and ubuntu installation seems very disk heavy in ways every other linux isn't.

I've attached a standalone example - it runs about 2000fps on one system when it's down to 1 window.  It's intentionally updating each pulse ignoring the 'frac' value so the pulse timing can be visualised.

I don't know if it's useful but this is sample output from quantum.debug:

QT.postPulse#(95891966861546): DROP : TAPN
QT.endPulse: 95891966882376
QT.vsyncHint: postPulse: 95891967385262
QT.postPulse@(95891967391984): TAPN
QT.endPulse: 95891967429776
QT.vsyncHint: postPulse: 95891968031510
QT.postPulse@(95891968039475): TAPN
QT.endPulse: 95891968072438
QT.vsyncHint: postPulse: 95891968663842
QT.postPulse@(95891968671827): TAPN
QT.endPulse: 95891968696373

This last basic sequence repeats a dozen or so times times until a DROP : TAPN again

Note that each postPulse() is about 650uS apart.


If you have a reliable patch for the bug, you can submit a PR although we will need to find a way to test it. See the CONTRIBUTING [1] guidelines for what you will need to do in order to contribute.


I spent a frustrating hour or so trying to log into an oracle account I forgot I had, then creating a new one which I also can't log into ("A federated user can't perform local authentication.") so I can't sign the OCA for now (if it ever gets to that point).

I looked more at the latest code (without the vsync patch).

Commenting out makeCurrent(null) in ES2SwapChain.present() throttles the pulse but only if there is a single window (i.e. because the context never changes).  Perhaps this is a driver difference, and causes glxswapbuffers not to throttle even with double buffering on the rendering context.  makeContext is quite expensive (a few ms), so i'm surprised it is called every render pass (apart from the obvious of being easier to implement with a single render thread). Adding a glFinish() immediately after swapbuffers almost works but it halves the framerate when too many windows (>2) are active because it's performed per-window and not per-pulse.

Secondly, tracing PaintCollector.done() calls toolkit.vsyncHint() which calls postPulse() whose logic will always fire a new pulse when an animation is active (i.e. the Transition in the example). Is this really the expected behaviour here?  vsyncHint() seems to be to perform a vsync not to fire another pulse.   Just commenting out vsyncHint() fixes the poor throttling but then it runs quite jittery due to timing via gdk_thread_timeout.

I added some time measurement to ES2SWwapChain.present() with a single window and it confirms that this is where the throttling or not occurs - with makeCurrent(null) removed 2 frames are presented very quickly then swapBuffers() blocks to match the display (and also means rendering is 2 frames ahead).  With it added in it just runs asynchronously and returns very quickly.  I was hoping to confirm against a 'working' platform but alas so far.

TBH the throttling is important but I was also curious about addressing the jitteriness due to using gdk_thread_timeout, it seems vsyncHint() is meant for that but there doesn't seem to be any linkage between that and the pulse timing esp since the 'hint' isn't vsync-synced, so it just ends up firing another pulse immediately or pausing the timer if no animation is active.

I'm still thinking of fixes, perhaps a more accurate pulse timer is enough (and basically ignore vsyncHint) but it will have to be platform specific.  I will have to study the other backends a bit more first as well.

Are there plans for a vulkan backend?  That might solve/move the problem.  I've seen in mentioned in the past but I can't find anything on the web about anything concrete.

Regards,
 Michael
// java --module-path=/usr/local/javafx-sdk-z/lib --add-modules=javafx.graphics throttle.java

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static javafx.animation.Animation.INDEFINITE;
import javafx.animation.Interpolator;
import javafx.animation.Transition;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.shape.Line;
import javafx.scene.text.Font;
import javafx.scene.text.FontSmoothingType;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;

public class throttle extends Application {
	public void start(Stage stage) throws Exception {
		for (Stage st: new Stage[]{stage, new Stage(), new Stage()}) {
			//for (Stage st: new Stage[]{stage}) {
			AnchorPane root = new AnchorPane();
			Scene scene = new Scene(root, 1000, 1000, true, SceneAntialiasing.BALANCED);

			scene.addEventHandler(KeyEvent.KEY_PRESSED, ev -> {
					if (ev.getCode() == KeyCode.ESCAPE || ev.getCode() == KeyCode.Q)
						st.close();
				});

			st.setScene(scene);
			st.show();

			Group g = new Group(new Line(-1000, 0, 1000, 0), new Line(0, -1000, 0, 1000));
			g.setTranslateX(100);
			g.setTranslateY(100);
			root.getChildren().add(g);
			Transition anim = new Transition() {
					double arg = 0;
					int f = 0;
					long s = System.nanoTime();
						{
							setCycleCount(INDEFINITE);
							setCycleDuration(Duration.seconds(5));
							setInterpolator(Interpolator.LINEAR);
						}

					@Override
					protected void interpolate(double frac) {
						long n = System.nanoTime();
						if (n - s >= 1_000_000_000) {
							System.out.printf("fps: %d\n", f);
							s = n;
							f = 0;
						} else {
							f += 1;
						}
						g.setRotate(arg);
						arg += 360 / 5.0 / 60.0;
					}
				};
			anim.play();
		}
	}
	public static void main(String[] args) {
		launch(args);
	}
}

Reply via email to