On 4/10/25 01:20, Thiago Milczarek Sayão wrote:
Just out of curiosity, I compiled the EGL version. Here:
https://github.com/tsayao/jfx/releases/tag/test-egl

It does seem to have a difference in frame rate.


That's a lot better, it's basically the same as if I remove the makeCurrent() from ES2Pipeline.present and I presume how it behaves on other systems.  I still think it's throttling in the wrong place and the GlassTimer is terrible,  but at least it's throttling.  I'll probably try 25 with the latest mesa and if i still see it file a bug with them.

Over the last few days I did a lot of analysis with a more complex application - one that calculates an image sequence via OpenCL and displays it in JavaFX.  The OpenCL takes from about 3 to 15ms to complete depending on the scene.  I've attached some inter-pulse timing plots which are a bit more interesting than the previous ones.

There's JavaFX 25, my patches (Z), and the with your EGL build (I also tested with the makeCurrent() change since it's a single window, and it matches the EGL one).

From left to right, top to bottom:

1. Transition with no frame-rate, asynchronous rendering that drops
   work if a new request comes in while it's busy.
2. Transition with 60f/s desired frame rate, asynchronous rendering as 1.
3. Transition with no frame-rate, synchronous rendering on the
   animation thread.
4. Transition with 60/s desired frame rate, synchronous rendering as 3.


Setting a desired frame rate on the Transition results in considerable microstutter even with the egl version.  Not setting a desired frame rate - visually it's ok but there is still more frame pacing variation than calling glFinish() after all scenes have been drawn and with a more accurate timer.

I want to try to do some latency testing if I can work out a reasonable way to do it.

For what it's worth i've also attached current patch-in-progress that includes a more accurate if rather simple timer.

Regards,
 !Z


diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java
index f2a33641fd..b7bebbaa98 100644
--- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java
+++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java
@@ -397,11 +397,11 @@ final class GtkApplication extends Application implements
     @Override
     protected native int staticTimer_getMaxPeriod();
 
-    @Override protected double staticScreen_getVideoRefreshPeriod() {
-        return 0.0;     // indicate millisecond resolution
-    }
+    @Override
+    protected native double staticScreen_getVideoRefreshPeriod();
 
