Browse Source

Merge branch 'jMonkeyEngine:master' into master

joliver82 3 months ago
parent
commit
5c04ca753d
34 changed files with 1114 additions and 527 deletions
  1. 1 0
      .github/.well-known/funding-manifest-urls
  2. 32 37
      .github/workflows/main.yml
  3. 1 1
      .github/workflows/screenshot-test-comment.yml
  4. 148 83
      jme3-awt-dialogs/src/main/java/com/jme3/awt/AWTSettingsDialog.java
  5. 4 3
      jme3-core/src/main/java/com/jme3/effect/influencers/NewtonianParticleInfluencer.java
  6. 92 39
      jme3-core/src/main/java/com/jme3/effect/shapes/EmitterMeshFaceShape.java
  7. 140 57
      jme3-core/src/main/java/com/jme3/font/BitmapFont.java
  8. 179 98
      jme3-core/src/main/java/com/jme3/font/BitmapText.java
  9. 4 4
      jme3-core/src/main/java/com/jme3/material/RenderState.java
  10. 6 6
      jme3-core/src/main/java/com/jme3/renderer/RenderManager.java
  11. 22 16
      jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java
  12. 52 6
      jme3-core/src/main/java/com/jme3/shader/VarType.java
  13. 18 30
      jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java
  14. 41 44
      jme3-core/src/main/java/com/jme3/shadow/PointLightShadowRenderer.java
  15. 144 39
      jme3-core/src/main/java/com/jme3/system/AppSettings.java
  16. 46 45
      jme3-core/src/plugins/java/com/jme3/export/binary/BinaryExporter.java
  17. 18 2
      jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java
  18. 1 0
      jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java
  19. 9 0
      jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/App.java
  20. 10 4
      jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ExtentReportExtension.java
  21. 117 0
      jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ExtentReportLogCapture.java
  22. 29 13
      jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/TestDriver.java
  23. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.effects.TestIssue1773.testIssue1773_localSpace_f45.png
  24. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.effects.TestIssue1773.testIssue1773_worldSpace_f45.png
  25. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_DefaultDirectionalLight_f12.png
  26. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_HighRoughness_f12.png
  27. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_LowRoughness_f12.png
  28. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_UpdatedDirectionalLight_f12.png
  29. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithRealtimeBaking_f10.png
  30. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithoutRealtimeBaking_f10.png
  31. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromAbove_f1.png
  32. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromFront_f1.png
  33. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromRight_f1.png
  34. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.post.TestFog.testFog_f1.png

+ 1 - 0
.github/.well-known/funding-manifest-urls

@@ -0,0 +1 @@
+https://jmonkeyengine.org/funding.json

+ 32 - 37
.github/workflows/main.yml

@@ -59,46 +59,41 @@ jobs:
   ScreenshotTests:
   ScreenshotTests:
     name: Run Screenshot Tests
     name: Run Screenshot Tests
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
+    container:
+      image: ghcr.io/onemillionworlds/opengl-docker-image:v1
     permissions:
     permissions:
       contents: read
       contents: read
     steps:
     steps:
-    - uses: actions/checkout@v4
-    - name: Set up JDK 17
-      uses: actions/setup-java@v4
-      with:
-        java-version: '17'
-        distribution: 'temurin'
-    - name: Install Mesa3D
-      run: |
-        sudo apt-get update
-        sudo apt-get install -y mesa-utils libgl1-mesa-dri libgl1 libglx-mesa0 xvfb
-    - name: Set environment variables for Mesa3D
-      run: |
-        echo "LIBGL_ALWAYS_SOFTWARE=1" >> $GITHUB_ENV
-        echo "MESA_LOADER_DRIVER_OVERRIDE=llvmpipe" >> $GITHUB_ENV
-    - name: Start xvfb
-      run: |
-        sudo Xvfb :99 -ac -screen 0 1024x768x16 &
-        export DISPLAY=:99
-        echo "DISPLAY=:99" >> $GITHUB_ENV
-    - name: Verify Mesa3D Installation
-      run: |
-        glxinfo | grep "OpenGL"
-    - name: Validate the Gradle wrapper
-      uses: gradle/actions/wrapper-validation@v3
-    - name: Test with Gradle Wrapper
-      run: |
-        ./gradlew :jme3-screenshot-test:screenshotTest
-    - name: Upload Test Reports
-      uses: actions/upload-artifact@master
-      if: always()
-      with:
-        name: screenshot-test-report
-        retention-days: 30
-        path: |
-          **/build/reports/**
-          **/build/changed-images/**
-          **/build/test-results/**
+      - uses: actions/checkout@v4
+      - name: Start xvfb
+        run: |
+          Xvfb :99 -ac -screen 0 1024x768x16 &
+          export DISPLAY=:99
+          echo "DISPLAY=:99" >> $GITHUB_ENV
+      - name: Report GL/Vulkan
+        run: |
+          set -x
+          echo "DISPLAY=$DISPLAY"
+          glxinfo | grep -E "OpenGL version|OpenGL renderer|OpenGL vendor" || true
+          vulkaninfo --summary || true
+          echo "VK_ICD_FILENAMES=$VK_ICD_FILENAMES"
+          echo "MESA_LOADER_DRIVER_OVERRIDE=$MESA_LOADER_DRIVER_OVERRIDE"
+          echo "GALLIUM_DRIVER=$GALLIUM_DRIVER"
+      - name: Validate the Gradle wrapper
+        uses: gradle/actions/wrapper-validation@v3
+      - name: Test with Gradle Wrapper
+        run: |
+          ./gradlew :jme3-screenshot-test:screenshotTest
+      - name: Upload Test Reports
+        uses: actions/upload-artifact@master
+        if: always()
+        with:
+          name: screenshot-test-report
+          retention-days: 30
+          path: |
+            **/build/reports/**
+            **/build/changed-images/**
+            **/build/test-results/**
   # Build the natives on android
   # Build the natives on android
   BuildAndroidNatives:
   BuildAndroidNatives:
     name: Build natives for android
     name: Build natives for android

+ 1 - 1
.github/workflows/screenshot-test-comment.yml

@@ -21,7 +21,7 @@ jobs:
       contents: read
       contents: read
     steps:
     steps:
       - name: Wait for GitHub to register the workflow run
       - name: Wait for GitHub to register the workflow run
-        run: sleep 15
+        run: sleep 120
 
 
       - name: Wait for Screenshot Tests to complete
       - name: Wait for Screenshot Tests to complete
         uses: lewagon/[email protected]
         uses: lewagon/[email protected]

+ 148 - 83
jme3-awt-dialogs/src/main/java/com/jme3/awt/AWTSettingsDialog.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -57,12 +57,11 @@ import java.util.prefs.BackingStoreException;
 import javax.swing.*;
 import javax.swing.*;
 
 
 /**
 /**
- * <code>SettingsDialog</code> displays a Swing dialog box to interactively
- * configure the <code>AppSettings</code> of a desktop application before
- * <code>start()</code> is invoked.
- *
- * The <code>AppSettings</code> instance to be configured is passed to the
- * constructor.
+ * `AWTSettingsDialog` displays a Swing dialog box to interactively
+ * configure the `AppSettings` of a desktop application before
+ * `start()` is invoked.
+ * <p>
+ * The `AppSettings` instance to be configured is passed to the constructor.
  *
  *
  * @see AppSettings
  * @see AppSettings
  * @author Mark Powell
  * @author Mark Powell
@@ -71,14 +70,33 @@ import javax.swing.*;
  */
  */
 public final class AWTSettingsDialog extends JFrame {
 public final class AWTSettingsDialog extends JFrame {
 
 
-    public static interface SelectionListener {
-
-        public void onSelection(int selection);
+    /**
+     * Listener interface for handling selection events from the settings dialog.
+     */
+    public interface SelectionListener {
+        /**
+         * Called when a selection is made in the settings dialog (OK or Cancel).
+         *
+         * @param selection The type of selection made: `NO_SELECTION`, `APPROVE_SELECTION`, or `CANCEL_SELECTION`.
+         */
+        void onSelection(int selection);
     }
     }
 
 
     private static final Logger logger = Logger.getLogger(AWTSettingsDialog.class.getName());
     private static final Logger logger = Logger.getLogger(AWTSettingsDialog.class.getName());
     private static final long serialVersionUID = 1L;
     private static final long serialVersionUID = 1L;
-    public static final int NO_SELECTION = 0, APPROVE_SELECTION = 1, CANCEL_SELECTION = 2;
+
+    /**
+     * Indicates that no selection has been made yet.
+     */
+    public static final int NO_SELECTION = 0;
+    /**
+     * Indicates that the user approved the settings.
+     */
+    public static final int APPROVE_SELECTION = 1;
+    /**
+     * Indicates that the user canceled the settings dialog.
+     */
+    public static final int CANCEL_SELECTION = 2;
 
 
     // Resource bundle for i18n.
     // Resource bundle for i18n.
     ResourceBundle resourceBundle = ResourceBundle.getBundle("com.jme3.app/SettingsDialog");
     ResourceBundle resourceBundle = ResourceBundle.getBundle("com.jme3.app/SettingsDialog");
@@ -86,8 +104,12 @@ public final class AWTSettingsDialog extends JFrame {
     // the instance being configured
     // the instance being configured
     private final AppSettings source;
     private final AppSettings source;
 
 
-    // Title Image
+    /**
+     * The URL of the image file to be displayed as a title icon in the dialog.
+     * Can be `null` if no image is desired.
+     */
     private URL imageFile = null;
     private URL imageFile = null;
+
     // Array of supported display modes
     // Array of supported display modes
     private DisplayMode[] modes = null;
     private DisplayMode[] modes = null;
     private static final DisplayMode[] windowDefaults = new DisplayMode[] {
     private static final DisplayMode[] windowDefaults = new DisplayMode[] {
@@ -114,10 +136,24 @@ public final class AWTSettingsDialog extends JFrame {
     private int minWidth = 0;
     private int minWidth = 0;
     private int minHeight = 0;
     private int minHeight = 0;
 
 
+    /**
+     * Displays a settings dialog using the provided `AppSettings` source.
+     * Settings will be loaded from preferences.
+     *
+     * @param sourceSettings The `AppSettings` instance to configure.
+     * @return `true` if the user approved the settings, `false` otherwise.
+     */
     public static boolean showDialog(AppSettings sourceSettings) {
     public static boolean showDialog(AppSettings sourceSettings) {
         return showDialog(sourceSettings, true);
         return showDialog(sourceSettings, true);
     }
     }
 
 
+    /**
+     * Displays a settings dialog using the provided `AppSettings` source.
+     *
+     * @param sourceSettings The `AppSettings` instance to configure.
+     * @param loadSettings   If `true`, settings will be loaded from preferences; otherwise, they will be merged.
+     * @return `true` if the user approved the settings, `false` otherwise.
+     */
     public static boolean showDialog(AppSettings sourceSettings, boolean loadSettings) {
     public static boolean showDialog(AppSettings sourceSettings, boolean loadSettings) {
         String iconPath = sourceSettings.getSettingsDialogImage();
         String iconPath = sourceSettings.getSettingsDialogImage();
         final URL iconUrl = JmeSystem.class.getResource(iconPath.startsWith("/") ? iconPath : "/" + iconPath);
         final URL iconUrl = JmeSystem.class.getResource(iconPath.startsWith("/") ? iconPath : "/" + iconPath);
@@ -127,10 +163,30 @@ public final class AWTSettingsDialog extends JFrame {
         return showDialog(sourceSettings, iconUrl, loadSettings);
         return showDialog(sourceSettings, iconUrl, loadSettings);
     }
     }
 
 
+    /**
+     * Displays a settings dialog using the provided `AppSettings` source and an image file path.
+     *
+     * @param sourceSettings The `AppSettings` instance to configure.
+     * @param imageFile      The path to the image file to use as the title of the dialog;
+     *                       `null` will result in no image being displayed.
+     * @param loadSettings   If `true`, settings will be loaded from preferences; otherwise, they will be merged.
+     * @return `true` if the user approved the settings, `false` otherwise.
+     */
     public static boolean showDialog(AppSettings sourceSettings, String imageFile, boolean loadSettings) {
     public static boolean showDialog(AppSettings sourceSettings, String imageFile, boolean loadSettings) {
         return showDialog(sourceSettings, getURL(imageFile), loadSettings);
         return showDialog(sourceSettings, getURL(imageFile), loadSettings);
     }
     }
 
 
+    /**
+     * Displays a settings dialog using the provided `AppSettings` source and an image URL.
+     * This method blocks until the dialog is closed.
+     *
+     * @param sourceSettings The `AppSettings` instance to configure (not null).
+     * @param imageFile      The `URL` pointing to the image file to use as the title of the dialog;
+     *                       `null` will result in no image being displayed.
+     * @param loadSettings   If `true`, the dialog will copy settings from preferences. If `false`
+     *                       and preferences exist, they will be merged with the current settings.
+     * @return `true` if the user approved the settings, `false` otherwise (`CANCEL_SELECTION` or dialog close).
+     */
     public static boolean showDialog(AppSettings sourceSettings, URL imageFile, boolean loadSettings) {
     public static boolean showDialog(AppSettings sourceSettings, URL imageFile, boolean loadSettings) {
         if (SwingUtilities.isEventDispatchThread()) {
         if (SwingUtilities.isEventDispatchThread()) {
             throw new IllegalStateException("Cannot run from EDT");
             throw new IllegalStateException("Cannot run from EDT");
@@ -166,46 +222,47 @@ public final class AWTSettingsDialog extends JFrame {
         synchronized (lock) {
         synchronized (lock) {
             while (!done.get()) {
             while (!done.get()) {
                 try {
                 try {
+                    // Wait until notified by the selection listener
                     lock.wait();
                     lock.wait();
                 } catch (InterruptedException ex) {
                 } catch (InterruptedException ex) {
+                    Thread.currentThread().interrupt();
+                    logger.log(Level.WARNING, "Settings dialog thread interrupted while waiting.", ex);
+                    return false; // Treat as cancel if interrupted
                 }
                 }
             }
             }
         }
         }
 
 
-        sourceSettings.copyFrom(settings);
+        // If approved, copy the modified settings back to the original source
+        if (result.get() == APPROVE_SELECTION) {
+            sourceSettings.copyFrom(settings);
+        }
 
 
-        return result.get() == AWTSettingsDialog.APPROVE_SELECTION;
+        return result.get() == APPROVE_SELECTION;
     }
     }
 
 
     /**
     /**
-     * Instantiate a <code>SettingsDialog</code> for the primary display.
+     * Constructs a `SettingsDialog` for the primary display.
      *
      *
-     * @param source
-     *            the <code>AppSettings</code> (not null)
-     * @param imageFile
-     *            the image file to use as the title of the dialog;
-     *            <code>null</code> will result in to image being displayed
-     * @param loadSettings
-     *            if true, copy the settings, otherwise merge them
-     * @throws IllegalArgumentException
-     *             if the source is <code>null</code>
+     * @param source       The `AppSettings` instance to configure (not null).
+     * @param imageFile    The path to the image file to use as the title of the dialog;
+     *                     `null` will result in no image being displayed.
+     * @param loadSettings If `true`, the dialog will copy settings from preferences. If `false`
+     *                     and preferences exist, they will be merged with the current settings.
+     * @throws IllegalArgumentException if `source` is `null`.
      */
      */
     protected AWTSettingsDialog(AppSettings source, String imageFile, boolean loadSettings) {
     protected AWTSettingsDialog(AppSettings source, String imageFile, boolean loadSettings) {
         this(source, getURL(imageFile), loadSettings);
         this(source, getURL(imageFile), loadSettings);
     }
     }
 
 
     /**
     /**
-     * /** Instantiate a <code>SettingsDialog</code> for the primary display.
+     * Constructs a `SettingsDialog` for the primary display.
      *
      *
-     * @param source
-     *            the <code>AppSettings</code> object (not null)
-     * @param imageFile
-     *            the image file to use as the title of the dialog;
-     *            <code>null</code> will result in to image being displayed
-     * @param loadSettings
-     *            if true, copy the settings, otherwise merge them
-     * @throws IllegalArgumentException
-     *             if the source is <code>null</code>
+     * @param source       The `AppSettings` instance to configure (not null).
+     * @param imageFile    The `URL` pointing to the image file to use as the title of the dialog;
+     *                     `null` will result in no image being displayed.
+     * @param loadSettings If `true`, the dialog will copy settings from preferences. If `false`
+     *                     and preferences exist, they will be merged with the current settings.
+     * @throws IllegalArgumentException if `source` is `null`.
      */
      */
     protected AWTSettingsDialog(AppSettings source, URL imageFile, boolean loadSettings) {
     protected AWTSettingsDialog(AppSettings source, URL imageFile, boolean loadSettings) {
         if (source == null) {
         if (source == null) {
@@ -232,7 +289,10 @@ public final class AWTSettingsDialog extends JFrame {
         minHeight = source.getMinHeight();
         minHeight = source.getMinHeight();
 
 
         try {
         try {
+            logger.log(Level.INFO, "Loading AppSettings from PreferenceKey: {0}", appTitle);
             registrySettings.load(appTitle);
             registrySettings.load(appTitle);
+            AppSettings.printPreferences(appTitle);
+
         } catch (BackingStoreException ex) {
         } catch (BackingStoreException ex) {
             logger.log(Level.WARNING, "Failed to load settings", ex);
             logger.log(Level.WARNING, "Failed to load settings", ex);
         }
         }
@@ -355,8 +415,6 @@ public final class AWTSettingsDialog extends JFrame {
      * <code>init</code> creates the components to use the dialog.
      * <code>init</code> creates the components to use the dialog.
      */
      */
     private void createUI() {
     private void createUI() {
-        GridBagConstraints gbc;
-
         JPanel mainPanel = new JPanel(new GridBagLayout());
         JPanel mainPanel = new JPanel(new GridBagLayout());
 
 
         addWindowListener(new WindowAdapter() {
         addWindowListener(new WindowAdapter() {
@@ -368,8 +426,9 @@ public final class AWTSettingsDialog extends JFrame {
             }
             }
         });
         });
 
 
-        if (source.getIcons() != null) {
-            safeSetIconImages(Arrays.asList((BufferedImage[]) source.getIcons()));
+        Object[] sourceIcons = source.getIcons();
+        if (sourceIcons != null && sourceIcons.length > 0) {
+            safeSetIconImages(Arrays.asList((BufferedImage[]) sourceIcons));
         }
         }
 
 
         setTitle(MessageFormat.format(resourceBundle.getString("frame.title"), source.getTitle()));
         setTitle(MessageFormat.format(resourceBundle.getString("frame.title"), source.getTitle()));
@@ -419,7 +478,7 @@ public final class AWTSettingsDialog extends JFrame {
         gammaBox = new JCheckBox(resourceBundle.getString("checkbox.gamma"));
         gammaBox = new JCheckBox(resourceBundle.getString("checkbox.gamma"));
         gammaBox.setSelected(source.isGammaCorrection());
         gammaBox.setSelected(source.isGammaCorrection());
 
 
-        gbc = new GridBagConstraints();
+        GridBagConstraints gbc = new GridBagConstraints();
         gbc.weightx = 0.5;
         gbc.weightx = 0.5;
         gbc.gridx = 0;
         gbc.gridx = 0;
         gbc.gridwidth = 2;
         gbc.gridwidth = 2;
@@ -493,7 +552,6 @@ public final class AWTSettingsDialog extends JFrame {
         // Set the button action listeners. Cancel disposes without saving, OK
         // Set the button action listeners. Cancel disposes without saving, OK
         // saves.
         // saves.
         ok.addActionListener(new ActionListener() {
         ok.addActionListener(new ActionListener() {
-
             @Override
             @Override
             public void actionPerformed(ActionEvent e) {
             public void actionPerformed(ActionEvent e) {
                 if (verifyAndSaveCurrentSelection()) {
                 if (verifyAndSaveCurrentSelection()) {
@@ -501,12 +559,13 @@ public final class AWTSettingsDialog extends JFrame {
                     dispose();
                     dispose();
 
 
                     // System.gc() should be called to prevent "X Error of
                     // System.gc() should be called to prevent "X Error of
-                    // failed request: RenderBadPicture (invalid Picture
-                    // parameter)"
+                    // failed request: RenderBadPicture (invalid Picture parameter)"
                     // on Linux when using AWT/Swing + GLFW.
                     // on Linux when using AWT/Swing + GLFW.
                     // For more info see:
                     // For more info see:
                     // https://github.com/LWJGL/lwjgl3/issues/149,
                     // https://github.com/LWJGL/lwjgl3/issues/149,
-                    // https://hub.jmonkeyengine.org/t/experimenting-lwjgl3/37275
+
+                    //  intentional double call. see this discussion:
+                    //  https://hub.jmonkeyengine.org/t/experimenting-lwjgl3/37275/12
                     System.gc();
                     System.gc();
                     System.gc();
                     System.gc();
                 }
                 }
@@ -514,7 +573,6 @@ public final class AWTSettingsDialog extends JFrame {
         });
         });
 
 
         cancel.addActionListener(new ActionListener() {
         cancel.addActionListener(new ActionListener() {
-
             @Override
             @Override
             public void actionPerformed(ActionEvent e) {
             public void actionPerformed(ActionEvent e) {
                 setUserSelection(CANCEL_SELECTION);
                 setUserSelection(CANCEL_SELECTION);
@@ -568,7 +626,6 @@ public final class AWTSettingsDialog extends JFrame {
                 colorDepthCombo.setSelectedItem(source.getBitsPerPixel() + " bpp");
                 colorDepthCombo.setSelectedItem(source.getBitsPerPixel() + " bpp");
             }
             }
         });
         });
-
     }
     }
 
 
     /*
     /*
@@ -577,10 +634,8 @@ public final class AWTSettingsDialog extends JFrame {
      */
      */
     private void safeSetIconImages(List<? extends Image> icons) {
     private void safeSetIconImages(List<? extends Image> icons) {
         try {
         try {
-            // Due to Java bug 6445278, we try to set icon on our shared owner
-            // frame first.
-            // Otherwise, our alt-tab icon will be the Java default under
-            // Windows.
+            // Due to Java bug 6445278, we try to set icon on our shared owner frame first.
+            // Otherwise, our alt-tab icon will be the Java default under Windows.
             Window owner = getOwner();
             Window owner = getOwner();
             if (owner != null) {
             if (owner != null) {
                 Method setIconImages = owner.getClass().getMethod("setIconImages", List.class);
                 Method setIconImages = owner.getClass().getMethod("setIconImages", List.class);
@@ -608,9 +663,9 @@ public final class AWTSettingsDialog extends JFrame {
         boolean vsync = vsyncBox.isSelected();
         boolean vsync = vsyncBox.isSelected();
         boolean gamma = gammaBox.isSelected();
         boolean gamma = gammaBox.isSelected();
 
 
-        int width = Integer.parseInt(display.substring(0, display.indexOf(" x ")));
-        display = display.substring(display.indexOf(" x ") + 3);
-        int height = Integer.parseInt(display);
+        String[] parts = display.split(" x ");
+        int width = Integer.parseInt(parts[0]);
+        int height = Integer.parseInt(parts[1]);
 
 
         String depthString = (String) colorDepthCombo.getSelectedItem();
         String depthString = (String) colorDepthCombo.getSelectedItem();
         int depth = -1;
         int depth = -1;
@@ -639,21 +694,20 @@ public final class AWTSettingsDialog extends JFrame {
         }
         }
 
 
         // FIXME: Does not work in Linux
         // FIXME: Does not work in Linux
-        /*
-         * if (!fullscreen) { //query the current bit depth of the desktop int
-         * curDepth = GraphicsEnvironment.getLocalGraphicsEnvironment()
-         * .getDefaultScreenDevice().getDisplayMode().getBitDepth(); if (depth >
-         * curDepth) { showError(this,"Cannot choose a higher bit depth in
-         * windowed " + "mode than your current desktop bit depth"); return
-         * false; } }
-         */
-
-        boolean valid = false;
+//        if (!fullscreen) { //query the current bit depth of the desktop int
+//            curDepth = GraphicsEnvironment.getLocalGraphicsEnvironment()
+//                    .getDefaultScreenDevice().getDisplayMode().getBitDepth();
+//            if (depth > curDepth) {
+//                showError(this, "Cannot choose a higher bit depth in
+//                        windowed" + "mode than your current desktop bit depth");
+//                return false;
+//            }
+//        }
+
+        boolean valid = true;
 
 
         // test valid display mode when going full screen
         // test valid display mode when going full screen
-        if (!fullscreen) {
-            valid = true;
-        } else {
+        if (fullscreen) {
             GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
             GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
             valid = device.isFullScreenSupported();
             valid = device.isFullScreenSupported();
         }
         }
@@ -673,7 +727,10 @@ public final class AWTSettingsDialog extends JFrame {
             String appTitle = source.getTitle();
             String appTitle = source.getTitle();
 
 
             try {
             try {
+                logger.log(Level.INFO, "Saving AppSettings to PreferencesKey: {0}", appTitle);
                 source.save(appTitle);
                 source.save(appTitle);
+                AppSettings.printPreferences(appTitle);
+
             } catch (BackingStoreException ex) {
             } catch (BackingStoreException ex) {
                 logger.log(Level.WARNING, "Failed to save setting changes", ex);
                 logger.log(Level.WARNING, "Failed to save setting changes", ex);
             }
             }
@@ -769,7 +826,9 @@ public final class AWTSettingsDialog extends JFrame {
     private void updateAntialiasChoices() {
     private void updateAntialiasChoices() {
         // maybe in the future will add support for determining this info
         // maybe in the future will add support for determining this info
         // through PBuffer
         // through PBuffer
-        String[] choices = new String[] { resourceBundle.getString("antialias.disabled"), "2x", "4x", "6x", "8x", "16x" };
+        String[] choices = new String[] {
+                resourceBundle.getString("antialias.disabled"), "2x", "4x", "6x", "8x", "16x"
+        };
         antialiasCombo.setModel(new DefaultComboBoxModel<>(choices));
         antialiasCombo.setModel(new DefaultComboBoxModel<>(choices));
         antialiasCombo.setSelectedItem(choices[Math.min(source.getSamples() / 2, 5)]);
         antialiasCombo.setSelectedItem(choices[Math.min(source.getSamples() / 2, 5)]);
     }
     }
@@ -792,6 +851,12 @@ public final class AWTSettingsDialog extends JFrame {
         return url;
         return url;
     }
     }
 
 
+    /**
+     * Displays an error message dialog to the user.
+     *
+     * @param parent  The parent `Component` for the dialog.
+     * @param message The message `String` to display.
+     */
     private static void showError(java.awt.Component parent, String message) {
     private static void showError(java.awt.Component parent, String message) {
         JOptionPane.showMessageDialog(parent, message, "Error", JOptionPane.ERROR_MESSAGE);
         JOptionPane.showMessageDialog(parent, message, "Error", JOptionPane.ERROR_MESSAGE);
     }
     }
@@ -852,7 +917,7 @@ public final class AWTSettingsDialog extends JFrame {
      * Returns every possible bit depth for the given resolution.
      * Returns every possible bit depth for the given resolution.
      */
      */
     private static String[] getDepths(String resolution, DisplayMode[] modes) {
     private static String[] getDepths(String resolution, DisplayMode[] modes) {
-        List<String> depths = new ArrayList<>(4);
+        Set<String> depths = new LinkedHashSet<>(4); // Use LinkedHashSet for uniqueness and order
         for (DisplayMode mode : modes) {
         for (DisplayMode mode : modes) {
             int bitDepth = mode.getBitDepth();
             int bitDepth = mode.getBitDepth();
             if (bitDepth == DisplayMode.BIT_DEPTH_MULTI) {
             if (bitDepth == DisplayMode.BIT_DEPTH_MULTI) {
@@ -865,12 +930,8 @@ public final class AWTSettingsDialog extends JFrame {
                 continue;
                 continue;
             }
             }
             String res = mode.getWidth() + " x " + mode.getHeight();
             String res = mode.getWidth() + " x " + mode.getHeight();
-            if (!res.equals(resolution)) {
-                continue;
-            }
-            String depth = bitDepth + " bpp";
-            if (!depths.contains(depth)) {
-                depths.add(depth);
+            if (res.equals(resolution)) {
+                depths.add(bitDepth + " bpp");
             }
             }
         }
         }
 
 
@@ -884,10 +945,15 @@ public final class AWTSettingsDialog extends JFrame {
     }
     }
 
 
     /**
     /**
-     * Returns every possible refresh rate for the given resolution.
+     * Returns every possible unique refresh rate string ("XX Hz" or "???")
+     * for the given resolution from an array of `DisplayMode`s.
+     *
+     * @param resolution The resolution string (e.g., "1280 x 720") to filter by.
+     * @param modes      The array of `DisplayMode`s to process.
+     * @return An array of unique refresh rate strings.
      */
      */
     private static String[] getFrequencies(String resolution, DisplayMode[] modes) {
     private static String[] getFrequencies(String resolution, DisplayMode[] modes) {
-        List<String> freqs = new ArrayList<>(4);
+        Set<String> freqs = new LinkedHashSet<>(4); // Use LinkedHashSet for uniqueness and order
         for (DisplayMode mode : modes) {
         for (DisplayMode mode : modes) {
             String res = mode.getWidth() + " x " + mode.getHeight();
             String res = mode.getWidth() + " x " + mode.getHeight();
             String freq;
             String freq;
@@ -896,20 +962,19 @@ public final class AWTSettingsDialog extends JFrame {
             } else {
             } else {
                 freq = mode.getRefreshRate() + " Hz";
                 freq = mode.getRefreshRate() + " Hz";
             }
             }
-            if (res.equals(resolution) && !freqs.contains(freq)) {
-                freqs.add(freq);
-            }
+            freqs.add(freq);
         }
         }
 
 
         return freqs.toArray(new String[0]);
         return freqs.toArray(new String[0]);
     }
     }
 
 
     /**
     /**
-     * Chooses the closest frequency to 60 Hz.
-     * 
-     * @param resolution
-     * @param modes
-     * @return
+     * Chooses the closest known refresh rate to 60 Hz for a given resolution.
+     * If no known refresh rates are found for the resolution, returns `null`.
+     *
+     * @param resolution The resolution string (e.g., "1280 x 720") to find the best frequency for.
+     * @param modes      The array of `DisplayMode`s to search within.
+     * @return The best frequency string (e.g., "60 Hz") or `null` if no suitable frequency is found.
      */
      */
     private static String getBestFrequency(String resolution, DisplayMode[] modes) {
     private static String getBestFrequency(String resolution, DisplayMode[] modes) {
         int closest = Integer.MAX_VALUE;
         int closest = Integer.MAX_VALUE;

+ 4 - 3
jme3-core/src/main/java/com/jme3/effect/influencers/NewtonianParticleInfluencer.java

@@ -54,6 +54,8 @@ public class NewtonianParticleInfluencer extends DefaultParticleInfluencer {
     /** Emitters tangent rotation factor. */
     /** Emitters tangent rotation factor. */
     protected float surfaceTangentRotation;
     protected float surfaceTangentRotation;
 
 
+    protected Matrix3f tempMat3 = new Matrix3f();
+
     /**
     /**
      * Constructor. Sets velocity variation to 0.0f.
      * Constructor. Sets velocity variation to 0.0f.
      */
      */
@@ -71,9 +73,8 @@ public class NewtonianParticleInfluencer extends DefaultParticleInfluencer {
             // calculating surface tangent (velocity contains the 'normal' value)
             // calculating surface tangent (velocity contains the 'normal' value)
             temp.set(particle.velocity.z * surfaceTangentFactor, particle.velocity.y * surfaceTangentFactor, -particle.velocity.x * surfaceTangentFactor);
             temp.set(particle.velocity.z * surfaceTangentFactor, particle.velocity.y * surfaceTangentFactor, -particle.velocity.x * surfaceTangentFactor);
             if (surfaceTangentRotation != 0.0f) {// rotating the tangent
             if (surfaceTangentRotation != 0.0f) {// rotating the tangent
-                Matrix3f m = new Matrix3f();
-                m.fromAngleNormalAxis(FastMath.PI * surfaceTangentRotation, particle.velocity);
-                temp = m.multLocal(temp);
+                tempMat3.fromAngleNormalAxis(FastMath.PI * surfaceTangentRotation, particle.velocity);
+                temp = tempMat3.multLocal(temp);
             }
             }
             // applying normal factor (this must be done first)
             // applying normal factor (this must be done first)
             particle.velocity.multLocal(normalVelocity);
             particle.velocity.multLocal(normalVelocity);

+ 92 - 39
jme3-core/src/main/java/com/jme3/effect/shapes/EmitterMeshFaceShape.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -36,6 +36,7 @@ import com.jme3.math.Vector3f;
 import com.jme3.scene.Mesh;
 import com.jme3.scene.Mesh;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.util.BufferUtils;
 import com.jme3.util.BufferUtils;
+
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
 
 
@@ -52,79 +53,131 @@ public class EmitterMeshFaceShape extends EmitterMeshVertexShape {
     }
     }
 
 
     /**
     /**
-     * Constructor. It stores a copy of vertex list of all meshes.
-     * @param meshes
-     *        a list of meshes that will form the emitter's shape
+     * Constructor. Initializes the emitter shape with a list of meshes.
+     * The vertices and normals for all triangles of these meshes are
+     * extracted and stored internally.
+     *
+     * @param meshes a list of {@link Mesh} objects that will define the
+     * shape from which particles are emitted.
      */
      */
     public EmitterMeshFaceShape(List<Mesh> meshes) {
     public EmitterMeshFaceShape(List<Mesh> meshes) {
         super(meshes);
         super(meshes);
     }
     }
 
 
+    /**
+     * Sets the meshes for this emitter shape. This method extracts all
+     * triangle vertices and computes their normals, storing them internally
+     * for subsequent particle emission.
+     *
+     * @param meshes a list of {@link Mesh} objects to set as the emitter's shape.
+     */
     @Override
     @Override
     public void setMeshes(List<Mesh> meshes) {
     public void setMeshes(List<Mesh> meshes) {
         this.vertices = new ArrayList<List<Vector3f>>(meshes.size());
         this.vertices = new ArrayList<List<Vector3f>>(meshes.size());
         this.normals = new ArrayList<List<Vector3f>>(meshes.size());
         this.normals = new ArrayList<List<Vector3f>>(meshes.size());
+
         for (Mesh mesh : meshes) {
         for (Mesh mesh : meshes) {
             Vector3f[] vertexTable = BufferUtils.getVector3Array(mesh.getFloatBuffer(Type.Position));
             Vector3f[] vertexTable = BufferUtils.getVector3Array(mesh.getFloatBuffer(Type.Position));
             int[] indices = new int[3];
             int[] indices = new int[3];
-            List<Vector3f> vertices = new ArrayList<>(mesh.getTriangleCount() * 3);
-            List<Vector3f> normals = new ArrayList<>(mesh.getTriangleCount());
+            List<Vector3f> meshVertices = new ArrayList<>(mesh.getTriangleCount() * 3);
+            List<Vector3f> meshNormals = new ArrayList<>(mesh.getTriangleCount());
+
             for (int i = 0; i < mesh.getTriangleCount(); ++i) {
             for (int i = 0; i < mesh.getTriangleCount(); ++i) {
                 mesh.getTriangle(i, indices);
                 mesh.getTriangle(i, indices);
-                vertices.add(vertexTable[indices[0]]);
-                vertices.add(vertexTable[indices[1]]);
-                vertices.add(vertexTable[indices[2]]);
-                normals.add(FastMath.computeNormal(vertexTable[indices[0]], vertexTable[indices[1]], vertexTable[indices[2]]));
+
+                Vector3f v1 = vertexTable[indices[0]];
+                Vector3f v2 = vertexTable[indices[1]];
+                Vector3f v3 = vertexTable[indices[2]];
+
+                // Add all three vertices of the triangle
+                meshVertices.add(v1);
+                meshVertices.add(v2);
+                meshVertices.add(v3);
+
+                // Compute and add the normal for the current triangle face
+                meshNormals.add(FastMath.computeNormal(v1, v2, v3));
             }
             }
-            this.vertices.add(vertices);
-            this.normals.add(normals);
+            this.vertices.add(meshVertices);
+            this.normals.add(meshNormals);
         }
         }
     }
     }
 
 
     /**
     /**
-     * Randomly selects a point on a random face.
+     * Randomly selects a point on a random face of one of the stored meshes.
+     * The point is generated using barycentric coordinates to ensure uniform
+     * distribution within the selected triangle.
      *
      *
-     * @param store
-     *        storage for the coordinates of the selected point
+     * @param store a {@link Vector3f} object where the coordinates of the
+     *              selected point will be stored.
      */
      */
     @Override
     @Override
     public void getRandomPoint(Vector3f store) {
     public void getRandomPoint(Vector3f store) {
         int meshIndex = FastMath.nextRandomInt(0, vertices.size() - 1);
         int meshIndex = FastMath.nextRandomInt(0, vertices.size() - 1);
+        List<Vector3f> currVertices = vertices.get(meshIndex);
+        int numVertices = currVertices.size();
+
         // the index of the first vertex of a face (must be dividable by 3)
         // the index of the first vertex of a face (must be dividable by 3)
-        int vertIndex = FastMath.nextRandomInt(0, vertices.get(meshIndex).size() / 3 - 1) * 3;
-        // put the point somewhere between the first and the second vertex of a face
-        float moveFactor = FastMath.nextRandomFloat();
-        store.set(Vector3f.ZERO);
-        store.addLocal(vertices.get(meshIndex).get(vertIndex));
-        store.addLocal((vertices.get(meshIndex).get(vertIndex + 1).x - vertices.get(meshIndex).get(vertIndex).x) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 1).y - vertices.get(meshIndex).get(vertIndex).y) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 1).z - vertices.get(meshIndex).get(vertIndex).z) * moveFactor);
-        // move the result towards the last face vertex
-        moveFactor = FastMath.nextRandomFloat();
-        store.addLocal((vertices.get(meshIndex).get(vertIndex + 2).x - store.x) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 2).y - store.y) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 2).z - store.z) * moveFactor);
+        int faceIndex = FastMath.nextRandomInt(0, numVertices / 3 - 1);
+        int vertIndex = faceIndex * 3;
+
+        // Generate the random point on the triangle
+        generateRandomPointOnTriangle(currVertices, vertIndex, store);
     }
     }
 
 
     /**
     /**
-     * Randomly selects a point on a random face.
-     * The {@code normal} argument is set to the normal of the selected face.
+     * Randomly selects a point on a random face of one of the stored meshes,
+     * and also sets the normal of that selected face.
+     * The point is generated using barycentric coordinates for uniform distribution.
      *
      *
-     * @param store
-     *        storage for the coordinates of the selected point
-     * @param normal
-     *        storage for the normal of the selected face
+     * @param store  a {@link Vector3f} object where the coordinates of the
+     *               selected point will be stored.
+     * @param normal a {@link Vector3f} object where the normal of the
+     *               selected face will be stored.
      */
      */
     @Override
     @Override
     public void getRandomPointAndNormal(Vector3f store, Vector3f normal) {
     public void getRandomPointAndNormal(Vector3f store, Vector3f normal) {
         int meshIndex = FastMath.nextRandomInt(0, vertices.size() - 1);
         int meshIndex = FastMath.nextRandomInt(0, vertices.size() - 1);
+        List<Vector3f> currVertices = vertices.get(meshIndex);
+        int numVertices = currVertices.size();
+
         // the index of the first vertex of a face (must be dividable by 3)
         // the index of the first vertex of a face (must be dividable by 3)
-        int faceIndex = FastMath.nextRandomInt(0, vertices.get(meshIndex).size() / 3 - 1);
+        int faceIndex = FastMath.nextRandomInt(0, numVertices / 3 - 1);
         int vertIndex = faceIndex * 3;
         int vertIndex = faceIndex * 3;
-        // put the point somewhere between the first and the second vertex of a face
-        float moveFactor = FastMath.nextRandomFloat();
-        store.set(Vector3f.ZERO);
-        store.addLocal(vertices.get(meshIndex).get(vertIndex));
-        store.addLocal((vertices.get(meshIndex).get(vertIndex + 1).x - vertices.get(meshIndex).get(vertIndex).x) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 1).y - vertices.get(meshIndex).get(vertIndex).y) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 1).z - vertices.get(meshIndex).get(vertIndex).z) * moveFactor);
-        // move the result towards the last face vertex
-        moveFactor = FastMath.nextRandomFloat();
-        store.addLocal((vertices.get(meshIndex).get(vertIndex + 2).x - store.x) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 2).y - store.y) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 2).z - store.z) * moveFactor);
+
+        // Generate the random point on the triangle
+        generateRandomPointOnTriangle(currVertices, vertIndex, store);
+        // Set the normal from the pre-computed normals list for the selected face
         normal.set(normals.get(meshIndex).get(faceIndex));
         normal.set(normals.get(meshIndex).get(faceIndex));
     }
     }
+
+    /**
+     * Internal method to generate a random point within a specific triangle
+     * using barycentric coordinates.
+     *
+     * @param currVertices The list of vertices for the current mesh.
+     * @param vertIndex    The starting index of the triangle's first vertex
+     *                     within the {@code currVertices} list.
+     * @param store        A {@link Vector3f} object where the calculated point will be stored.
+     */
+    private void generateRandomPointOnTriangle(List<Vector3f> currVertices, int vertIndex, Vector3f store) {
+
+        Vector3f v1 = currVertices.get(vertIndex);
+        Vector3f v2 = currVertices.get(vertIndex + 1);
+        Vector3f v3 = currVertices.get(vertIndex + 2);
+
+        // Generate random barycentric coordinates
+        float u = FastMath.nextRandomFloat();
+        float v = FastMath.nextRandomFloat();
+
+        if ((u + v) > 1) {
+            u = 1 - u;
+            v = 1 - v;
+        }
+
+        // P = v1 + u * (v2 - v1) + v * (v3 - v1)
+        store.x = v1.x + u * (v2.x - v1.x) + v * (v3.x - v1.x);
+        store.y = v1.y + u * (v2.y - v1.y) + v * (v3.y - v1.y);
+        store.z = v1.z + u * (v2.z - v1.z) + v * (v3.z - v1.z);
+    }
+
 }
 }

+ 140 - 57
jme3-core/src/main/java/com/jme3/font/BitmapFont.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -31,14 +31,22 @@
  */
  */
 package com.jme3.font;
 package com.jme3.font;
 
 
-import com.jme3.export.*;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
 import com.jme3.material.Material;
 import com.jme3.material.Material;
 
 
 import java.io.IOException;
 import java.io.IOException;
 
 
 /**
 /**
- * Represents a font within jME that is generated with the AngelCode Bitmap Font Generator
+ * Represents a font loaded from a bitmap font definition
+ * (e.g., generated by <a href="https://libgdx.com/wiki/tools/hiero">AngelCode Bitmap Font Generator</a>).
+ * It manages character sets, font pages (textures), and provides utilities for text measurement and rendering.
+ *
  * @author dhdd
  * @author dhdd
+ * @author Yonghoon
  */
  */
 public class BitmapFont implements Savable {
 public class BitmapFont implements Savable {
 
 
@@ -87,33 +95,30 @@ public class BitmapFont implements Savable {
         Bottom
         Bottom
     }
     }
 
 
+    // The character set containing definitions for each character (glyph) in the font.
     private BitmapCharacterSet charSet;
     private BitmapCharacterSet charSet;
+    // An array of materials, where each material corresponds to a font page (texture).
     private Material[] pages;
     private Material[] pages;
+    // Indicates whether this font is designed for right-to-left (RTL) text rendering.
     private boolean rightToLeft = false;
     private boolean rightToLeft = false;
     // For cursive bitmap fonts in which letter shape is determined by the adjacent glyphs.
     // For cursive bitmap fonts in which letter shape is determined by the adjacent glyphs.
     private GlyphParser glyphParser;
     private GlyphParser glyphParser;
 
 
     /**
     /**
-     * @return true, if this is a right-to-left font, otherwise it will return false.
+     * Creates a new instance of `BitmapFont`.
+     * This constructor is primarily used for deserialization.
      */
      */
-    public boolean isRightToLeft() {
-        return rightToLeft;
+    public BitmapFont() {
     }
     }
 
 
     /**
     /**
-     * Specify if this is a right-to-left font. By default it is set to false.
-     * This can be "overwritten" in the BitmapText constructor.
+     * Creates a new {@link BitmapText} instance initialized with this font.
+     * The label's size will be set to the font's rendered size, and its text content
+     * will be set to the provided string.
      *
      *
-     * @param rightToLeft true &rarr; right-to-left, false &rarr; left-to-right
-     *     (default=false)
+     * @param content The initial text content for the label.
+     * @return A new {@link BitmapText} instance.
      */
      */
-    public void setRightToLeft(boolean rightToLeft) {
-        this.rightToLeft = rightToLeft;
-    }
-
-    public BitmapFont() {
-    }
-
     public BitmapText createLabel(String content) {
     public BitmapText createLabel(String content) {
         BitmapText label = new BitmapText(this);
         BitmapText label = new BitmapText(this);
         label.setSize(getCharSet().getRenderedSize());
         label.setSize(getCharSet().getRenderedSize());
@@ -121,27 +126,81 @@ public class BitmapFont implements Savable {
         return label;
         return label;
     }
     }
 
 
+    /**
+     * Checks if this font is configured for right-to-left (RTL) text rendering.
+     *
+     * @return true if this is a right-to-left font, otherwise false (default is left-to-right).
+     */
+    public boolean isRightToLeft() {
+        return rightToLeft;
+    }
+
+    /**
+     * Specifies whether this font should be rendered as right-to-left (RTL).
+     * By default, it is set to false (left-to-right).
+     *
+     * @param rightToLeft true to enable right-to-left rendering; false for left-to-right.
+     */
+    public void setRightToLeft(boolean rightToLeft) {
+        this.rightToLeft = rightToLeft;
+    }
+
+    /**
+     * Returns the preferred size of the font, which is typically its rendered size.
+     *
+     * @return The preferred size of the font in font units.
+     */
     public float getPreferredSize() {
     public float getPreferredSize() {
         return getCharSet().getRenderedSize();
         return getCharSet().getRenderedSize();
     }
     }
 
 
+    /**
+     * Sets the character set for this font. The character set contains
+     * information about individual glyphs, their positions, and kerning data.
+     *
+     * @param charSet The {@link BitmapCharacterSet} to associate with this font.
+     */
     public void setCharSet(BitmapCharacterSet charSet) {
     public void setCharSet(BitmapCharacterSet charSet) {
         this.charSet = charSet;
         this.charSet = charSet;
     }
     }
 
 
+    /**
+     * Sets the array of materials (font pages) for this font. Each material
+     * corresponds to a texture page containing character bitmaps.
+     * The character set's page size is also updated based on the number of pages.
+     *
+     * @param pages An array of {@link Material} objects representing the font pages.
+     */
     public void setPages(Material[] pages) {
     public void setPages(Material[] pages) {
         this.pages = pages;
         this.pages = pages;
         charSet.setPageSize(pages.length);
         charSet.setPageSize(pages.length);
     }
     }
 
 
+    /**
+     * Retrieves a specific font page material by its index.
+     *
+     * @param index The index of the font page to retrieve.
+     * @return The {@link Material} for the specified font page.
+     * @throws IndexOutOfBoundsException if the index is out of bounds.
+     */
     public Material getPage(int index) {
     public Material getPage(int index) {
         return pages[index];
         return pages[index];
     }
     }
 
 
+    /**
+     * Returns the total number of font pages (materials) associated with this font.
+     *
+     * @return The number of font pages.
+     */
     public int getPageSize() {
     public int getPageSize() {
         return pages.length;
         return pages.length;
     }
     }
 
 
+    /**
+     * Retrieves the character set associated with this font.
+     *
+     * @return The {@link BitmapCharacterSet} of this font.
+     */
     public BitmapCharacterSet getCharSet() {
     public BitmapCharacterSet getCharSet() {
         return charSet;
         return charSet;
     }
     }
@@ -192,26 +251,19 @@ public class BitmapFont implements Savable {
         return c.getKerning(nextChar);
         return c.getKerning(nextChar);
     }
     }
 
 
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        OutputCapsule oc = ex.getCapsule(this);
-        oc.write(charSet, "charSet", null);
-        oc.write(pages, "pages", null);
-        oc.write(rightToLeft, "rightToLeft", false);
-        oc.write(glyphParser, "glyphParser", null);
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        InputCapsule ic = im.getCapsule(this);
-        charSet = (BitmapCharacterSet) ic.readSavable("charSet", null);
-        Savable[] pagesSavable = ic.readSavableArray("pages", null);
-        pages = new Material[pagesSavable.length];
-        System.arraycopy(pagesSavable, 0, pages, 0, pages.length);
-        rightToLeft = ic.readBoolean("rightToLeft", false);
-        glyphParser = (GlyphParser) ic.readSavable("glyphParser", null);
-    }
-
+    /**
+     * Calculates the width of the given text in font units.
+     * This method accounts for character advances, kerning, and line breaks.
+     * It also attempts to skip custom color tags (e.g., "\#RRGGBB#" or "\#RRGGBBAA#")
+     * based on a specific format.
+     * <p>
+     * Note: This method calculates width in "font units" where the font's
+     * {@link BitmapCharacterSet#getRenderedSize() rendered size} is the base.
+     * Actual pixel scaling for display is typically handled by {@link BitmapText}.
+     *
+     * @param text The text to measure.
+     * @return The maximum line width of the text in font units.
+     */
     public float getLineWidth(CharSequence text) {
     public float getLineWidth(CharSequence text) {
         // This method will probably always be a bit of a maintenance
         // This method will probably always be a bit of a maintenance
         // nightmare since it bases its calculation on a different
         // nightmare since it bases its calculation on a different
@@ -252,29 +304,36 @@ public class BitmapFont implements Savable {
         boolean firstCharOfLine = true;
         boolean firstCharOfLine = true;
 //        float sizeScale = (float) block.getSize() / charSet.getRenderedSize();
 //        float sizeScale = (float) block.getSize() / charSet.getRenderedSize();
         float sizeScale = 1f;
         float sizeScale = 1f;
-        CharSequence characters = glyphParser != null ? glyphParser.parse(text) : text;
 
 
-        for (int i = 0; i < characters.length(); i++) {
-            char theChar = characters.charAt(i);
-            if (theChar == '\n') {
+        // Use GlyphParser if available for complex script shaping (e.g., cursive fonts).
+        CharSequence processedText = glyphParser != null ? glyphParser.parse(text) : text;
+
+        for (int i = 0; i < processedText.length(); i++) {
+            char currChar = processedText.charAt(i);
+            if (currChar == '\n') {
                 maxLineWidth = Math.max(maxLineWidth, lineWidth);
                 maxLineWidth = Math.max(maxLineWidth, lineWidth);
                 lineWidth = 0f;
                 lineWidth = 0f;
                 firstCharOfLine = true;
                 firstCharOfLine = true;
                 continue;
                 continue;
             }
             }
-            BitmapCharacter c = charSet.getCharacter(theChar);
+            BitmapCharacter c = charSet.getCharacter(currChar);
             if (c != null) {
             if (c != null) {
-                if (theChar == '\\' && i < characters.length() - 1 && characters.charAt(i + 1) == '#') {
-                    if (i + 5 < characters.length() && characters.charAt(i + 5) == '#') {
+                // Custom color tag skipping logic:
+                // Assumes tags are of the form `\#RRGGBB#` (9 chars total) or `\#RRGGBBAA#` (12 chars total).
+                if (currChar == '\\' && i < processedText.length() - 1 && processedText.charAt(i + 1) == '#') {
+                    // Check for `\#XXXXX#` (6 chars after '\', including final '#')
+                    if (i + 5 < processedText.length() && processedText.charAt(i + 5) == '#') {
                         i += 5;
                         i += 5;
                         continue;
                         continue;
-                    } else if (i + 8 < characters.length() && characters.charAt(i + 8) == '#') {
+                    }
+                    // Check for `\#XXXXXXXX#` (9 chars after '\', including final '#')
+                    else if (i + 8 < processedText.length() && processedText.charAt(i + 8) == '#') {
                         i += 8;
                         i += 8;
                         continue;
                         continue;
                     }
                     }
                 }
                 }
                 if (!firstCharOfLine) {
                 if (!firstCharOfLine) {
-                    lineWidth += findKerningAmount(lastChar, theChar) * sizeScale;
+                    lineWidth += findKerningAmount(lastChar, currChar) * sizeScale;
                 } else {
                 } else {
                     if (rightToLeft) {
                     if (rightToLeft) {
                         // Ignore offset, so it will be compatible with BitmapText.getLineWidth().
                         // Ignore offset, so it will be compatible with BitmapText.getLineWidth().
@@ -292,7 +351,7 @@ public class BitmapFont implements Savable {
                 // If this is the last character of a line, then we really should
                 // If this is the last character of a line, then we really should
                 // have only added its width. The advance may include extra spacing
                 // have only added its width. The advance may include extra spacing
                 // that we don't care about.
                 // that we don't care about.
-                if (i == characters.length() - 1 || characters.charAt(i + 1) == '\n') {
+                if (i == processedText.length() - 1 || processedText.charAt(i + 1) == '\n') {
                     if (rightToLeft) {
                     if (rightToLeft) {
                         // In RTL text we move the letter x0 by its xAdvance, so
                         // In RTL text we move the letter x0 by its xAdvance, so
                         // we should add it to lineWidth.
                         // we should add it to lineWidth.
@@ -315,30 +374,54 @@ public class BitmapFont implements Savable {
         return Math.max(maxLineWidth, lineWidth);
         return Math.max(maxLineWidth, lineWidth);
     }
     }
 
 
-
     /**
     /**
-     * Merge two fonts.
-     * If two font have the same style, merge will fail.
-     * @param newFont Style must be assigned to this.
-     * author: Yonghoon
+     * Merges another {@link BitmapFont} into this one.
+     * This operation combines the character sets and font pages.
+     * If both fonts contain the same style, the merge will fail and throw a RuntimeException.
+     *
+     * @param newFont The {@link BitmapFont} to merge into this one. It must have a style assigned.
      */
      */
     public void merge(BitmapFont newFont) {
     public void merge(BitmapFont newFont) {
         charSet.merge(newFont.charSet);
         charSet.merge(newFont.charSet);
         final int size1 = this.pages.length;
         final int size1 = this.pages.length;
         final int size2 = newFont.pages.length;
         final int size2 = newFont.pages.length;
 
 
-        Material[] tmp = new Material[size1+size2];
+        Material[] tmp = new Material[size1 + size2];
         System.arraycopy(this.pages, 0, tmp, 0, size1);
         System.arraycopy(this.pages, 0, tmp, 0, size1);
         System.arraycopy(newFont.pages, 0, tmp, size1, size2);
         System.arraycopy(newFont.pages, 0, tmp, size1, size2);
 
 
         this.pages = tmp;
         this.pages = tmp;
-
-//        this.pages = Arrays.copyOf(this.pages, size1+size2);
-//        System.arraycopy(newFont.pages, 0, this.pages, size1, size2);
     }
     }
 
 
+    /**
+     * Sets the style for the font's character set.
+     * This method is typically used when a font file contains only one style
+     * but needs to be assigned a specific style identifier for merging
+     * with other multi-style fonts.
+     *
+     * @param style The integer style identifier to set.
+     */
     public void setStyle(int style) {
     public void setStyle(int style) {
         charSet.setStyle(style);
         charSet.setStyle(style);
     }
     }
 
 
-}
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(charSet, "charSet", null);
+        oc.write(pages, "pages", null);
+        oc.write(rightToLeft, "rightToLeft", false);
+        oc.write(glyphParser, "glyphParser", null);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+        charSet = (BitmapCharacterSet) ic.readSavable("charSet", null);
+        Savable[] pagesSavable = ic.readSavableArray("pages", null);
+        pages = new Material[pagesSavable.length];
+        System.arraycopy(pagesSavable, 0, pages, 0, pages.length);
+        rightToLeft = ic.readBoolean("rightToLeft", false);
+        glyphParser = (GlyphParser) ic.readSavable("glyphParser", null);
+    }
+}

+ 179 - 98
jme3-core/src/main/java/com/jme3/font/BitmapText.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -38,20 +38,38 @@ import com.jme3.math.ColorRGBA;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.RenderManager;
 import com.jme3.scene.Node;
 import com.jme3.scene.Node;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.Cloner;
+
 import java.util.regex.Matcher;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
 
 
 /**
 /**
+ * `BitmapText` is a spatial node that displays text using a {@link BitmapFont}.
+ * It handles text layout, alignment, wrapping, coloring, and styling based on
+ * the properties set via its methods. The text is rendered as a series of
+ * quads (rectangles) with character textures from the font's pages.
+ *
  * @author YongHoon
  * @author YongHoon
  */
  */
 public class BitmapText extends Node {
 public class BitmapText extends Node {
 
 
+    // The font used to render this text.
     private BitmapFont font;
     private BitmapFont font;
+    // Stores the text content and its layout properties (size, box, alignment, etc.).
     private StringBlock block;
     private StringBlock block;
+    // A flag indicating whether the text needs to be re-assembled
     private boolean needRefresh = true;
     private boolean needRefresh = true;
+    // An array of `BitmapTextPage` instances, each corresponding to a font page.
     private BitmapTextPage[] textPages;
     private BitmapTextPage[] textPages;
+    // Manages the individual letter quads, their positions, colors, and styles.
     private Letters letters;
     private Letters letters;
 
 
+    /**
+     * Creates a new `BitmapText` instance using the specified font.
+     * The text will be rendered left-to-right by default, unless the font itself
+     * is configured for right-to-left rendering.
+     *
+     * @param font The {@link BitmapFont} to use for rendering the text (not null).
+     */
     public BitmapText(BitmapFont font) {
     public BitmapText(BitmapFont font) {
         this(font, font.isRightToLeft(), false);
         this(font, font.isRightToLeft(), false);
     }
     }
@@ -69,6 +87,15 @@ public class BitmapText extends Node {
         this(font, rightToLeft, false);
         this(font, rightToLeft, false);
     }
     }
 
 
+    /**
+     * Creates a new `BitmapText` instance with the specified font, text direction,
+     * and a flag for array-based rendering.
+     *
+     * @param font The {@link BitmapFont} to use for rendering the text (not null).
+     * @param rightToLeft true for right-to-left text rendering, false for left-to-right.
+     * @param arrayBased If true, the internal text pages will use array-based buffers for rendering.
+     * This might affect performance or compatibility depending on the renderer.
+     */
     public BitmapText(BitmapFont font, boolean rightToLeft, boolean arrayBased) {
     public BitmapText(BitmapFont font, boolean rightToLeft, boolean arrayBased) {
         textPages = new BitmapTextPage[font.getPageSize()];
         textPages = new BitmapTextPage[font.getPageSize()];
         for (int page = 0; page < textPages.length; page++) {
         for (int page = 0; page < textPages.length; page++) {
@@ -84,7 +111,7 @@ public class BitmapText extends Node {
 
 
     @Override
     @Override
     public BitmapText clone() {
     public BitmapText clone() {
-        return (BitmapText)super.clone(false);
+        return (BitmapText) super.clone(false);
     }
     }
 
 
     /**
     /**
@@ -114,13 +141,19 @@ public class BitmapText extends Node {
         // so I guess cloning doesn't come up that often.
         // so I guess cloning doesn't come up that often.
     }
     }
 
 
+    /**
+     * Returns the {@link BitmapFont} currently used by this `BitmapText` instance.
+     *
+     * @return The {@link BitmapFont} object.
+     */
     public BitmapFont getFont() {
     public BitmapFont getFont() {
         return font;
         return font;
     }
     }
 
 
     /**
     /**
-     * Changes text size
-     * @param size text size
+     * Sets the size of the text. This value scales the font's base character sizes.
+     *
+     * @param size The desired text size (e.g., in world units or pixels).
      */
      */
     public void setSize(float size) {
     public void setSize(float size) {
         block.setSize(size);
         block.setSize(size);
@@ -128,13 +161,20 @@ public class BitmapText extends Node {
         letters.invalidate();
         letters.invalidate();
     }
     }
 
 
+    /**
+     * Returns the current size of the text.
+     *
+     * @return The text size.
+     */
     public float getSize() {
     public float getSize() {
         return block.getSize();
         return block.getSize();
     }
     }
 
 
     /**
     /**
+     * Sets the text content to be displayed.
      *
      *
-     * @param text charsequence to change text to
+     * @param text The `CharSequence` (e.g., `String` or `StringBuilder`) to display.
+     * If null, the text will be set to an empty string.
      */
      */
     public void setText(CharSequence text) {
     public void setText(CharSequence text) {
         // note: text.toString() is free if text is already a java.lang.String.
         // note: text.toString() is free if text is already a java.lang.String.
@@ -142,72 +182,50 @@ public class BitmapText extends Node {
     }
     }
 
 
     /**
     /**
+     * Sets the text content to be displayed.
+     * If the new text is the same as the current text, no update occurs.
+     * Otherwise, the internal `StringBlock` and `Letters` objects are updated,
+     * and a refresh is flagged to re-layout the text.
      *
      *
-     * @param text String to change text to
+     * @param text The `String` to display. If null, the text will be set to an empty string.
      */
      */
     public void setText(String text) {
     public void setText(String text) {
         text = text == null ? "" : text;
         text = text == null ? "" : text;
-        if (text == block.getText() || block.getText().equals(text)) {
+        if (block.getText().equals(text)) {
             return;
             return;
         }
         }
 
 
-        /*
-        The problem with the below block is that StringBlock carries
-        pretty much all of the text-related state of the BitmapText such
-        as size, text box, alignment, etc.
-
-        I'm not sure why this change was needed and the commit message was
-        not entirely helpful because it purports to fix a problem that I've
-        never encountered.
-
-        If block.setText("") doesn't do the right thing then that's where
-        the fix should go because StringBlock carries too much information to
-        be blown away every time.  -pspeed
-
-        Change was made:
-        http://code.google.com/p/jmonkeyengine/source/detail?spec=svn9389&r=9389
-        Diff:
-        http://code.google.com/p/jmonkeyengine/source/diff?path=/trunk/engine/src/core/com/jme3/font/BitmapText.java&format=side&r=9389&old_path=/trunk/engine/src/core/com/jme3/font/BitmapText.java&old=8843
-
-        // If the text is empty, reset
-        if (text.isEmpty()) {
-            detachAllChildren();
-
-            for (int page = 0; page < textPages.length; page++) {
-                textPages[page] = new BitmapTextPage(font, true, page);
-                attachChild(textPages[page]);
-            }
-
-            block = new StringBlock();
-            letters = new Letters(font, block, letters.getQuad().isRightToLeft());
-        }
-        */
-
         // Update the text content
         // Update the text content
         block.setText(text);
         block.setText(text);
         letters.setText(text);
         letters.setText(text);
-
-        // Flag for refresh
         needRefresh = true;
         needRefresh = true;
     }
     }
 
 
     /**
     /**
-     * @return returns text
+     * Returns the current text content displayed by this `BitmapText` instance.
+     *
+     * @return The text content as a `String`.
      */
      */
     public String getText() {
     public String getText() {
         return block.getText();
         return block.getText();
     }
     }
 
 
     /**
     /**
-     * @return color of the text
+     * Returns the base color applied to the entire text.
+     * Note: Substring colors set via `setColor(int, int, ColorRGBA)` or
+     * `setColor(String, ColorRGBA)` will override this base color for their respective ranges.
+     *
+     * @return The base {@link ColorRGBA} of the text.
      */
      */
     public ColorRGBA getColor() {
     public ColorRGBA getColor() {
         return letters.getBaseColor();
         return letters.getBaseColor();
     }
     }
 
 
     /**
     /**
-     * changes text color. all substring colors are deleted.
-     * @param color new color of text
+     * Sets the base color for the entire text.
+     * This operation will clear any previously set substring colors.
+     *
+     * @param color The new base {@link ColorRGBA} for the text.
      */
      */
     public void setColor(ColorRGBA color) {
     public void setColor(ColorRGBA color) {
         letters.setColor(color);
         letters.setColor(color);
@@ -216,26 +234,34 @@ public class BitmapText extends Node {
     }
     }
 
 
     /**
     /**
-     *  Sets an overall alpha that will be applied to all
-     *  letters.  If the alpha passed is -1 then alpha reverts
-     *  to default... which will be 1 for anything unspecified
-     *  and color tags will be reset to 1 or their encoded
-     *  alpha.
+     * Sets an overall alpha (transparency) value that will be applied to all
+     * letters in the text.
+     * If the alpha passed is -1, the alpha reverts to its default behavior:
+     * 1.0 for unspecified parts, and the encoded alpha from any color tags.
      *
      *
-     * @param alpha the desired alpha, or -1 to revert to the default
+     * @param alpha The desired alpha value (0.0 for fully transparent, 1.0 for fully opaque),
+     * or -1 to revert to default alpha behavior.
      */
      */
     public void setAlpha(float alpha) {
     public void setAlpha(float alpha) {
         letters.setBaseAlpha(alpha);
         letters.setBaseAlpha(alpha);
         needRefresh = true;
         needRefresh = true;
     }
     }
 
 
+    /**
+     * Returns the current base alpha value applied to the text.
+     *
+     * @return The base alpha value, or -1 if default alpha behavior is active.
+     */
     public float getAlpha() {
     public float getAlpha() {
         return letters.getBaseAlpha();
         return letters.getBaseAlpha();
     }
     }
 
 
     /**
     /**
-     * Define the area where the BitmapText will be rendered.
-     * @param rect position and size box where text is rendered
+     * Defines a rectangular bounding box within which the text will be rendered.
+     * This box is used for text wrapping and alignment.
+     *
+     * @param rect The {@link Rectangle} defining the position (x, y) and size (width, height)
+     * of the text rendering area.
      */
      */
     public void setBox(Rectangle rect) {
     public void setBox(Rectangle rect) {
         block.setTextBox(rect);
         block.setTextBox(rect);
@@ -244,14 +270,19 @@ public class BitmapText extends Node {
     }
     }
 
 
     /**
     /**
-     * @return height of the line
+     * Returns the height of a single line of text, scaled by the current text size.
+     *
+     * @return The calculated line height.
      */
      */
     public float getLineHeight() {
     public float getLineHeight() {
         return font.getLineHeight(block);
         return font.getLineHeight(block);
     }
     }
 
 
     /**
     /**
-     * @return height of whole text block
+     * Calculates and returns the total height of the entire text block,
+     * considering all lines and the defined text box (if any).
+     *
+     * @return The total height of the text block.
      */
      */
     public float getHeight() {
     public float getHeight() {
         if (needRefresh) {
         if (needRefresh) {
@@ -266,7 +297,9 @@ public class BitmapText extends Node {
     }
     }
 
 
     /**
     /**
-     * @return width of line
+     * Calculates and returns the maximum width of any line in the text block.
+     *
+     * @return The maximum line width of the text.
      */
      */
     public float getLineWidth() {
     public float getLineWidth() {
         if (needRefresh) {
         if (needRefresh) {
@@ -282,7 +315,9 @@ public class BitmapText extends Node {
     }
     }
 
 
     /**
     /**
-     * @return line count
+     * Returns the number of lines the text currently occupies.
+     *
+     * @return The total number of lines.
      */
      */
     public int getLineCount() {
     public int getLineCount() {
         if (needRefresh) {
         if (needRefresh) {
@@ -291,14 +326,21 @@ public class BitmapText extends Node {
         return block.getLineCount();
         return block.getLineCount();
     }
     }
 
 
+    /**
+     * Returns the current line wrapping mode set for this text.
+     *
+     * @return The {@link LineWrapMode} enum value.
+     */
     public LineWrapMode getLineWrapMode() {
     public LineWrapMode getLineWrapMode() {
         return block.getLineWrapMode();
         return block.getLineWrapMode();
     }
     }
 
 
     /**
     /**
-     * Set horizontal alignment. Applicable only when text bound is set.
+     * Sets the horizontal alignment for the text within its bounding box.
+     * This is only applicable if a text bounding box has been set using {@link #setBox(Rectangle)}.
      *
      *
-     * @param align the desired alignment (such as Align.Left)
+     * @param align The desired horizontal alignment (e.g., {@link Align#Left}, {@link Align#Center}, {@link Align#Right}).
+     * @throws RuntimeException If a bounding box is not set and `align` is not `Align.Left`.
      */
      */
     public void setAlignment(BitmapFont.Align align) {
     public void setAlignment(BitmapFont.Align align) {
         if (block.getTextBox() == null && align != Align.Left) {
         if (block.getTextBox() == null && align != Align.Left) {
@@ -310,9 +352,11 @@ public class BitmapText extends Node {
     }
     }
 
 
     /**
     /**
-     * Set vertical alignment. Applicable only when text bound is set.
+     * Sets the vertical alignment for the text within its bounding box.
+     * This is only applicable if a text bounding box has been set using {@link #setBox(Rectangle)}.
      *
      *
-     * @param align the desired alignment (such as Align.Top)
+     * @param align The desired vertical alignment (e.g., {@link VAlign#Top}, {@link VAlign#Center}, {@link VAlign#Bottom}).
+     * @throws RuntimeException If a bounding box is not set and `align` is not `VAlign.Top`.
      */
      */
     public void setVerticalAlignment(BitmapFont.VAlign align) {
     public void setVerticalAlignment(BitmapFont.VAlign align) {
         if (block.getTextBox() == null && align != VAlign.Top) {
         if (block.getTextBox() == null && align != VAlign.Top) {
@@ -323,28 +367,42 @@ public class BitmapText extends Node {
         needRefresh = true;
         needRefresh = true;
     }
     }
 
 
+    /**
+     * Returns the current horizontal alignment set for the text.
+     *
+     * @return The current {@link Align} value.
+     */
     public BitmapFont.Align getAlignment() {
     public BitmapFont.Align getAlignment() {
         return block.getAlignment();
         return block.getAlignment();
     }
     }
 
 
+    /**
+     * Returns the current vertical alignment set for the text.
+     *
+     * @return The current {@link VAlign} value.
+     */
     public BitmapFont.VAlign getVerticalAlignment() {
     public BitmapFont.VAlign getVerticalAlignment() {
         return block.getVerticalAlignment();
         return block.getVerticalAlignment();
     }
     }
 
 
     /**
     /**
-     * Set the font style of substring. If font doesn't contain style, default style is used
-     * @param start start index to set style. inclusive.
-     * @param end   end index to set style. EXCLUSIVE.
-     * @param style the style to apply
+     * Sets the font style for a specific substring of the text.
+     * If the font does not contain the specified style, the default style will be used.
+     *
+     * @param start The starting index of the substring (inclusive).
+     * @param end   The ending index of the substring (exclusive).
+     * @param style The integer style identifier to apply.
      */
      */
     public void setStyle(int start, int end, int style) {
     public void setStyle(int start, int end, int style) {
         letters.setStyle(start, end, style);
         letters.setStyle(start, end, style);
     }
     }
 
 
     /**
     /**
-     * Set the font style of substring. If font doesn't contain style, default style is applied
-     * @param regexp regular expression
-     * @param style  the style to apply
+     * Sets the font style for all substrings matching a given regular expression.
+     * If the font does not contain the specified style, the default style will be used.
+     *
+     * @param regexp The regular expression string to match against the text.
+     * @param style  The integer style identifier to apply.
      */
      */
     public void setStyle(String regexp, int style) {
     public void setStyle(String regexp, int style) {
         Pattern p = Pattern.compile(regexp);
         Pattern p = Pattern.compile(regexp);
@@ -355,10 +413,11 @@ public class BitmapText extends Node {
     }
     }
 
 
     /**
     /**
-     * Set the color of substring.
-     * @param start start index to set style. inclusive.
-     * @param end   end index to set style. EXCLUSIVE.
-     * @param color the desired color
+     * Sets the color for a specific substring of the text.
+     *
+     * @param start The starting index of the substring (inclusive).
+     * @param end   The ending index of the substring (exclusive).
+     * @param color The desired {@link ColorRGBA} to apply to the substring.
      */
      */
     public void setColor(int start, int end, ColorRGBA color) {
     public void setColor(int start, int end, ColorRGBA color) {
         letters.setColor(start, end, color);
         letters.setColor(start, end, color);
@@ -367,9 +426,10 @@ public class BitmapText extends Node {
     }
     }
 
 
     /**
     /**
-     * Set the color of substring.
-     * @param regexp regular expression
-     * @param color  the desired color
+     * Sets the color for all substrings matching a given regular expression.
+     *
+     * @param regexp The regular expression string to match against the text.
+     * @param color  The desired {@link ColorRGBA} to apply.
      */
      */
     public void setColor(String regexp, ColorRGBA color) {
     public void setColor(String regexp, ColorRGBA color) {
         Pattern p = Pattern.compile(regexp);
         Pattern p = Pattern.compile(regexp);
@@ -382,7 +442,10 @@ public class BitmapText extends Node {
     }
     }
 
 
     /**
     /**
-     * @param tabs tab positions
+     * Sets custom tab stop positions for the text.
+     * Tab characters (`\t`) will align to these specified positions.
+     *
+     * @param tabs An array of float values representing the horizontal tab stop positions.
      */
      */
     public void setTabPosition(float... tabs) {
     public void setTabPosition(float... tabs) {
         block.setTabPosition(tabs);
         block.setTabPosition(tabs);
@@ -391,8 +454,10 @@ public class BitmapText extends Node {
     }
     }
 
 
     /**
     /**
-     * used for the tabs over the last tab position.
-     * @param width tab size
+     * Sets the default width for tabs that extend beyond the last defined tab position.
+     * This value is used if a tab character is encountered after all custom tab stops have been passed.
+     *
+     * @param width The default width for tabs in font units.
      */
      */
     public void setTabWidth(float width) {
     public void setTabWidth(float width) {
         block.setTabWidth(width);
         block.setTabWidth(width);
@@ -401,10 +466,10 @@ public class BitmapText extends Node {
     }
     }
 
 
     /**
     /**
-     * for setLineWrapType(LineWrapType.NoWrap),
-     * set the last character when the text exceeds the bound.
+     * When {@link LineWrapMode#NoWrap} is used and the text exceeds the bounding box,
+     * this character will be appended to indicate truncation (e.g., '...').
      *
      *
-     * @param c the character to indicate truncated text
+     * @param c The character to use as the ellipsis.
      */
      */
     public void setEllipsisChar(char c) {
     public void setEllipsisChar(char c) {
         block.setEllipsisChar(c);
         block.setEllipsisChar(c);
@@ -413,12 +478,18 @@ public class BitmapText extends Node {
     }
     }
 
 
     /**
     /**
-     * Available only when bounding is set. <code>setBox()</code> method call is needed in advance.
-     * true when
-     * @param wrap NoWrap   : Letters over the text bound is not shown. the last character is set to '...'(0x2026)
-     *             Character: Character is split at the end of the line.
-     *             Word     : Word is split at the end of the line.
-     *             Clip     : The text is hard-clipped at the border including showing only a partial letter if it goes beyond the text bound.
+     * Sets the line wrapping mode for the text. This is only applicable when
+     * a text bounding box has been set using {@link #setBox(Rectangle)}.
+     *
+     * @param wrap The desired {@link LineWrapMode}:
+     * <ul>
+     * <li>{@link LineWrapMode#NoWrap}: Letters exceeding the text bound are not shown.
+     * The last visible character might be replaced by an ellipsis character
+     * (set via {@link #setEllipsisChar(char)}).</li>
+     * <li>{@link LineWrapMode#Character}: Text is split at the end of the line, even in the middle of a word.</li>
+     * <li>{@link LineWrapMode#Word}: Words are split at the end of the line.</li>
+     * <li>{@link LineWrapMode#Clip}: The text is hard-clipped at the border, potentially showing only a partial letter.</li>
+     * </ul>
      */
      */
     public void setLineWrapMode(LineWrapMode wrap) {
     public void setLineWrapMode(LineWrapMode wrap) {
         if (block.getLineWrapMode() != wrap) {
         if (block.getLineWrapMode() != wrap) {
@@ -436,28 +507,38 @@ public class BitmapText extends Node {
         }
         }
     }
     }
 
 
+    /**
+     * Assembles the text by generating the quad list (character positions and sizes)
+     * and then populating the vertex buffers of each `BitmapTextPage`.
+     * This method is called internally when `needRefresh` is true.
+     */
     private void assemble() {
     private void assemble() {
-        // first generate quad list
+        // First, generate or update the list of letter quads
+        // based on current text and layout properties.
         letters.update();
         letters.update();
-        for (int i = 0; i < textPages.length; i++) {
-            textPages[i].assemble(letters);
+        // Then, for each font page, assemble its mesh data from the generated quads.
+        for (BitmapTextPage textPage : textPages) {
+            textPage.assemble(letters);
         }
         }
         needRefresh = false;
         needRefresh = false;
     }
     }
 
 
+    /**
+     * Renders the `BitmapText` spatial. This method iterates through each
+     * `BitmapTextPage`, sets its texture, and renders it using the provided
+     * `RenderManager`.
+     *
+     * @param rm The `RenderManager` responsible for drawing.
+     * @param color The base color to apply during rendering. Note that colors
+     * set per-substring will override this for those parts.
+     */
     public void render(RenderManager rm, ColorRGBA color) {
     public void render(RenderManager rm, ColorRGBA color) {
         for (BitmapTextPage page : textPages) {
         for (BitmapTextPage page : textPages) {
             Material mat = page.getMaterial();
             Material mat = page.getMaterial();
             mat.setTexture("ColorMap", page.getTexture());
             mat.setTexture("ColorMap", page.getTexture());
-            //ColorRGBA original = getColor(mat, "Color");
-            //mat.setColor("Color", color);
+            // mat.setColor("Color", color); // If the material supports a "Color" parameter
             mat.render(page, rm);
             mat.render(page, rm);
-
-            //if( original == null ) {
-            //    mat.clearParam("Color");
-            //} else {
-            //    mat.setColor("Color", original);
-            //}
         }
         }
     }
     }
+
 }
 }

+ 4 - 4
jme3-core/src/main/java/com/jme3/material/RenderState.java

@@ -1629,9 +1629,9 @@ public class RenderState implements Cloneable, Savable {
             state.backStencilFunction = additionalState.backStencilFunction;
             state.backStencilFunction = additionalState.backStencilFunction;
 
 
             state.frontStencilMask = additionalState.frontStencilMask;
             state.frontStencilMask = additionalState.frontStencilMask;
-            state.frontStencilReference = additionalState.frontStencilMask;
+            state.frontStencilReference = additionalState.frontStencilReference;
             state.backStencilMask = additionalState.backStencilMask;
             state.backStencilMask = additionalState.backStencilMask;
-            state.backStencilReference = additionalState.backStencilMask;
+            state.backStencilReference = additionalState.backStencilReference;
         } else {
         } else {
             state.stencilTest = stencilTest;
             state.stencilTest = stencilTest;
 
 
@@ -1647,9 +1647,9 @@ public class RenderState implements Cloneable, Savable {
             state.backStencilFunction = backStencilFunction;
             state.backStencilFunction = backStencilFunction;
 
 
             state.frontStencilMask = frontStencilMask;
             state.frontStencilMask = frontStencilMask;
-            state.frontStencilReference = frontStencilMask;
+            state.frontStencilReference = frontStencilReference;
             state.backStencilMask = backStencilMask;
             state.backStencilMask = backStencilMask;
-            state.backStencilReference = backStencilMask;
+            state.backStencilReference = backStencilReference;
         }
         }
         if (additionalState.applyLineWidth) {
         if (additionalState.applyLineWidth) {
             state.lineWidth = additionalState.lineWidth;
             state.lineWidth = additionalState.lineWidth;

+ 6 - 6
jme3-core/src/main/java/com/jme3/renderer/RenderManager.java

@@ -95,8 +95,8 @@ public class RenderManager {
     private final ArrayList<ViewPort> viewPorts = new ArrayList<>();
     private final ArrayList<ViewPort> viewPorts = new ArrayList<>();
     private final ArrayList<ViewPort> postViewPorts = new ArrayList<>();
     private final ArrayList<ViewPort> postViewPorts = new ArrayList<>();
     private final HashMap<Class, PipelineContext> contexts = new HashMap<>();
     private final HashMap<Class, PipelineContext> contexts = new HashMap<>();
-    private final LinkedList<PipelineContext> usedContexts = new LinkedList<>();
-    private final LinkedList<RenderPipeline> usedPipelines = new LinkedList<>();
+    private final ArrayList<PipelineContext> usedContexts = new ArrayList<>();
+    private final ArrayList<RenderPipeline> usedPipelines = new ArrayList<>();
     private RenderPipeline defaultPipeline = new ForwardPipeline();
     private RenderPipeline defaultPipeline = new ForwardPipeline();
     private Camera prevCam = null;
     private Camera prevCam = null;
     private Material forcedMaterial = null;
     private Material forcedMaterial = null;
@@ -1367,11 +1367,11 @@ public class RenderManager {
         }
         }
         
         
         // cleanup for used render pipelines and pipeline contexts only
         // cleanup for used render pipelines and pipeline contexts only
-        for (PipelineContext c : usedContexts) {
-            c.endContextRenderFrame(this);
+        for (int i = 0; i < usedContexts.size(); i++) {
+            usedContexts.get(i).endContextRenderFrame(this);
         }
         }
-        for (RenderPipeline p : usedPipelines) {
-            p.endRenderFrame(this);
+        for (int i = 0; i < usedPipelines.size(); i++) {
+            usedPipelines.get(i).endRenderFrame(this);
         }
         }
         usedContexts.clear();
         usedContexts.clear();
         usedPipelines.clear();
         usedPipelines.clear();

+ 22 - 16
jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java

@@ -121,7 +121,7 @@ public class ArmatureDebugAppState extends BaseAppState {
     }
     }
 
 
     private void registerInput() {
     private void registerInput() {
-        inputManager.addMapping(PICK_JOINT, new MouseButtonTrigger(MouseInput.BUTTON_LEFT), new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
+        inputManager.addMapping(PICK_JOINT, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
         inputManager.addMapping(TOGGLE_JOINTS, new KeyTrigger(KeyInput.KEY_F10));
         inputManager.addMapping(TOGGLE_JOINTS, new KeyTrigger(KeyInput.KEY_F10));
         inputManager.addListener(actionListener, PICK_JOINT, TOGGLE_JOINTS);
         inputManager.addListener(actionListener, PICK_JOINT, TOGGLE_JOINTS);
     }
     }
@@ -248,6 +248,10 @@ public class ArmatureDebugAppState extends BaseAppState {
 
 
         @Override
         @Override
         public void onAction(String name, boolean isPressed, float tpf) {
         public void onAction(String name, boolean isPressed, float tpf) {
+            if (!isEnabled()) {
+                return;
+            }
+
             if (name.equals(PICK_JOINT)) {
             if (name.equals(PICK_JOINT)) {
                 if (isPressed) {
                 if (isPressed) {
                     // Start counting click delay on mouse press
                     // Start counting click delay on mouse press
@@ -290,22 +294,24 @@ public class ArmatureDebugAppState extends BaseAppState {
             }
             }
         }
         }
 
 
-        private void printJointInfo(Joint selectedjoint, ArmatureDebugger ad) {
+        private void printJointInfo(Joint selectedJoint, ArmatureDebugger ad) {
             if (enableJointInfoLogging) {
             if (enableJointInfoLogging) {
-                System.err.println("-----------------------");
-                System.err.println("Selected Joint : " + selectedjoint.getName() + " in armature " + ad.getName());
-                System.err.println("Root Bone : " + (selectedjoint.getParent() == null));
-                System.err.println("-----------------------");
-                System.err.println("Local translation: " + selectedjoint.getLocalTranslation());
-                System.err.println("Local rotation: " + selectedjoint.getLocalRotation());
-                System.err.println("Local scale: " + selectedjoint.getLocalScale());
-                System.err.println("---");
-                System.err.println("Model translation: " + selectedjoint.getModelTransform().getTranslation());
-                System.err.println("Model rotation: " + selectedjoint.getModelTransform().getRotation());
-                System.err.println("Model scale: " + selectedjoint.getModelTransform().getScale());
-                System.err.println("---");
-                System.err.println("Bind inverse Transform: ");
-                System.err.println(selectedjoint.getInverseModelBindMatrix());
+                String info = "\n-----------------------\n" +
+                        "Selected Joint : " + selectedJoint.getName() + " in armature " + ad.getName() + "\n" +
+                        "Root Bone : " + (selectedJoint.getParent() == null) + "\n" +
+                        "-----------------------\n" +
+                        "Local translation: " + selectedJoint.getLocalTranslation() + "\n" +
+                        "Local rotation: " + selectedJoint.getLocalRotation() + "\n" +
+                        "Local scale: " + selectedJoint.getLocalScale() + "\n" +
+                        "---\n" +
+                        "Model translation: " + selectedJoint.getModelTransform().getTranslation() + "\n" +
+                        "Model rotation: " + selectedJoint.getModelTransform().getRotation() + "\n" +
+                        "Model scale: " + selectedJoint.getModelTransform().getScale() + "\n" +
+                        "---\n" +
+                        "Bind inverse Transform: \n" +
+                        selectedJoint.getInverseModelBindMatrix();
+
+                logger.log(Level.INFO, info);
             }
             }
         }
         }
 
 

+ 52 - 6
jme3-core/src/main/java/com/jme3/shader/VarType.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2024 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -45,6 +45,9 @@ import com.jme3.texture.TextureArray;
 import com.jme3.texture.TextureCubeMap;
 import com.jme3.texture.TextureCubeMap;
 import com.jme3.texture.TextureImage;
 import com.jme3.texture.TextureImage;
 
 
+/**
+ * Enum representing various GLSL variable types and their corresponding Java types.
+ */
 public enum VarType {
 public enum VarType {
     
     
     Float("float", float.class, Float.class),
     Float("float", float.class, Float.class),
@@ -59,7 +62,7 @@ public enum VarType {
     Vector4Array(true, false, "vec4", Vector4f[].class),
     Vector4Array(true, false, "vec4", Vector4f[].class),
     
     
     Int("int", int.class, Integer.class),
     Int("int", int.class, Integer.class),
-    Boolean("bool", Boolean.class, boolean.class),
+    Boolean("bool", boolean.class, Boolean.class),
 
 
     Matrix3(true, false, "mat3", Matrix3f.class),
     Matrix3(true, false, "mat3", Matrix3f.class),
     Matrix4(true, false, "mat4", Matrix4f.class),
     Matrix4(true, false, "mat4", Matrix4f.class),
@@ -83,8 +86,14 @@ public enum VarType {
     private boolean textureType = false;
     private boolean textureType = false;
     private boolean imageType = false;
     private boolean imageType = false;
     private final String glslType;
     private final String glslType;
-    private Class<?>[] javaTypes;
+    private final Class<?>[] javaTypes;
 
 
+    /**
+     * Constructs a VarType with the specified GLSL type and corresponding Java types.
+     *
+     * @param glslType  the GLSL type name(s)
+     * @param javaTypes the Java classes mapped to this GLSL type
+     */
     VarType(String glslType, Class<?>... javaTypes) {
     VarType(String glslType, Class<?>... javaTypes) {
         this.glslType = glslType;
         this.glslType = glslType;
         if (javaTypes != null) {
         if (javaTypes != null) {
@@ -94,6 +103,14 @@ public enum VarType {
         }
         }
     }
     }
 
 
+    /**
+     * Constructs a VarType with additional flags for multi-data and texture types.
+     *
+     * @param multiData   true if this type uses multiple data elements (e.g. arrays, matrices)
+     * @param textureType true if this type represents a texture sampler
+     * @param glslType    the GLSL type name(s)
+     * @param javaTypes   the Java classes mapped to this GLSL type
+     */
     VarType(boolean multiData, boolean textureType, String glslType, Class<?>... javaTypes) {
     VarType(boolean multiData, boolean textureType, String glslType, Class<?>... javaTypes) {
         this.usesMultiData = multiData;
         this.usesMultiData = multiData;
         this.textureType = textureType;
         this.textureType = textureType;
@@ -104,7 +121,16 @@ public enum VarType {
             this.javaTypes = new Class<?>[0];
             this.javaTypes = new Class<?>[0];
         }
         }
     }
     }
-    
+
+    /**
+     * Constructs a VarType with flags for multi-data, texture, and image types.
+     *
+     * @param multiData   true if this type uses multiple data elements
+     * @param textureType true if this type represents a texture sampler
+     * @param imageType   true if this type represents an image
+     * @param glslType    the GLSL type name(s)
+     * @param javaTypes   the Java classes mapped to this GLSL type
+     */
     VarType(boolean multiData, boolean textureType, boolean imageType, String glslType, Class<?>... javaTypes) {
     VarType(boolean multiData, boolean textureType, boolean imageType, String glslType, Class<?>... javaTypes) {
         this(multiData, textureType, glslType, javaTypes);
         this(multiData, textureType, glslType, javaTypes);
         this.imageType = imageType;
         this.imageType = imageType;
@@ -127,25 +153,45 @@ public enum VarType {
 
 
     /**
     /**
      * Get the java types mapped to this VarType
      * Get the java types mapped to this VarType
-     * 
+     *
      * @return an array of classes mapped to this VarType
      * @return an array of classes mapped to this VarType
      */
      */
     public Class<?>[] getJavaType() {
     public Class<?>[] getJavaType() {
         return javaTypes;
         return javaTypes;
     }
     }
 
 
+    /**
+     * Returns whether this VarType represents a texture sampler type.
+     *
+     * @return true if this is a texture type, false otherwise
+     */
     public boolean isTextureType() {
     public boolean isTextureType() {
         return textureType;
         return textureType;
     }
     }
-    
+
+    /**
+     * Returns whether this VarType represents an image type.
+     *
+     * @return true if this is an image type, false otherwise
+     */
     public boolean isImageType() {
     public boolean isImageType() {
         return imageType;
         return imageType;
     }
     }
 
 
+    /**
+     * Returns whether this VarType uses multiple data elements (e.g. arrays or matrices).
+     *
+     * @return true if this type uses multiple data elements, false otherwise
+     */
     public boolean usesMultiData() {
     public boolean usesMultiData() {
         return usesMultiData;
         return usesMultiData;
     }
     }
 
 
+    /**
+     * Returns the GLSL type name(s) associated with this VarType.
+     *
+     * @return the GLSL type string (e.g. "float", "vec3", "sampler2D")
+     */
     public String getGlslType() {
     public String getGlslType() {
         return glslType;
         return glslType;
     }
     }

+ 18 - 30
jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java

@@ -32,10 +32,6 @@
 package com.jme3.shadow;
 package com.jme3.shadow;
 
 
 import com.jme3.asset.AssetManager;
 import com.jme3.asset.AssetManager;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
 import com.jme3.material.Material;
 import com.jme3.material.Material;
 import com.jme3.material.RenderState;
 import com.jme3.material.RenderState;
 import com.jme3.math.Matrix4f;
 import com.jme3.math.Matrix4f;
@@ -45,38 +41,38 @@ import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.renderer.ViewPort;
 import com.jme3.renderer.queue.RenderQueue;
 import com.jme3.renderer.queue.RenderQueue;
 import com.jme3.texture.FrameBuffer;
 import com.jme3.texture.FrameBuffer;
+import com.jme3.util.TempVars;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.JmeCloneable;
 import com.jme3.util.clone.JmeCloneable;
 
 
-import java.io.IOException;
-
 /**
 /**
  * Generic abstract filter that holds common implementations for the different
  * Generic abstract filter that holds common implementations for the different
  * shadow filters
  * shadow filters
  *
  *
  * @author Rémy Bouquet aka Nehon
  * @author Rémy Bouquet aka Nehon
  */
  */
-public abstract class AbstractShadowFilter<T extends AbstractShadowRenderer> extends Filter implements Cloneable, JmeCloneable {
+public abstract class AbstractShadowFilter<T extends AbstractShadowRenderer> extends Filter implements JmeCloneable {
 
 
     protected T shadowRenderer;
     protected T shadowRenderer;
     protected ViewPort viewPort;
     protected ViewPort viewPort;
 
 
+    private final Vector4f tempVec4 = new Vector4f();
+    private final Matrix4f tempMat4 = new Matrix4f();
+
     /**
     /**
      * For serialization only. Do not use.
      * For serialization only. Do not use.
      */
      */
     protected AbstractShadowFilter() {
     protected AbstractShadowFilter() {
     }
     }
-    
+
     /**
     /**
-     * Abstract class constructor
+     * Creates an AbstractShadowFilter. Subclasses invoke this constructor.
      *
      *
-     * @param manager the application asset manager
-     * @param shadowMapSize the size of the rendered shadowmaps (512,1024,2048,
-     * etc...)
-     * @param shadowRenderer the shadowRenderer to use for this Filter
+     * @param assetManager The application's asset manager.
+     * @param shadowMapSize The size of the rendered shadow maps (e.g., 512, 1024, 2048).
+     * @param shadowRenderer The shadowRenderer to use for this Filter
      */
      */
-    @SuppressWarnings("all")
-    protected AbstractShadowFilter(AssetManager manager, int shadowMapSize, T shadowRenderer) {
+    protected AbstractShadowFilter(AssetManager assetManager, int shadowMapSize, T shadowRenderer) {
         super("Post Shadow");
         super("Post Shadow");
         this.shadowRenderer = shadowRenderer;
         this.shadowRenderer = shadowRenderer;
         // this is legacy setting for shadows with backface shadows
         // this is legacy setting for shadows with backface shadows
@@ -93,18 +89,21 @@ public abstract class AbstractShadowFilter<T extends AbstractShadowRenderer> ext
         return true;
         return true;
     }
     }
 
 
+    /**
+     * @deprecated Use {@link #getMaterial()} instead.
+     * @return The Material used by this filter.
+     */
+    @Deprecated
     public Material getShadowMaterial() {       
     public Material getShadowMaterial() {       
         return material;
         return material;
     }
     }
 
 
-    Vector4f tmpv = new Vector4f();
-
     @Override
     @Override
     protected void preFrame(float tpf) {
     protected void preFrame(float tpf) {
         shadowRenderer.preFrame(tpf);
         shadowRenderer.preFrame(tpf);
-        material.setMatrix4("ViewProjectionMatrixInverse", viewPort.getCamera().getViewProjectionMatrix().invert());
         Matrix4f m = viewPort.getCamera().getViewProjectionMatrix();
         Matrix4f m = viewPort.getCamera().getViewProjectionMatrix();
-        material.setVector4("ViewProjectionMatrixRow2", tmpv.set(m.m20, m.m21, m.m22, m.m23));
+        material.setMatrix4("ViewProjectionMatrixInverse", tempMat4.set(m).invertLocal());
+        material.setVector4("ViewProjectionMatrixRow2", tempVec4.set(m.m20, m.m21, m.m22, m.m23));
     }
     }
 
 
     @Override
     @Override
@@ -337,15 +336,4 @@ public abstract class AbstractShadowFilter<T extends AbstractShadowRenderer> ext
         shadowRenderer.setPostShadowMaterial(material);
         shadowRenderer.setPostShadowMaterial(material);
     }
     }
 
 
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        super.write(ex);
-        OutputCapsule oc = ex.getCapsule(this);
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        super.read(im);
-        InputCapsule ic = im.getCapsule(this);
-    }
 }
 }

+ 41 - 44
jme3-core/src/main/java/com/jme3/shadow/PointLightShadowRenderer.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -51,33 +51,39 @@ import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 import java.io.IOException;
 
 
 /**
 /**
- * PointLightShadowRenderer renders shadows for a point light
+ * Renders shadows for a {@link PointLight}. This renderer uses six cameras,
+ * one for each face of a cube map, to capture shadows from the point light's
+ * perspective.
  *
  *
  * @author Rémy Bouquet aka Nehon
  * @author Rémy Bouquet aka Nehon
  */
  */
 public class PointLightShadowRenderer extends AbstractShadowRenderer {
 public class PointLightShadowRenderer extends AbstractShadowRenderer {
 
 
+    /**
+     * The fixed number of cameras used for rendering point light shadows (6 for a cube map).
+     */
     public static final int CAM_NUMBER = 6;
     public static final int CAM_NUMBER = 6;
+
     protected PointLight light;
     protected PointLight light;
     protected Camera[] shadowCams;
     protected Camera[] shadowCams;
-    private Geometry[] frustums = null;
+    protected Geometry[] frustums = null;
+    protected final Vector3f X_NEG = Vector3f.UNIT_X.mult(-1f);
+    protected final Vector3f Y_NEG = Vector3f.UNIT_Y.mult(-1f);
+    protected final Vector3f Z_NEG = Vector3f.UNIT_Z.mult(-1f);
 
 
     /**
     /**
-     * Used for serialization.
-     * Use PointLightShadowRenderer#PointLightShadowRenderer(AssetManager
-     * assetManager, int shadowMapSize)
-     * instead.
+     * For serialization only. Do not use.
      */
      */
     protected PointLightShadowRenderer() {
     protected PointLightShadowRenderer() {
         super();
         super();
     }
     }
 
 
     /**
     /**
-     * Creates a PointLightShadowRenderer
+     * Creates a new {@code PointLightShadowRenderer} instance.
      *
      *
-     * @param assetManager the application asset manager
-     * @param shadowMapSize the size of the rendered shadowmaps (512,1024,2048,
-     * etc...)
+     * @param assetManager The application's asset manager.
+     * @param shadowMapSize The size of the rendered shadow maps (e.g., 512, 1024, 2048).
+     * Higher values produce better quality shadows but may impact performance.
      */
      */
     public PointLightShadowRenderer(AssetManager assetManager, int shadowMapSize) {
     public PointLightShadowRenderer(AssetManager assetManager, int shadowMapSize) {
         super(assetManager, shadowMapSize, CAM_NUMBER);
         super(assetManager, shadowMapSize, CAM_NUMBER);
@@ -86,7 +92,7 @@ public class PointLightShadowRenderer extends AbstractShadowRenderer {
 
 
     private void init(int shadowMapSize) {
     private void init(int shadowMapSize) {
         shadowCams = new Camera[CAM_NUMBER];
         shadowCams = new Camera[CAM_NUMBER];
-        for (int i = 0; i < CAM_NUMBER; i++) {
+        for (int i = 0; i < shadowCams.length; i++) {
             shadowCams[i] = new Camera(shadowMapSize, shadowMapSize);
             shadowCams[i] = new Camera(shadowMapSize, shadowMapSize);
         }
         }
     }
     }
@@ -95,9 +101,9 @@ public class PointLightShadowRenderer extends AbstractShadowRenderer {
     protected void initFrustumCam() {
     protected void initFrustumCam() {
         Camera viewCam = viewPort.getCamera();
         Camera viewCam = viewPort.getCamera();
         frustumCam = viewCam.clone();
         frustumCam = viewCam.clone();
-        frustumCam.setFrustum(viewCam.getFrustumNear(), zFarOverride, viewCam.getFrustumLeft(), viewCam.getFrustumRight(), viewCam.getFrustumTop(), viewCam.getFrustumBottom());
+        frustumCam.setFrustum(viewCam.getFrustumNear(), zFarOverride,
+                viewCam.getFrustumLeft(), viewCam.getFrustumRight(), viewCam.getFrustumTop(), viewCam.getFrustumBottom());
     }
     }
-    
 
 
     @Override
     @Override
     protected void updateShadowCams(Camera viewCam) {
     protected void updateShadowCams(Camera viewCam) {
@@ -107,31 +113,21 @@ public class PointLightShadowRenderer extends AbstractShadowRenderer {
             return;
             return;
         }
         }
 
 
-        //bottom
-        shadowCams[0].setAxes(Vector3f.UNIT_X.mult(-1f), Vector3f.UNIT_Z.mult(-1f), Vector3f.UNIT_Y.mult(-1f));
-
-        //top
-        shadowCams[1].setAxes(Vector3f.UNIT_X.mult(-1f), Vector3f.UNIT_Z, Vector3f.UNIT_Y);
-
-        //forward
-        shadowCams[2].setAxes(Vector3f.UNIT_X.mult(-1f), Vector3f.UNIT_Y, Vector3f.UNIT_Z.mult(-1f));
-
-        //backward
-        shadowCams[3].setAxes(Vector3f.UNIT_X, Vector3f.UNIT_Y, Vector3f.UNIT_Z);
-
-        //left
-        shadowCams[4].setAxes(Vector3f.UNIT_Z, Vector3f.UNIT_Y, Vector3f.UNIT_X.mult(-1f));
-
-        //right
-        shadowCams[5].setAxes(Vector3f.UNIT_Z.mult(-1f), Vector3f.UNIT_Y, Vector3f.UNIT_X);
-
-        for (int i = 0; i < CAM_NUMBER; i++) {
-            shadowCams[i].setFrustumPerspective(90f, 1f, 0.1f, light.getRadius());
-            shadowCams[i].setLocation(light.getPosition());
-            shadowCams[i].update();
-            shadowCams[i].updateViewProjection();
+        // Configure axes for each of the six cube map cameras (positive/negative X, Y, Z)
+        shadowCams[0].setAxes(X_NEG, Z_NEG, Y_NEG);                                 // -Y (bottom)
+        shadowCams[1].setAxes(X_NEG, Vector3f.UNIT_Z, Vector3f.UNIT_Y);             // +Y (top)
+        shadowCams[2].setAxes(X_NEG, Vector3f.UNIT_Y, Z_NEG);                       // +Z (forward)
+        shadowCams[3].setAxes(Vector3f.UNIT_X, Vector3f.UNIT_Y, Vector3f.UNIT_Z);   // -Z (backward)
+        shadowCams[4].setAxes(Vector3f.UNIT_Z, Vector3f.UNIT_Y, X_NEG);             // -X (left)
+        shadowCams[5].setAxes(Z_NEG, Vector3f.UNIT_Y, Vector3f.UNIT_X);             // +X (right)
+
+        // Set perspective and location for all shadow cameras
+        for (Camera shadowCam : shadowCams) {
+            shadowCam.setFrustumPerspective(90f, 1f, 0.1f, light.getRadius());
+            shadowCam.setLocation(light.getPosition());
+            shadowCam.update();
+            shadowCam.updateViewProjection();
         }
         }
-
     }
     }
 
 
     @Override
     @Override
@@ -160,7 +156,7 @@ public class PointLightShadowRenderer extends AbstractShadowRenderer {
         if (frustums == null) {
         if (frustums == null) {
             frustums = new Geometry[CAM_NUMBER];
             frustums = new Geometry[CAM_NUMBER];
             Vector3f[] points = new Vector3f[8];
             Vector3f[] points = new Vector3f[8];
-            for (int i = 0; i < 8; i++) {
+            for (int i = 0; i < points.length; i++) {
                 points[i] = new Vector3f();
                 points[i] = new Vector3f();
             }
             }
             for (int i = 0; i < CAM_NUMBER; i++) {
             for (int i = 0; i < CAM_NUMBER; i++) {
@@ -168,8 +164,9 @@ public class PointLightShadowRenderer extends AbstractShadowRenderer {
                 frustums[i] = createFrustum(points, i);
                 frustums[i] = createFrustum(points, i);
             }
             }
         }
         }
-        if (frustums[shadowMapIndex].getParent() == null) {
-            ((Node) viewPort.getScenes().get(0)).attachChild(frustums[shadowMapIndex]);
+        Geometry geo = frustums[shadowMapIndex];
+        if (geo.getParent() == null) {
+            ((Node) viewPort.getScenes().get(0)).attachChild(geo);
         }
         }
     }
     }
 
 
@@ -237,13 +234,13 @@ public class PointLightShadowRenderer extends AbstractShadowRenderer {
         }
         }
 
 
         Camera cam = viewCam;
         Camera cam = viewCam;
-        if(frustumCam != null){
-            cam = frustumCam;            
+        if (frustumCam != null) {
+            cam = frustumCam;
             cam.setLocation(viewCam.getLocation());
             cam.setLocation(viewCam.getLocation());
             cam.setRotation(viewCam.getRotation());
             cam.setRotation(viewCam.getRotation());
         }
         }
         TempVars vars = TempVars.get();
         TempVars vars = TempVars.get();
-        boolean intersects = light.intersectsFrustum(cam,vars);
+        boolean intersects = light.intersectsFrustum(cam, vars);
         vars.release();
         vars.release();
         return intersects;
         return intersects;
     }
     }

+ 144 - 39
jme3-core/src/main/java/com/jme3/system/AppSettings.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -39,6 +39,8 @@ import java.io.OutputStream;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 import java.util.Properties;
 import java.util.Properties;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import java.util.prefs.BackingStoreException;
 import java.util.prefs.BackingStoreException;
 import java.util.prefs.Preferences;
 import java.util.prefs.Preferences;
 
 
@@ -58,6 +60,8 @@ public final class AppSettings extends HashMap<String, Object> {
 
 
     private static final long serialVersionUID = 1L;
     private static final long serialVersionUID = 1L;
 
 
+    private static final Logger logger = Logger.getLogger(AppSettings.class.getName());
+
     private static final AppSettings defaults = new AppSettings(false);
     private static final AppSettings defaults = new AppSettings(false);
 
 
     /**
     /**
@@ -295,6 +299,7 @@ public final class AppSettings extends HashMap<String, Object> {
         defaults.put("UseRetinaFrameBuffer", false);
         defaults.put("UseRetinaFrameBuffer", false);
         defaults.put("WindowYPosition", 0);
         defaults.put("WindowYPosition", 0);
         defaults.put("WindowXPosition", 0);
         defaults.put("WindowXPosition", 0);
+        defaults.put("X11PlatformPreferred", false);
         //  defaults.put("Icons", null);
         //  defaults.put("Icons", null);
     }
     }
 
 
@@ -507,12 +512,25 @@ public final class AppSettings extends HashMap<String, Object> {
      * @return the corresponding value, or 0 if not set
      * @return the corresponding value, or 0 if not set
      */
      */
     public int getInteger(String key) {
     public int getInteger(String key) {
-        Integer i = (Integer) get(key);
-        if (i == null) {
-            return 0;
-        }
+        return getInteger(key, 0);
+    }
 
 
-        return i.intValue();
+    /**
+     * Get an integer from the settings.
+     * <p>
+     * If the key is not set, or the stored value is not an Integer, then the
+     * provided default value is returned.
+     *
+     * @param key the key of an integer setting
+     * @param defaultValue the value to return if the key is not found or the
+     * value is not an integer
+     */
+    public int getInteger(String key, int defaultValue) {
+        Object val = get(key);
+        if (val == null) {
+            return defaultValue;
+        }
+        return (Integer) val;
     }
     }
 
 
     /**
     /**
@@ -524,12 +542,25 @@ public final class AppSettings extends HashMap<String, Object> {
      * @return the corresponding value, or false if not set
      * @return the corresponding value, or false if not set
      */
      */
     public boolean getBoolean(String key) {
     public boolean getBoolean(String key) {
-        Boolean b = (Boolean) get(key);
-        if (b == null) {
-            return false;
-        }
+        return getBoolean(key, false);
+    }
 
 
-        return b.booleanValue();
+    /**
+     * Get a boolean from the settings.
+     * <p>
+     * If the key is not set, or the stored value is not a Boolean, then the
+     * provided default value is returned.
+     *
+     * @param key the key of a boolean setting
+     * @param defaultValue the value to return if the key is not found or the
+     * value is not a boolean
+     */
+    public boolean getBoolean(String key, boolean defaultValue) {
+        Object val = get(key);
+        if (val == null) {
+            return defaultValue;
+        }
+        return (Boolean) val;
     }
     }
 
 
     /**
     /**
@@ -541,12 +572,25 @@ public final class AppSettings extends HashMap<String, Object> {
      * @return the corresponding value, or null if not set
      * @return the corresponding value, or null if not set
      */
      */
     public String getString(String key) {
     public String getString(String key) {
-        String s = (String) get(key);
-        if (s == null) {
-            return null;
-        }
+        return getString(key, null);
+    }
 
 
-        return s;
+    /**
+     * Get a string from the settings.
+     * <p>
+     * If the key is not set, or the stored value is not a String, then the
+     * provided default value is returned.
+     *
+     * @param key the key of a string setting
+     * @param defaultValue the value to return if the key is not found or the
+     * value is not a string
+     */
+    public String getString(String key, String defaultValue) {
+        Object val = get(key);
+        if (val == null) {
+            return defaultValue;
+        }
+        return (String) val;
     }
     }
 
 
     /**
     /**
@@ -558,12 +602,25 @@ public final class AppSettings extends HashMap<String, Object> {
      * @return the corresponding value, or 0 if not set
      * @return the corresponding value, or 0 if not set
      */
      */
     public float getFloat(String key) {
     public float getFloat(String key) {
-        Float f = (Float) get(key);
-        if (f == null) {
-            return 0f;
-        }
+        return getFloat(key, 0f);
+    }
 
 
-        return f.floatValue();
+    /**
+     * Get a float from the settings.
+     * <p>
+     * If the key is not set, or the stored value is not a Float, then the
+     * provided default value is returned.
+     *
+     * @param key the key of a float setting
+     * @param defaultValue the value to return if the key is not found or the
+     * value is not a float
+     */
+    public float getFloat(String key, float defaultValue) {
+        Object val = get(key);
+        if (val == null) {
+            return defaultValue;
+        }
+        return (Float) val;
     }
     }
 
 
     /**
     /**
@@ -573,7 +630,7 @@ public final class AppSettings extends HashMap<String, Object> {
      * @param value the desired integer value
      * @param value the desired integer value
      */
      */
     public void putInteger(String key, int value) {
     public void putInteger(String key, int value) {
-        put(key, Integer.valueOf(value));
+        put(key, value);
     }
     }
 
 
     /**
     /**
@@ -583,7 +640,7 @@ public final class AppSettings extends HashMap<String, Object> {
      * @param value the desired boolean value
      * @param value the desired boolean value
      */
      */
     public void putBoolean(String key, boolean value) {
     public void putBoolean(String key, boolean value) {
-        put(key, Boolean.valueOf(value));
+        put(key, value);
     }
     }
 
 
     /**
     /**
@@ -603,7 +660,7 @@ public final class AppSettings extends HashMap<String, Object> {
      * @param value the desired float value
      * @param value the desired float value
      */
      */
     public void putFloat(String key, float value) {
     public void putFloat(String key, float value) {
-        put(key, Float.valueOf(value));
+        put(key, value);
     }
     }
 
 
     /**
     /**
@@ -698,9 +755,9 @@ public final class AppSettings extends HashMap<String, Object> {
     /**
     /**
      * Set the graphics renderer to use, one of:<br>
      * Set the graphics renderer to use, one of:<br>
      * <ul>
      * <ul>
-     * <li>AppSettings.LWJGL_OPENGL1 - Force OpenGL1.1 compatability</li>
-     * <li>AppSettings.LWJGL_OPENGL2 - Force OpenGL2 compatability</li>
-     * <li>AppSettings.LWJGL_OPENGL3 - Force OpenGL3.3 compatability</li>
+     * <li>AppSettings.LWJGL_OPENGL1 - Force OpenGL1.1 compatibility</li>
+     * <li>AppSettings.LWJGL_OPENGL2 - Force OpenGL2 compatibility</li>
+     * <li>AppSettings.LWJGL_OPENGL3 - Force OpenGL3.3 compatibility</li>
      * <li>AppSettings.LWJGL_OPENGL_ANY - Choose an appropriate
      * <li>AppSettings.LWJGL_OPENGL_ANY - Choose an appropriate
      * OpenGL version based on system capabilities</li>
      * OpenGL version based on system capabilities</li>
      * <li>AppSettings.JOGL_OPENGL_BACKWARD_COMPATIBLE</li>
      * <li>AppSettings.JOGL_OPENGL_BACKWARD_COMPATIBLE</li>
@@ -739,7 +796,7 @@ public final class AppSettings extends HashMap<String, Object> {
     }
     }
 
 
     /**
     /**
-     * @param value the width for the default framebuffer.
+     * @param value the width for the default frame buffer.
      * (Default: 640)
      * (Default: 640)
      */
      */
     public void setWidth(int value) {
     public void setWidth(int value) {
@@ -747,7 +804,7 @@ public final class AppSettings extends HashMap<String, Object> {
     }
     }
 
 
     /**
     /**
-     * @param value the height for the default framebuffer.
+     * @param value the height for the default frame buffer.
      * (Default: 480)
      * (Default: 480)
      */
      */
     public void setHeight(int value) {
     public void setHeight(int value) {
@@ -755,7 +812,7 @@ public final class AppSettings extends HashMap<String, Object> {
     }
     }
 
 
     /**
     /**
-     * Set the resolution for the default framebuffer
+     * Set the resolution for the default frame buffer
      * Use {@link #setWindowSize(int, int)} instead, for HiDPI display support.
      * Use {@link #setWindowSize(int, int)} instead, for HiDPI display support.
      * @param width The width
      * @param width The width
      * @param height The height
      * @param height The height
@@ -769,8 +826,8 @@ public final class AppSettings extends HashMap<String, Object> {
     /**
     /**
      * Set the size of the window
      * Set the size of the window
      *
      *
-     * @param width The width in pixels (default = width of the default framebuffer)
-     * @param height The height in pixels (default = height of the default framebuffer)
+     * @param width The width in pixels (default = width of the default frame buffer)
+     * @param height The height in pixels (default = height of the default frame buffer)
      */
      */
     public void setWindowSize(int width, int height) {
     public void setWindowSize(int width, int height) {
         putInteger("WindowWidth", width);
         putInteger("WindowWidth", width);
@@ -960,7 +1017,7 @@ public final class AppSettings extends HashMap<String, Object> {
     /**
     /**
      * Enable or disable gamma correction. If enabled, the main framebuffer will
      * Enable or disable gamma correction. If enabled, the main framebuffer will
      * be configured for sRGB colors, and sRGB images will be linearized.
      * be configured for sRGB colors, and sRGB images will be linearized.
-     *
+     * <p>
      * Gamma correction requires a GPU that supports GL_ARB_framebuffer_sRGB;
      * Gamma correction requires a GPU that supports GL_ARB_framebuffer_sRGB;
      * otherwise this setting will be ignored.
      * otherwise this setting will be ignored.
      *
      *
@@ -971,7 +1028,7 @@ public final class AppSettings extends HashMap<String, Object> {
     }
     }
 
 
     /**
     /**
-     * Get the framerate.
+     * Get the frame rate.
      *
      *
      * @return the maximum rate (in frames per second), or -1 for unlimited
      * @return the maximum rate (in frames per second), or -1 for unlimited
      * @see #setFrameRate(int)
      * @see #setFrameRate(int)
@@ -1004,7 +1061,7 @@ public final class AppSettings extends HashMap<String, Object> {
     /**
     /**
      * Get the width
      * Get the width
      *
      *
-     * @return the width of the default framebuffer (in pixels)
+     * @return the width of the default frame buffer (in pixels)
      * @see #setWidth(int)
      * @see #setWidth(int)
      */
      */
     public int getWidth() {
     public int getWidth() {
@@ -1014,7 +1071,7 @@ public final class AppSettings extends HashMap<String, Object> {
     /**
     /**
      * Get the height
      * Get the height
      *
      *
-     * @return the height of the default framebuffer (in pixels)
+     * @return the height of the default frame buffer (in pixels)
      * @see #setHeight(int)
      * @see #setHeight(int)
      */
      */
     public int getHeight() {
     public int getHeight() {
@@ -1215,7 +1272,7 @@ public final class AppSettings extends HashMap<String, Object> {
 
 
     /**
     /**
      * Allows the display window to be resized by dragging its edges.
      * Allows the display window to be resized by dragging its edges.
-     *
+     * <p>
      * Only supported for {@link JmeContext.Type#Display} contexts which
      * Only supported for {@link JmeContext.Type#Display} contexts which
      * are in windowed mode, ignored for other types.
      * are in windowed mode, ignored for other types.
      * The default value is <code>false</code>.
      * The default value is <code>false</code>.
@@ -1240,7 +1297,7 @@ public final class AppSettings extends HashMap<String, Object> {
 
 
     /**
     /**
      * When enabled the display context will swap buffers every frame.
      * When enabled the display context will swap buffers every frame.
-     *
+     * <p>
      * This may need to be disabled when integrating with an external
      * This may need to be disabled when integrating with an external
      * library that handles buffer swapping on its own, e.g. Oculus Rift.
      * library that handles buffer swapping on its own, e.g. Oculus Rift.
      * When disabled, the engine will process window messages
      * When disabled, the engine will process window messages
@@ -1282,7 +1339,7 @@ public final class AppSettings extends HashMap<String, Object> {
     /**
     /**
      * Sets a custom platform chooser. This chooser specifies which platform and
      * Sets a custom platform chooser. This chooser specifies which platform and
      * which devices are used for the OpenCL context.
      * which devices are used for the OpenCL context.
-     *
+     * <p>
      * Default: an implementation defined one.
      * Default: an implementation defined one.
      *
      *
      * @param chooser the class of the chooser, must have a default constructor
      * @param chooser the class of the chooser, must have a default constructor
@@ -1507,4 +1564,52 @@ public final class AppSettings extends HashMap<String, Object> {
     public void setDisplay(int mon) {
     public void setDisplay(int mon) {
         putInteger("Display", mon);
         putInteger("Display", mon);
     }
     }
+
+    /**
+     * Prints all key-value pairs stored under a given preferences key
+     * in the Java Preferences API to standard output.
+     *
+     * @param preferencesKey The preferences key (node path) to inspect.
+     * @throws BackingStoreException If an exception occurs while accessing the preferences.
+     */
+    public static void printPreferences(String preferencesKey) throws BackingStoreException {
+        Preferences prefs = Preferences.userRoot().node(preferencesKey);
+        String[] keys = prefs.keys();
+
+        if (keys == null || keys.length == 0) {
+            logger.log(Level.WARNING, "No Preferences found under key: {0}", preferencesKey);
+        } else {
+            StringBuilder sb = new StringBuilder();
+            sb.append("Preferences for key: ").append(preferencesKey);
+            for (String key : keys) {
+                // Retrieve the value as a String (default fallback for Preferences API)
+                String value = prefs.get(key, "[Value Not Found]");
+                sb.append("\n * ").append(key).append(" = ").append(value);
+            }
+            logger.log(Level.INFO, sb.toString());
+        }
+    }
+    /**
+     * Sets the preferred native platform for creating the GL context on Linux distributions.
+     * <p>
+     * This setting is relevant for Linux distributions or derivatives that utilize a Wayland session alongside an X11 via the XWayland bridge.
+     * Enabling this option allows the use of GLX for window positioning and/or icon configuration.
+     *
+     * @param preferred true to prefer GLX (native X11) for the GL context, false to prefer EGL (native Wayland).
+     */
+    public void setX11PlatformPreferred(boolean preferred) {
+        putBoolean("X11PlatformPreferred", preferred);
+    }
+    
+    /**
+     * Determines which native platform is preferred for GL context creation on Linux distributions.
+     * <p>
+     * This setting is only valid on Linux distributions or derivatives that support Wayland,
+     * and it indicates whether GLX (native X11) or EGL (native Wayland) is enabled for the GL context.
+     *
+     * @return true if GLX is preferred, otherwise false if EGL is preferred (native Wayland).
+     */
+    public boolean isX11PlatformPreferred() {
+        return getBoolean("X11PlatformPreferred");
+    }
 }
 }

+ 46 - 45
jme3-core/src/plugins/java/com/jme3/export/binary/BinaryExporter.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -38,7 +38,13 @@ import com.jme3.export.OutputCapsule;
 import com.jme3.export.Savable;
 import com.jme3.export.Savable;
 import com.jme3.export.SavableClassUtil;
 import com.jme3.export.SavableClassUtil;
 import com.jme3.math.FastMath;
 import com.jme3.math.FastMath;
-import java.io.*;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.IdentityHashMap;
@@ -116,39 +122,40 @@ import java.util.logging.Logger;
  *
  *
  * @author Joshua Slack
  * @author Joshua Slack
  */
  */
-
 public class BinaryExporter implements JmeExporter {
 public class BinaryExporter implements JmeExporter {
-    private static final Logger logger = Logger.getLogger(BinaryExporter.class
-            .getName());
+
+    private static final Logger logger = Logger.getLogger(BinaryExporter.class.getName());
 
 
     protected int aliasCount = 1;
     protected int aliasCount = 1;
     protected int idCount = 1;
     protected int idCount = 1;
 
 
-    private final IdentityHashMap<Savable, BinaryIdContentPair> contentTable
-             = new IdentityHashMap<>();
-
-    protected HashMap<Integer, Integer> locationTable
-             = new HashMap<>();
+    private final IdentityHashMap<Savable, BinaryIdContentPair> contentTable = new IdentityHashMap<>();
+    protected HashMap<Integer, Integer> locationTable = new HashMap<>();
 
 
     // key - class name, value = bco
     // key - class name, value = bco
-    private final HashMap<String, BinaryClassObject> classes
-             = new HashMap<>();
-
+    private final HashMap<String, BinaryClassObject> classes = new HashMap<>();
     private final ArrayList<Savable> contentKeys = new ArrayList<>();
     private final ArrayList<Savable> contentKeys = new ArrayList<>();
 
 
     public static boolean debug = false;
     public static boolean debug = false;
     public static boolean useFastBufs = true;
     public static boolean useFastBufs = true;
 
 
+    /**
+     * Constructs a new {@code BinaryExporter}.
+     */
     public BinaryExporter() {
     public BinaryExporter() {
     }
     }
 
 
+    /**
+     * Returns a new instance of {@code BinaryExporter}.
+     *
+     * @return A new {@code BinaryExporter} instance.
+     */
     public static BinaryExporter getInstance() {
     public static BinaryExporter getInstance() {
         return new BinaryExporter();
         return new BinaryExporter();
     }
     }
 
 
     /**
     /**
      * Saves the object into memory then loads it from memory.
      * Saves the object into memory then loads it from memory.
-     *
      * Used by tests to check if the persistence system is working.
      * Used by tests to check if the persistence system is working.
      *
      *
      * @param <T> The type of savable.
      * @param <T> The type of savable.
@@ -163,9 +170,11 @@ public class BinaryExporter implements JmeExporter {
         try {
         try {
             BinaryExporter exporter = new BinaryExporter();
             BinaryExporter exporter = new BinaryExporter();
             exporter.save(object, baos);
             exporter.save(object, baos);
+
             BinaryImporter importer = new BinaryImporter();
             BinaryImporter importer = new BinaryImporter();
             importer.setAssetManager(assetManager);
             importer.setAssetManager(assetManager);
             return (T) importer.load(baos.toByteArray());
             return (T) importer.load(baos.toByteArray());
+
         } catch (IOException ex) {
         } catch (IOException ex) {
             // Should never happen.
             // Should never happen.
             throw new AssertionError(ex);
             throw new AssertionError(ex);
@@ -191,28 +200,25 @@ public class BinaryExporter implements JmeExporter {
         // write out tag table
         // write out tag table
         int classTableSize = 0;
         int classTableSize = 0;
         int classNum = classes.keySet().size();
         int classNum = classes.keySet().size();
-        int aliasSize = ((int) FastMath.log(classNum, 256) + 1); // make all
-                                                                  // aliases a
-                                                                  // fixed width
+        int aliasSize = ((int) FastMath.log(classNum, 256) + 1); // make all aliases a fixed width
 
 
         os.write(ByteUtils.convertToBytes(classNum)); // 3. "number of classes"
         os.write(ByteUtils.convertToBytes(classNum)); // 3. "number of classes"
         for (String key : classes.keySet()) {
         for (String key : classes.keySet()) {
             BinaryClassObject bco = classes.get(key);
             BinaryClassObject bco = classes.get(key);
 
 
             // write alias
             // write alias
-            byte[] aliasBytes = fixClassAlias(bco.alias,
-                    aliasSize);
+            byte[] aliasBytes = fixClassAlias(bco.alias, aliasSize);
             os.write(aliasBytes);                     // 4. "class alias"
             os.write(aliasBytes);                     // 4. "class alias"
             classTableSize += aliasSize;
             classTableSize += aliasSize;
 
 
             // jME3 NEW: Write class hierarchy version numbers
             // jME3 NEW: Write class hierarchy version numbers
-            os.write( bco.classHierarchyVersions.length );
-            for (int version : bco.classHierarchyVersions){
+            os.write(bco.classHierarchyVersions.length);
+            for (int version : bco.classHierarchyVersions) {
                 os.write(ByteUtils.convertToBytes(version));
                 os.write(ByteUtils.convertToBytes(version));
             }
             }
             classTableSize += 1 + bco.classHierarchyVersions.length * 4;
             classTableSize += 1 + bco.classHierarchyVersions.length * 4;
 
 
-            // write classname size & classname
+            // write class name size & class name
             byte[] classBytes = key.getBytes();
             byte[] classBytes = key.getBytes();
             os.write(ByteUtils.convertToBytes(classBytes.length)); // 5. "full class-name size"
             os.write(ByteUtils.convertToBytes(classBytes.length)); // 5. "full class-name size"
             os.write(classBytes);                                  // 6. "full class name"
             os.write(classBytes);                                  // 6. "full class name"
@@ -236,14 +242,12 @@ public class BinaryExporter implements JmeExporter {
         // write out data to a separate stream
         // write out data to a separate stream
         int location = 0;
         int location = 0;
         // keep track of location for each piece
         // keep track of location for each piece
-        HashMap<String, ArrayList<BinaryIdContentPair>> alreadySaved = new HashMap<>(
-                contentTable.size());
+        HashMap<String, ArrayList<BinaryIdContentPair>> alreadySaved = new HashMap<>(contentTable.size());
         for (Savable savable : contentKeys) {
         for (Savable savable : contentKeys) {
             // look back at previous written data for matches
             // look back at previous written data for matches
             String savableName = savable.getClass().getName();
             String savableName = savable.getClass().getName();
             BinaryIdContentPair pair = contentTable.get(savable);
             BinaryIdContentPair pair = contentTable.get(savable);
-            ArrayList<BinaryIdContentPair> bucket = alreadySaved
-                    .get(savableName + getChunk(pair));
+            ArrayList<BinaryIdContentPair> bucket = alreadySaved.get(savableName + getChunk(pair));
             int prevLoc = findPrevMatch(pair, bucket);
             int prevLoc = findPrevMatch(pair, bucket);
             if (prevLoc != -1) {
             if (prevLoc != -1) {
                 locationTable.put(pair.getId(), prevLoc);
                 locationTable.put(pair.getId(), prevLoc);
@@ -286,17 +290,14 @@ public class BinaryExporter implements JmeExporter {
         // append stream to the output stream
         // append stream to the output stream
         out.writeTo(os);
         out.writeTo(os);
 
 
-
-        out = null;
-        os = null;
-
         if (debug) {
         if (debug) {
-            logger.fine("Stats:");
-            logger.log(Level.FINE, "classes: {0}", classNum);
-            logger.log(Level.FINE, "class table: {0} bytes", classTableSize);
-            logger.log(Level.FINE, "objects: {0}", numLocations);
-            logger.log(Level.FINE, "location table: {0} bytes", locationTableSize);
-            logger.log(Level.FINE, "data: {0} bytes", location);
+            logger.log(Level.INFO, "BinaryExporter Stats:"
+                    + "\n * Classes: {0}"
+                    + "\n * Class Table: {1} bytes"
+                    + "\n * Objects: {2}"
+                    + "\n * Location Table: {3} bytes"
+                    + "\n * Data: {4} bytes",
+                    new Object[] {classNum, classTableSize, numLocations, locationTableSize, location});
         }
         }
     }
     }
 
 
@@ -305,14 +306,15 @@ public class BinaryExporter implements JmeExporter {
                 .getContent().bytes.length));
                 .getContent().bytes.length));
     }
     }
 
 
-    private int findPrevMatch(BinaryIdContentPair oldPair,
-            ArrayList<BinaryIdContentPair> bucket) {
-        if (bucket == null)
+    private int findPrevMatch(BinaryIdContentPair oldPair, ArrayList<BinaryIdContentPair> bucket) {
+        if (bucket == null) {
             return -1;
             return -1;
+        }
         for (int x = bucket.size(); --x >= 0;) {
         for (int x = bucket.size(); --x >= 0;) {
             BinaryIdContentPair pair = bucket.get(x);
             BinaryIdContentPair pair = bucket.get(x);
-            if (pair.getContent().equals(oldPair.getContent()))
+            if (pair.getContent().equals(oldPair.getContent())) {
                 return locationTable.get(pair.getId());
                 return locationTable.get(pair.getId());
+            }
         }
         }
         return -1;
         return -1;
     }
     }
@@ -345,7 +347,7 @@ public class BinaryExporter implements JmeExporter {
         return contentTable.get(object).getContent();
         return contentTable.get(object).getContent();
     }
     }
 
 
-    private BinaryClassObject createClassObject(Class<? extends Savable> clazz) throws IOException{
+    private BinaryClassObject createClassObject(Class<? extends Savable> clazz) throws IOException {
         BinaryClassObject bco = new BinaryClassObject();
         BinaryClassObject bco = new BinaryClassObject();
         bco.alias = generateTag();
         bco.alias = generateTag();
         bco.nameFields = new HashMap<>();
         bco.nameFields = new HashMap<>();
@@ -361,10 +363,10 @@ public class BinaryExporter implements JmeExporter {
             return -1;
             return -1;
         }
         }
         Class<? extends Savable> clazz = object.getClass();
         Class<? extends Savable> clazz = object.getClass();
-        BinaryClassObject bco = classes.get(object.getClass().getName());
+        BinaryClassObject bco = classes.get(clazz.getName());
         // is this class been looked at before? in tagTable?
         // is this class been looked at before? in tagTable?
         if (bco == null) {
         if (bco == null) {
-            bco = createClassObject(object.getClass());
+            bco = createClassObject(clazz);
         }
         }
 
 
         // is object in contentTable?
         // is object in contentTable?
@@ -379,7 +381,6 @@ public class BinaryExporter implements JmeExporter {
         object.write(this);
         object.write(this);
         newPair.getContent().finish();
         newPair.getContent().finish();
         return newPair.getId();
         return newPair.getId();
-
     }
     }
 
 
     protected byte[] generateTag() {
     protected byte[] generateTag() {
@@ -401,4 +402,4 @@ public class BinaryExporter implements JmeExporter {
                 new BinaryOutputCapsule(this, bco));
                 new BinaryOutputCapsule(this, bco));
         return pair;
         return pair;
     }
     }
-}
+}

+ 18 - 2
jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java

@@ -275,12 +275,20 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
         );
         );
 
 
         if (glfwPlatformSupported(GLFW_PLATFORM_WAYLAND)) {
         if (glfwPlatformSupported(GLFW_PLATFORM_WAYLAND)) {
-            
+
+            /*
+             * Change the platform GLFW uses to enable GLX on Wayland as long as you 
+             * have XWayland (X11 compatibility)
+             */
+            if (settings.isX11PlatformPreferred() && glfwPlatformSupported(GLFW_PLATFORM_X11)) {
+                glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
+            }
+
             // Disables the libdecor bar when creating a fullscreen context
             // Disables the libdecor bar when creating a fullscreen context
             // https://www.glfw.org/docs/latest/intro_guide.html#init_hints_wayland
             // https://www.glfw.org/docs/latest/intro_guide.html#init_hints_wayland
             glfwInitHint(GLFW_WAYLAND_LIBDECOR, settings.isFullscreen() ? GLFW_WAYLAND_DISABLE_LIBDECOR : GLFW_WAYLAND_PREFER_LIBDECOR);
             glfwInitHint(GLFW_WAYLAND_LIBDECOR, settings.isFullscreen() ? GLFW_WAYLAND_DISABLE_LIBDECOR : GLFW_WAYLAND_PREFER_LIBDECOR);
         }
         }
-        
+
         if (!glfwInit()) {
         if (!glfwInit()) {
             throw new IllegalStateException("Unable to initialize GLFW");
             throw new IllegalStateException("Unable to initialize GLFW");
         }
         }
@@ -404,6 +412,14 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
         );
         );
 
 
         int platformId = glfwGetPlatform();
         int platformId = glfwGetPlatform();
+        if (settings.isX11PlatformPreferred()) {
+            if (platformId == GLFW_PLATFORM_X11) {
+                LOGGER.log(Level.INFO, "Active X11 server for GLX management:\n * Platform: GLFW_PLATFORM_X11|XWayland ({0})", platformId);
+            } else {
+                LOGGER.log(Level.WARNING, "Can't change platform to X11 (GLX), check if you have XWayland enabled");
+            }
+        }
+        
         if (platformId != GLFW_PLATFORM_WAYLAND && !settings.isFullscreen()) {
         if (platformId != GLFW_PLATFORM_WAYLAND && !settings.isFullscreen()) {
             /*
             /*
              * in case the window positioning hints above were ignored, but not
              * in case the window positioning hints above were ignored, but not

+ 1 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java

@@ -209,6 +209,7 @@ public class LightsPunctualExtensionLoader implements ExtensionLoader {
             Light light = lightDefinitions.get(lightIndex);
             Light light = lightDefinitions.get(lightIndex);
             parent.addLight(light);
             parent.addLight(light);
             LightControl control = new LightControl(light);
             LightControl control = new LightControl(light);
+            control.setInvertAxisDirection(true);
             node.addControl(control);
             node.addControl(control);
         } else {
         } else {
             throw new AssetLoadException("KHR_lights_punctual extension accessed undefined light at index " + lightIndex);
             throw new AssetLoadException("KHR_lights_punctual extension accessed undefined light at index " + lightIndex);

+ 9 - 0
jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/App.java

@@ -36,6 +36,8 @@ import com.jme3.app.state.AppState;
 import com.jme3.app.state.VideoRecorderAppState;
 import com.jme3.app.state.VideoRecorderAppState;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.ColorRGBA;
 
 
+import java.util.function.Consumer;
+
 /**
 /**
  * The app used for the tests. AppState(s) are used to inject the actual test code.
  * The app used for the tests. AppState(s) are used to inject the actual test code.
  * @author Richard Tingle (aka richtea)
  * @author Richard Tingle (aka richtea)
@@ -46,10 +48,17 @@ public class App extends SimpleApplication {
         super(initialStates);
         super(initialStates);
     }
     }
 
 
+    Consumer<Throwable> onError = (onError) -> {};
+
     @Override
     @Override
     public void simpleInitApp(){
     public void simpleInitApp(){
         getViewPort().setBackgroundColor(ColorRGBA.Black);
         getViewPort().setBackgroundColor(ColorRGBA.Black);
         setTimer(new VideoRecorderAppState.IsoTimer(60));
         setTimer(new VideoRecorderAppState.IsoTimer(60));
     }
     }
 
 
+    @Override
+    public void handleError(String errMsg, Throwable t) {
+        super.handleError(errMsg, t);
+        onError.accept(t);
+    }
 }
 }

+ 10 - 4
jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ExtentReportExtension.java

@@ -50,7 +50,7 @@ import java.util.Optional;
  */
  */
 public class ExtentReportExtension implements BeforeAllCallback, AfterAllCallback, TestWatcher, BeforeTestExecutionCallback{
 public class ExtentReportExtension implements BeforeAllCallback, AfterAllCallback, TestWatcher, BeforeTestExecutionCallback{
     private static ExtentReports extent;
     private static ExtentReports extent;
-    private static final ThreadLocal<ExtentTest> test = new ThreadLocal<>();
+    private static ExtentTest currentTest;
 
 
     @Override
     @Override
     public void beforeAll(ExtensionContext context) {
     public void beforeAll(ExtensionContext context) {
@@ -62,6 +62,8 @@ public class ExtentReportExtension implements BeforeAllCallback, AfterAllCallbac
             extent = new ExtentReports();
             extent = new ExtentReports();
             extent.attachReporter(spark);
             extent.attachReporter(spark);
         }
         }
+        // Initialize log capture to redirect console output to the report
+        ExtentReportLogCapture.initialize();
     }
     }
 
 
     @Override
     @Override
@@ -71,6 +73,9 @@ public class ExtentReportExtension implements BeforeAllCallback, AfterAllCallbac
         * anywhere else I can hook into the lifecycle of the end of all tests to write the report.
         * anywhere else I can hook into the lifecycle of the end of all tests to write the report.
         */
         */
         extent.flush();
         extent.flush();
+
+        // Restore the original System.out
+        ExtentReportLogCapture.restore();
     }
     }
 
 
     @Override
     @Override
@@ -96,10 +101,11 @@ public class ExtentReportExtension implements BeforeAllCallback, AfterAllCallbac
     @Override
     @Override
     public void beforeTestExecution(ExtensionContext context) {
     public void beforeTestExecution(ExtensionContext context) {
         String testName = context.getDisplayName();
         String testName = context.getDisplayName();
-        test.set(extent.createTest(testName));
+        String className = context.getRequiredTestClass().getSimpleName();
+        currentTest = extent.createTest(className + "." + testName);
     }
     }
 
 
     public static ExtentTest getCurrentTest() {
     public static ExtentTest getCurrentTest() {
-        return test.get();
+        return currentTest;
     }
     }
-}
+}

+ 117 - 0
jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ExtentReportLogCapture.java

@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2025 jMonkeyEngine
+ * 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 'jMonkeyEngine' 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.jmonkeyengine.screenshottests.testframework;
+
+import com.aventstack.extentreports.ExtentTest;
+
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+/**
+ * This class captures console logs and adds them to the ExtentReport.
+ * It redirects System.out to both the original console and the ExtentReport.
+ *
+ * @author Richard Tingle (aka richtea)
+ */
+public class ExtentReportLogCapture {
+
+    private static final PrintStream originalOut = System.out;
+    private static final PrintStream originalErr = System.err;
+    private static boolean initialized = false;
+
+    /**
+     * Initializes the log capture system. This should be called once at the start of the test suite.
+     */
+    public static void initialize() {
+        if (!initialized) {
+            // Redirect System.out and System.err
+            System.setOut(new ExtentReportPrintStream(originalOut));
+            System.setErr(new ExtentReportPrintStream(originalErr));
+
+            initialized = true;
+        }
+    }
+
+    /**
+     * Restores the original System.out. This should be called at the end of the test suite.
+     */
+    public static void restore() {
+        if(initialized) {
+            // Restore System.out and System.err
+            System.setOut(originalOut);
+            System.setErr(originalErr);
+            initialized = false;
+        }
+    }
+
+    /**
+     * A custom PrintStream that redirects output to both the original console and the ExtentReport.
+     */
+    private static class ExtentReportPrintStream extends PrintStream {
+        private StringBuilder buffer = new StringBuilder();
+
+        public ExtentReportPrintStream(OutputStream out) {
+            super(out, true);
+        }
+
+        @Override
+        public void write(byte[] buf, int off, int len) {
+            super.write(buf, off, len);
+
+            // Convert the byte array to a string and add to buffer
+            String s = new String(buf, off, len);
+            buffer.append(s);
+
+            // If we have a complete line (ends with newline), process it
+            if (s.endsWith("\n") || s.endsWith("\r\n")) {
+                String line = buffer.toString().trim();
+                if (!line.isEmpty()) {
+                    addToExtentReport(line);
+                }
+                buffer.setLength(0); // Clear the buffer
+            }
+        }
+
+        private void addToExtentReport(String s) {
+            try {
+                ExtentTest currentTest = ExtentReportExtension.getCurrentTest();
+                if (currentTest != null) {
+                    currentTest.info(s);
+                }
+            } catch (Exception e) {
+                // If there's an error adding to the report, just continue
+                // This ensures that console logs are still displayed even if there's an issue with the report
+            }
+        }
+    }
+
+}

+ 29 - 13
jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/TestDriver.java

@@ -57,8 +57,12 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Comparator;
 import java.util.List;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import java.util.stream.Stream;
 import java.util.stream.Stream;
 
 
 import static org.junit.jupiter.api.Assertions.fail;
 import static org.junit.jupiter.api.Assertions.fail;
@@ -72,6 +76,8 @@ import static org.junit.jupiter.api.Assertions.fail;
  */
  */
 public class TestDriver extends BaseAppState{
 public class TestDriver extends BaseAppState{
 
 
+    private static final Logger logger = Logger.getLogger(TestDriver.class.getName());
+
     public static final String IMAGES_ARE_DIFFERENT = "Images are different. (If you are running the test locally this is expected, images only reproducible on github CI infrastructure)";
     public static final String IMAGES_ARE_DIFFERENT = "Images are different. (If you are running the test locally this is expected, images only reproducible on github CI infrastructure)";
 
 
     public static final String IMAGES_ARE_DIFFERENT_SIZES = "Images are different sizes.";
     public static final String IMAGES_ARE_DIFFERENT_SIZES = "Images are different sizes.";
@@ -94,7 +100,7 @@ public class TestDriver extends BaseAppState{
 
 
     ScreenshotNoInputAppState screenshotAppState;
     ScreenshotNoInputAppState screenshotAppState;
 
 
-    private final Object waitLock = new Object();
+    private CountDownLatch waitLatch;
 
 
     private final int tickToTerminateApp;
     private final int tickToTerminateApp;
 
 
@@ -113,15 +119,19 @@ public class TestDriver extends BaseAppState{
         }
         }
         if(tick >= tickToTerminateApp){
         if(tick >= tickToTerminateApp){
             getApplication().stop(true);
             getApplication().stop(true);
-            synchronized (waitLock) {
-                waitLock.notify(); // Release the wait
-            }
+            waitLatch.countDown();
         }
         }
 
 
         tick++;
         tick++;
     }
     }
 
 
-    @Override protected void initialize(Application app){}
+    @Override protected void initialize(Application app){
+        ((App)app).onError = error -> {
+            logger.log(Level.WARNING, "Error in test application", error);
+            waitLatch.countDown();
+        };
+
+    }
 
 
     @Override protected void cleanup(Application app){}
     @Override protected void cleanup(Application app){}
 
 
@@ -129,7 +139,6 @@ public class TestDriver extends BaseAppState{
 
 
     @Override protected void onDisable(){}
     @Override protected void onDisable(){}
 
 
-
     /**
     /**
      * Boots up the application on a separate thread (blocks this thread) and then does the following:
      * Boots up the application on a separate thread (blocks this thread) and then does the following:
      * - Takes screenshots on the requested frames
      * - Takes screenshots on the requested frames
@@ -161,16 +170,23 @@ public class TestDriver extends BaseAppState{
         app.setSettings(appSettings);
         app.setSettings(appSettings);
         app.setShowSettings(false);
         app.setShowSettings(false);
 
 
+        testDriver.waitLatch = new CountDownLatch(1);
         executor.execute(() -> app.start(JmeContext.Type.Display));
         executor.execute(() -> app.start(JmeContext.Type.Display));
 
 
-        synchronized (testDriver.waitLock) {
-            try {
-                testDriver.waitLock.wait(10000); // Wait for the screenshot to be taken and application to stop
-                Thread.sleep(200); //give time for openGL is fully released before starting a new test (get random JVM crashes without this)
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                throw new RuntimeException(e);
+        int maxWaitTimeMilliseconds = 45000;
+
+        try {
+            boolean exitedProperly = testDriver.waitLatch.await(maxWaitTimeMilliseconds, TimeUnit.MILLISECONDS);
+
+            if(!exitedProperly){
+                logger.warning("Test driver did not exit in " + maxWaitTimeMilliseconds + "ms. Timed out");
+                app.stop(true);
             }
             }
+
+            Thread.sleep(1000); //give time for openGL is fully released before starting a new test (get random JVM crashes without this)
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException(e);
         }
         }
 
 
         //search the imageTempDir
         //search the imageTempDir

BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.effects.TestIssue1773.testIssue1773_localSpace_f45.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.effects.TestIssue1773.testIssue1773_worldSpace_f45.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_DefaultDirectionalLight_f12.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_HighRoughness_f12.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_LowRoughness_f12.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_UpdatedDirectionalLight_f12.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithRealtimeBaking_f10.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithoutRealtimeBaking_f10.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromAbove_f1.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromFront_f1.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromRight_f1.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.post.TestFog.testFog_f1.png