-    @Override native protected Screen[] staticScreen_getScreens();
+    @Override
+    native protected Screen[] staticScreen_getScreens();
 
     @Override
     protected FileChooserResult staticCommonDialogs_showFileChooser(
diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkTimer.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkTimer.java
index be63a67333..74103b6a4f 100644
--- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkTimer.java
+++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkTimer.java
@@ -32,9 +32,8 @@ final class GtkTimer extends Timer{
         super(runnable);
     }
 
-    @Override protected long _start(Runnable runnable) {
-        throw new RuntimeException("vsync timer not supported");
-    }
+    @Override
+    protected native long _start(Runnable runnable);
 
     @Override
     protected native long _start(Runnable runnable, int period);
diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassScene.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassScene.java
index e623060b30..021535334d 100644
--- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassScene.java
+++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassScene.java
@@ -65,6 +65,7 @@ abstract class GlassScene implements TKScene {
     private volatile boolean entireSceneDirty = true;
 
     private boolean doPresent = true;
+    private boolean doVSync = false;
     private final AtomicBoolean painting = new AtomicBoolean(false);
 
     private final boolean depthBuffer;
@@ -305,6 +306,14 @@ abstract class GlassScene implements TKScene {
         return doPresent;
     }
 
+    public final synchronized void setDoVSync(boolean value) {
+        doVSync = value;
+    }
+
+    public final synchronized boolean getDoVsync() {
+        return doVSync;
+    }
+
     protected Color getClearColor() {
         WindowStage windowStage = stage instanceof WindowStage ? (WindowStage)stage : null;
         if (windowStage != null && windowStage.getPlatformWindow() != null &&
diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/PaintCollector.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/PaintCollector.java
index e6c538bc5d..991e5e828e 100644
--- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/PaintCollector.java
+++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/PaintCollector.java
@@ -434,10 +434,14 @@ final class PaintCollector implements CompletionListener {
                 if (!needsHint) {
                     needsHint = gs.isSynchronous();
                 }
+            }
+
+            for (final GlassScene gs : dirtyScenes) {
                 // On platforms with a window manager, we always set doPresent = true, because
                 // we always need to rerender the scene  if it's in the dirty list and we do a
                 // swap on a per-window basis
                 gs.setDoPresent(true);
+                gs.setDoVSync(needsHint && dirtyScenes.getLast() == gs);
                 try {
                     gs.repaint();
                 } catch (Throwable t) {
diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/PresentingPainter.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/PresentingPainter.java
index c2895de7bc..94e1618343 100644
--- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/PresentingPainter.java
+++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/PresentingPainter.java
@@ -104,7 +104,7 @@ final class PresentingPainter extends ViewPainter {
 
                 /* present for vsync buffer swap */
                 if (vs.getDoPresent()) {
-                    if (!presentable.present()) {
+                    if (!presentable.present(vs.getDoVsync())) {
                         disposePresentable();
                         sceneState.getScene().entireSceneNeedsRepaint();
                     }
diff --git a/modules/javafx.graphics/src/main/java/com/sun/prism/Presentable.java b/modules/javafx.graphics/src/main/java/com/sun/prism/Presentable.java
index 8fc4c29016..75367a4d74 100644
--- a/modules/javafx.graphics/src/main/java/com/sun/prism/Presentable.java
+++ b/modules/javafx.graphics/src/main/java/com/sun/prism/Presentable.java
@@ -57,6 +57,10 @@ public interface Presentable extends RenderTarget {
      */
     public boolean present();
 
+    public default boolean present(boolean vsync) {
+        return present();
+    }
+
     public float getPixelScaleFactorX();
     public float getPixelScaleFactorY();
 }
diff --git a/modules/javafx.graphics/src/main/java/com/sun/prism/es2/ES2SwapChain.java b/modules/javafx.graphics/src/main/java/com/sun/prism/es2/ES2SwapChain.java
index b7b074d72b..886d90e0ce 100644
--- a/modules/javafx.graphics/src/main/java/com/sun/prism/es2/ES2SwapChain.java
+++ b/modules/javafx.graphics/src/main/java/com/sun/prism/es2/ES2SwapChain.java
@@ -184,7 +184,17 @@ class ES2SwapChain implements ES2RenderTarget, Presentable, GraphicsResource {
     @Override
     public boolean present() {
         boolean presented = drawable.swapBuffers(context.getGLContext());
-        context.makeCurrent(null);
+        //context.makeCurrent(null); no need to null this here
+        return presented;
+    }
+
+    @Override
+    public boolean present(boolean vsync) {
+        boolean presented = present();
+
+        if (vsync)
+            context.getGLContext().finish();
+
         return presented;
     }
 
@@ -308,6 +318,7 @@ class ES2SwapChain implements ES2RenderTarget, Presentable, GraphicsResource {
         }
 
         if (drawable != null) {
+            context.makeCurrent(null);
             drawable.dispose();
             drawable = null;
         }
diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp
index a9e84cd155..4486adc024 100644
--- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp
+++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp
@@ -362,6 +362,24 @@ JNIEXPORT jint JNICALL Java_com_sun_glass_ui_gtk_GtkApplication_staticTimer_1get
     return 10000; // There are no restrictions on period in g_threads
 }
 
+/*
+ * Class:     com_sun_glass_ui_gtk_GtkApplication
+ * Method:    staticScreen_getVideoRefreshPeriod
+ * Signature: ()D
+ */
+JNIEXPORT jdouble JNICALL Java_com_sun_glass_ui_gtk_GtkApplication_staticScreen_1getVideoRefreshPeriod
+  (JNIEnv * env, jobject obj)
+{
+    (void)env;
+    (void)obj;
+
+    GdkMonitor *m = gdk_display_get_primary_monitor(gdk_display_get_default());
+
+    int rr = gdk_monitor_get_refresh_rate(m);
+
+    return 1.0E6 / rr;
+}
+
 /*
  * Class:     com_sun_glass_ui_gtk_GtkApplication
  * Method:    staticView_getMultiClickTime
diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/GlassTimer.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/GlassTimer.cpp
index ff31b55b0d..1f5d219bb5 100644
--- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassTimer.cpp
+++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassTimer.cpp
@@ -28,27 +28,112 @@
 #include <glib.h>
 #include <gdk/gdk.h>
 #include <stdlib.h>
-
-static gboolean call_runnable_in_timer
-  (gpointer);
+#include <time.h>
 
 extern "C" {
 
-/*
- * Class:     com_sun_glass_ui_gtk_GtkTimer
- * Method:    _start
- * Signature: (Ljava/lang/Runnable;I)J
- */
-JNIEXPORT jlong JNICALL Java_com_sun_glass_ui_gtk_GtkTimer__1start
-  (JNIEnv * env, jobject obj, jobject runnable, jint period)
+typedef struct TimerContext {
+    jobject runnable;
+    struct timespec start;
+    uint64_t frame;
+    int refresh_rate;
+    // 0 = gdk_thread_timeout
+    // 1 = gdk_thread_timeout stop
+    // 2 = vsync thread
+    // 3 = vsync thread stop
+    // 4 = vsync thread failed
+    volatile int flag;
+} TimerContext;
+
+static void *_GlassTimerTask(void *data)
 {
-    (void)obj;
+    TimerContext* context = (TimerContext*) data;
+
+    JNIEnv *env;
+    jint err = javaVM->AttachCurrentThreadAsDaemon((void **)&env, NULL);
+
+    printf("Start glass timer thread: rate=%6.3f\n", context->refresh_rate * 1.0E-3); fflush(stdout);
+
+    if (err == 0)
+    {
+	int64_t frame = 1;
+
+        while (context->flag == 2) {
+            struct timespec time;
+
+            clock_gettime(CLOCK_REALTIME, &time);
+
+            int64_t current = ((time.tv_sec - context->start.tv_sec) * 1000000 + time.tv_nsec / 1000) - context->start.tv_nsec / 1000;
+            int64_t target = ((frame * 1000000000 / context->refresh_rate));
+            int delay;
+
+            if (target < current) {
+                frame += (current - target) * context->refresh_rate / 1000000000 + 1;
+                delay = 0;
+            } else {
+                delay = target - current;
+                frame++;
+            }
+
+            usleep(delay);
+
+            if (context->flag == 2)
+            {
+                env->CallVoidMethod(context->runnable, jRunnableRun, NULL);
+                LOG_EXCEPTION(env);
+            }
+
+            //printf(" %8ld: %ld  %ld  %9.6f\n", frame, current, target, (target-current) * 1.0E-6);
+        }
+
+        env->DeleteGlobalRef(context->runnable);
+        context->runnable = NULL;
+        free(context);
+    } else {
+        ERROR0("GlassTimer: Unable to attach JNIEnv\n");
+
+        // FIXME: races
+        context->flag = 4;
+        env->DeleteGlobalRef(context->runnable);
+        context->runnable = NULL;
+    }
+
+    printf("Exit glass timer thread\n");
+
+    return NULL;
+}
+
+static jlong start_timer_task(JNIEnv *env, jobject runnable, int rate) {
+    TimerContext* context = (TimerContext*) malloc(sizeof(TimerContext));
 
-    RunnableContext* context = (RunnableContext*) malloc(sizeof(RunnableContext));
     if (context != NULL) {
-        context->runnable = env->NewGlobalRef(runnable);
-        context->flag = 0;
-        gdk_threads_add_timeout_full(G_PRIORITY_HIGH_IDLE, period, call_runnable_in_timer, context, NULL);
+        pthread_t thread;
+        pthread_attr_t attr;
+
+        clock_gettime(CLOCK_REALTIME, &context->start);
+
+        // detach thread, so its resources can be reclaimed as soon as it's finished
+        pthread_attr_init(&attr);
+        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+        pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
+
+        int err = pthread_create(&thread, &attr, _GlassTimerTask, context);
+        if (err == 0)
+        {
+            pthread_setname_np(thread, "Glass Timer Thread");
+
+            context->refresh_rate = rate;
+            context->runnable = env->NewGlobalRef(runnable);
+            context->flag = 2;
+        }
+        else
+        {
+            free(context);
+            context = NULL;
+        }
+
+        pthread_attr_destroy(&attr);
+
         return PTR_TO_JLONG(context);
     } else {
         // we throw RuntimeException on Java side when we can't
@@ -59,45 +144,57 @@ JNIEXPORT jlong JNICALL Java_com_sun_glass_ui_gtk_GtkTimer__1start
 
 /*
  * Class:     com_sun_glass_ui_gtk_GtkTimer
- * Method:    _stop
- * Signature: (J)V
+ * Method:    _start
+ * Signature: (Ljava/lang/Runnable;)J
  */
-JNIEXPORT void JNICALL Java_com_sun_glass_ui_gtk_GtkTimer__1stop
-  (JNIEnv * env, jobject obj, jlong ptr)
+JNIEXPORT jlong JNICALL Java_com_sun_glass_ui_gtk_GtkTimer__1start__Ljava_lang_Runnable_2
+  (JNIEnv *env, jobject obj, jobject runnable)
 {
     (void)obj;
 
-    RunnableContext* context = (RunnableContext*) JLONG_TO_PTR(ptr);
-    context->flag = 1;
-    env->DeleteGlobalRef(context->runnable);
-    context->runnable = NULL;
-}
+    GdkMonitor *m = gdk_display_get_primary_monitor(gdk_display_get_default());
+    int rr = gdk_monitor_get_refresh_rate(m);
 
-} // extern "C"
+    printf("start 'vsync' timer: %d\n", rr); fflush(stdout);
 
+    return start_timer_task(env, runnable, rr);
+}
 
-static gboolean call_runnable_in_timer
-  (gpointer data)
+/*
+ * Class:     com_sun_glass_ui_gtk_GtkTimer
+ * Method:    _start
+ * Signature: (Ljava/lang/Runnable;I)J
+ */
+JNIEXPORT jlong JNICALL Java_com_sun_glass_ui_gtk_GtkTimer__1start__Ljava_lang_Runnable_2I
+  (JNIEnv * env, jobject obj, jobject runnable, jint period)
 {
-    RunnableContext* context = (RunnableContext*) data;
-    if (context->flag) {
-        free(context);
-        return FALSE;
-    }
-    else if (context->runnable) {
-        JNIEnv *env;
-        int envStatus = javaVM->GetEnv((void **)&env, JNI_VERSION_1_6);
-        if (envStatus == JNI_EDETACHED) {
-            javaVM->AttachCurrentThread((void **)&env, NULL);
-        }
+    (void)obj;
 
-        env->CallVoidMethod(context->runnable, jRunnableRun, NULL);
-        LOG_EXCEPTION(env);
+    printf("start gdk timer: %d\n", period); fflush(stdout);
 
-        if (envStatus == JNI_EDETACHED) {
-            javaVM->DetachCurrentThread();
-        }
+    return start_timer_task(env, runnable, 1000000 / period);
+}
+
+/*
+ * Class:     com_sun_glass_ui_gtk_GtkTimer
+ * Method:    _stop
+ * Signature: (J)V
+ */
+JNIEXPORT void JNICALL Java_com_sun_glass_ui_gtk_GtkTimer__1stop
+  (JNIEnv * env, jobject obj, jlong ptr)
+{
+    (void)obj;
+
+    TimerContext* context = (TimerContext*) JLONG_TO_PTR(ptr);
+    if (context->flag == 0) {
+        context->flag = 1;
+        env->DeleteGlobalRef(context->runnable);
+        context->runnable = NULL;
+    } else if (context->flag == 2) {
+        context->flag = 3;
+    } else if (context->flag == 4) {
+        free(context);
     }
-    return TRUE;
 }
 
+} // extern "C"

Reply via email to