The first beta1 release for 3.10 is now available

project.ext {
  jmeVer = '3.10.0-beta1' 
}
dependencies {
    implementation "org.jmonkeyengine:jme3-core:$jmeVer"
    implementation "org.jmonkeyengine:jme3-desktop:$jmeVer"
    ...
}



This version includes a few final features for 3.10 as well as a handful of fixes that resolve some of the issues that were found with all of the new rendering changes in the recent alpha5 release. From this point on, all new features or major changes will be targeted for 3.11 and the remainder of 3.10’s beta phase will be focused on testing and resolving any further issues that are reported.

The full changelog is available here: Release jMonkeyEngine 3.10.0-beta1 · jMonkeyEngine/jmonkeyengine

10 Likes

The change from GLFW to SDL3 (in PR 2585) broke some of my projects, starting with the Heart library. I’m working to figure out what changes I need to make to Heart.

2 Likes

The removal of GLFW unfortunately will cause a lot of jME apps to encounter breakages. I had not used GLFW for much else aside from setting up a borderless full screen mode (which is now built in as an option in the engine’s appSettings with SDL as of a recent PR) however if you used GLFW more extensively then you likely will have more breakages that will require learning the ins and outs of SDL to fix.

So it would be beneficial if any jME users can share more information on the changes required to make previous GLFW features work with SDL now, that way we can make the migration path as smooth as possible for the community.

The main reason GLFW was removed without a deprecation phase is because it isn’t compatible with the new Angle renderer, which meant that adding Angle in 3.10 required GLFW to be removed.

So in hindsight it may have been a better strategy to first deprecate GLFW with the addition of SDL3 in 3.10, and then target the next 3.11 release for the new angle back end and remove GLFW in the next release rather than 3.10. That would have given jme devs a full release cycle to migrate from GLFW to SDL.

So 3.10 has turned out to be a bigger release with more breakages than I initially planned. But SDL is definitely an objectively better library than GLFW, so I do think there is some benefit to the quick removal that forces a full migration to SDL sooner than later.

At least in the case of the new angle backend, there is still the option to use opengl for anyone who isn’t ready to deal with the breakages that come along with switching to angle

And in the case of desktop development, I am finding that openGl does tend to be the better choice still for the majority of desktop devices (with some few exceptions), while angle is the objectively better choice for apps targeting mobile devices. I’ve noticed that my game has a significantly lower framerate when using angle on my desktop device, so I plan to make my game default to using opengl and will add an option that allows the player to toggle to angle for edge cases where their device my not handle opengl as well. And if I were making an app that highly prioritzed targeting multiple platforms, then I’d instead choose angle as the default.

4 Likes

My work for 3.10 so far has mostly been geared toward making jME more reliable across all the platforms we support, safer and nicer to work with. This includes:

  • Complete and reliable GLES 3.0 support everywhere, with updated jME internals that can make full use of it
  • Modern GraalVM-based iOS backend that does not depend on deprecated APIs
  • Refactoring every platform backend to make sure they are reliable and up to date: switching to SDL3 for desktop, proper gamepad and remapping support, gamepad rumble, device rumble, improvements to the sRGB pipeline, HiDPI support, and an on-screen gamepad
  • Improvements and fixes to PBR and faster IBL environment baking
  • Better scene design with render parity with Blender: light attenuation, global lights, and tonemapping. So if both jME and Blender are configured in the same way, the scene should look almost the same.
  • GraalVM support, mostly for the iOS backend, but also for those who want to release native binaries
  • Replacement of JNI/AWT asset loaders with cross-platform java alternative/portings
  • Native memory allocators with resource management in every platform
  • Optional “Saferallocator” with built-in memory hardening and dynamic memory pressure detection to trigger the garbage collector for native buffers

The defaults are set to maximize cross-platform compatibility, so if you start your project with jME 3.10.0-beta1 today, you will be working with an engine that is much more stable and has less surprising behavior across different platforms.

The fact that the default renderer is ANGLE_GLES3 also means that, no matter what OS or GPU your workstation uses, you will be testing with almost the same renderer that the game will use everywhere else, including Android and iOS.

With the upgrade to jME’s GLES 3.0 support, this is also not as limiting as it would have been before. Remember how many jME features were not supported, or were half-broken, on Android (that is a GLES platform)? Not anymore!

But if you are porting an existing app from a previous jME version, and you only ever tested it on desktop, so with the previous default OpenGL 3.2 renderer, you might run into some hiccups. OpenGL is generally more lax with the standard, and more permissive when dealing with technically invalid resource management or code.

If you just want your app to run again, the first thing to try is setting the app to use OpenGL 3.2, which was the previous default:

settings.setRenderer(AppSettings.LWJGL_OPENGL32);

If this makes your app work, you can choose to leave it like that. Or, depending on your goals, you might consider doing the extra work needed to make it work with ANGLE. If you do not care about Android or iOS, this could still be worth doing for better macOS support, since OpenGL is deprecated there, or just to make sure the app doesn’t break with different combinations of drivers and hardware.

If anyone has troubles updating their apps due to these changes, please post here on the forum and @ping me, or contact me via dm or discord, I’ll help out.

7 Likes

Regarding the GLFW → SDL3 change: unless you were using GLFW manually in your app, you should not notice any change, other than gamepads being remapped properly and a few corner cases being resolved where GLFW was misbehaving.

Many of my apps use GLFW to determine the available resolutions for full-screen display. I’m sure SDL3 has equivalent functionality; I just need to read the C++ documentation and then figure out how to access it via LWJGL.

jme application has these two methods:


    /**
     * This call will return a list of Monitors that glfwGetMonitors()
     * returns and information about the monitor, like width, height,
     * and refresh rate.
     *
     * @return returns a list of monitors and their information.
     */
    public Displays getDisplays() {
        return context.getDisplays();
    }

    /**
     * Use this to get the positional number of the primary
     * monitor from the glfwGetMonitors() function call.
     *
     * @return the position of the value in the arraylist of
     *         the primary monitor.
     */
    public int getPrimaryDisplay() {
        return context.getPrimaryDisplay();
    }

that i have (hopefully correctly) implemented in SDL3.
These are useful if you want in-game settings where you can change the resolution and then the app is restarted (you can use jme’s restart() for this).

Doing it without the app being first initialized doesn’t make a lot of sense in SDL3, because (afaik) it needs a living SDL3 context anyway. But SDL3 is less sensitive than our previous GLFW implementation, regarding fullscreen resolutions, if you ask it for a wrong one it should select the closest one automatically. So in theory there shouldn’t be a state where the app just crashes because of the resolution.

1 Like

I have been using awt to get the resoltions and it has worked so far in both game projects with lwjgl3 for desktop. I am curious now with how jme application and SDL3 do it :slight_smile:

This is a great move! I actually had to roll my own low-level solution using system calls via JNI on both Linux and Windows, since GLFW didn’t support gamepad rumble out of the box. It’s going to take me some time to refactor my code, as I’m currently calling some GLFW functions directly (to fetch available screen resolutions and for some joystick-related features), but the refactoring time is totally worth it, since SDL3 is a much more mature library and will definitely make working with the engine a lot easier.

2 Likes

This is what I’ve done so far:

    public List<VideoMode> getAvailableVideoModes() {
        initVideo();

        final List<VideoMode> videoModes = new ArrayList<>();
        final PointerBuffer displayModes = SDLVideo.SDL_GetFullscreenDisplayModes(display);

        for (int i = 0; i < displayModes.capacity(); i++) {
            final SDL_DisplayMode displayMode = SDL_DisplayMode.create(displayModes.get());
            final VideoMode videoMode = new VideoMode(displayMode.w(), displayMode.h(), displayMode.refresh_rate());
            videoModes.add(videoMode);
        }

        SDL_free_func.free(displayModes.address());

        return videoModes;
    }

    private void initVideo() {
        if ((SDLInit.SDL_WasInit(SDLInit.SDL_INIT_VIDEO) & SDLInit.SDL_INIT_VIDEO) == 0 && !SDLInit.SDL_Init(SDLInit.SDL_INIT_VIDEO)) {
            String message = SDLError.SDL_GetError();
            logger.log(Level.SEVERE, "CRITICAL ERROR: Failed to initialize SDL3 Video Subsystem!\n * {0}", message);
            throw new RuntimeException(message);
        }
    }

VideoMode is a record I created that holds only width, height and frequency, but SDL_DisplayMode has other data.

public record VideoMode(int width, int height, float refreshRate) {
}
4 Likes

Or LWJGL? I remember I copied this idea from you: OpenKeeper/src/toniarts/openkeeper/utils/DisplayModeUtils.java at a5d563fd06b85668bd2036e71e4b03ae295887f8 · tonihele/OpenKeeper · GitHub . Problem with AWT is that it is not reliable. LWJGL works better. So I suppose now ask from Angle or SDL directly?

Edit: oh yeah, with LWJGL 3 it does ask from GLFW directly. So yeah, SDL then I suppose…

2 Likes

could not load

bug report:

error msg

Could not find org.jmonkeyengine:jme3-vr:3.10.0-beta1.
Searched in the following locations:
- https://repo.maven.apache.org/maven2/org/jmonkeyengine/jme3-vr/3.10.0-beta1/jme3-vr-3.10.0-beta1.pom
- https://jitpack.io/org/jmonkeyengine/jme3-vr/3.10.0-beta1/jme3-vr-3.10.0-beta1.pom
- file:/Users/tliu/Pictures/ImageBackup/ProjectZero/libs/jme3-vr-3.10.0-beta1.jar
- file:/Users/tliu/Pictures/ImageBackup/ProjectZero/libs/jme3-vr.jar
Required by:
project :

the jme3-vr module has been removed from the engine ,so you need to remove that dependency from your project. If you need vr in jme, GitHub - oneMillionWorlds/Tamarin: A VR library for JMonkeyEngine · GitHub is the way nowadays

3 Likes

A post was merged into an existing topic: Testing new iOS backend

package com.jme3.renderer.vulkan.context;

import com.jme3.input.JoyInput;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.TouchInput;
import com.jme3.input.lwjgl.SdlJoystickInput;
import com.jme3.input.lwjgl.SdlKeyInput;
import com.jme3.input.lwjgl.SdlMouseInput;
import com.jme3.renderer.Renderer;
import com.jme3.renderer.vulkan.VKRenderer;
import com.jme3.renderer.vulkan.VulkanRuntime;
import com.jme3.system.AppSettings;
import com.jme3.system.Displays;
import com.jme3.system.JmeContext;
import com.jme3.system.NanoTimer;
import com.jme3.system.SystemListener;
import com.jme3.system.Timer;
import com.jme3.system.lwjgl.Sync;

import org.lwjgl.sdl.SDL_DisplayMode;
import org.lwjgl.sdl.SDL_Event;
import org.lwjgl.system.MemoryStack;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.lwjgl.sdl.SDL.*;
import static org.lwjgl.sdl.SDLError.SDL_GetError;
import static org.lwjgl.sdl.SDLEvents.*;
import static org.lwjgl.sdl.SDLInit.SDL_INIT_EVENTS;
import static org.lwjgl.sdl.SDLInit.SDL_INIT_VIDEO;
import static org.lwjgl.sdl.SDLInit.SDL_InitSubSystem;
import static org.lwjgl.sdl.SDLInit.SDL_QuitSubSystem;
import static org.lwjgl.sdl.SDLVideo.SDL_GetCurrentDisplayMode;
import static org.lwjgl.sdl.SDLVideo.SDL_GetPrimaryDisplay;
import static org.lwjgl.sdl.SDLVideo.SDL_GetWindowID;
import static org.lwjgl.sdl.SDLVideo.SDL_GetWindowPosition;
import static org.lwjgl.sdl.SDLVideo.SDL_GetWindowSizeInPixels;
import static org.lwjgl.sdl.SDLVideo.SDL_SetWindowTitle;
import static org.lwjgl.sdl.SDLVideo.SDL_ShowWindow;
import static org.lwjgl.system.MemoryUtil.NULL;

/**
 * SDL3 + Vulkan 上下文实现 彻底抛弃 LwjglWindow,直接实现 JmeContext 接口。
 */
public class LwjglVulkanContext implements JmeContext, Runnable {

    private static final Logger LOGGER = Logger.getLogger(LwjglVulkanContext.class.getName());

    private final AppSettings settings = new AppSettings(true);
    private final JmeContext.Type type;

    // --- 生命周期状态 ---
    private final AtomicBoolean created = new AtomicBoolean(false);
    private final AtomicBoolean renderable = new AtomicBoolean(false);
    private final AtomicBoolean needClose = new AtomicBoolean(false);
    private final Object createdLock = new Object();

    // --- JME3 引擎组件 ---
    private SystemListener listener;
    private Timer timer;
    private Thread renderThread;

    // --- Vulkan 核心组件 ---
    private VulkanRuntime runtime;
    private VKRenderer renderer;
    private volatile long windowHandle = NULL;
    private int windowId;

    // --- 输入驱动 ---
    private SdlKeyInput keyInput;
    private SdlMouseInput mouseInput;
    private SdlJoystickInput joyInput;

    private boolean wasActive = false;
    private boolean autoFlush = true;

    public LwjglVulkanContext() {
        this.type = Type.Display;
    }

    public LwjglVulkanContext(JmeContext.Type type) {
        this.type = type;
    }

    @Override
    public void setSettings(AppSettings settings) {
        this.settings.copyFrom(settings);
    }

    @Override
    public AppSettings getSettings() {
        return settings;
    }

    @Override
    public void setSystemListener(SystemListener listener) {
        this.listener = listener;
    }

    @Override
    public SystemListener getSystemListener() {
        return listener;
    }

    // =======================================================
    // 线程启动与生命周期控制
    // =======================================================
    @Override
    public void create(boolean waitFor) {
        if (created.get()) {
            return;
        }

        renderThread = new Thread(this, "VulkanRenderThread");
        renderThread.start();

        if (waitFor) {
            synchronized (createdLock) {
                while (!created.get()) {
                    try {
                        createdLock.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }
        }
    }

    @Override
    public void destroy(boolean waitFor) {
        needClose.set(true);
        if (renderThread == Thread.currentThread()) {
            return;
        }
        if (waitFor) {
            try {
                renderThread.join();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    @Override
    public boolean isCreated() {
        return created.get();
    }

    @Override
    public boolean isRenderable() {
        return renderable.get();
    }

    @Override
    public void run() {
        if (listener == null) {
            throw new IllegalStateException("SystemListener is not set!");
        }

        try {
            // 1. 创建 SDL3 窗口与 Vulkan 底层
            createContext();

            // 2. 初始化输入驱动
            keyInput = new SdlKeyInput(this);
            mouseInput = new SdlMouseInput(this);
            joyInput = new SdlJoystickInput(settings);

            timer = new NanoTimer();
            renderable.set(true);

            // 3. 唤醒等待线程
            synchronized (createdLock) {
                created.set(true);
                createdLock.notifyAll();
            }

            // 4. 启动 JME3 引擎(会回调 input 的 initialize)
            listener.initialize();

        } catch (Exception ex) {
            LOGGER.log(Level.SEVERE, "Failed to initialize Vulkan Context", ex);
            listener.handleError("Failed to create Vulkan display", ex);
            synchronized (createdLock) {
                createdLock.notifyAll();
            }
            return; // 启动失败直接退出
        }

        // 5. 渲染主循环
        while (!needClose.get() && !runtime.shouldClose()) {
            runLoop();
        }

        // 6. 清理退出
        destroyContext();
    }

    private void createContext() throws IOException {
        // 先行初始化 SDL Video 子系统,以便能获取到真实的屏幕分辨率
        if (!SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
            throw new IllegalStateException("Unable to initialize SDL video subsystem: " + SDL_GetError());
        }

        int displayID = SDL_GetPrimaryDisplay();
        SDL_DisplayMode videoMode = SDL_GetCurrentDisplayMode(displayID);

        int requestWidth = settings.getWindowWidth() <= 0 ? (videoMode != null ? videoMode.w() : 1280) : settings.getWindowWidth();
        int requestHeight = settings.getWindowHeight() <= 0 ? (videoMode != null ? videoMode.h() : 720) : settings.getWindowHeight();

        // 将修正后的长宽写回 settings,这样内部的 SdlWindow 才能按照正确的尺寸创建
        settings.setWidth(requestWidth);
        settings.setHeight(requestHeight);

        // 初始化 Runtime,内部的 VulkanRuntimeLifecycle 会通过 SdlWindow.create() 创建窗口
        runtime = new VulkanRuntime(settings);
        runtime.init(); // <--- 【核心修复点】不再传递 windowHandle,调用无参 init()

        renderer = new VKRenderer(runtime);
        renderer.initialize();

        // 提取出内部创建好的窗口句柄,供 JmeContext 管理
        windowHandle = runtime.getWindowHandle();
        windowId = SDL_GetWindowID(windowHandle);

        SDL_ShowWindow(windowHandle);
        updateSizes();
    }

    private void runLoop() {
        // 泵取 SDL 事件
        pollEvents();

        // 引擎逻辑更新
        listener.update();

        // Vulkan 渲染帧
        if (runtime != null && renderer != null) {
            timer.update();
            float tpf = timer.getTimePerFrame();

            int fbW = runtime.getFramebufferWidth();
            int fbH = runtime.getFramebufferHeight();
            if (fbW > 0 && fbH > 0) {
                runtime.renderFrame(tpf, renderer, null);
                renderer.postFrame();
            } else {
                try {
                    Thread.sleep(16);
                } catch (InterruptedException ignored) {
                }
            }
        }

        int frameRateLimit = settings.getFrameRate();
        if (!autoFlush) {
            frameRateLimit = 20;
        }
        Sync.sync(frameRateLimit);
    }

    private void pollEvents() {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            SDL_Event event = SDL_Event.malloc(stack);
            while (SDL_PollEvent(event)) {
                int evtType = event.type();

                if (evtType == SDL_EVENT_QUIT) {
                    needClose.set(true);
                } else if (evtType == SDL_EVENT_WINDOW_RESIZED || evtType == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) {
                    if (event.window().windowID() == windowId) {
                        updateSizes();
                    }
                } else if (evtType == SDL_EVENT_WINDOW_FOCUS_GAINED) {
                    if (!wasActive) {
                        listener.gainFocus();
                        timer.reset();
                        wasActive = true;
                    }
                } else if (evtType == SDL_EVENT_WINDOW_FOCUS_LOST) {
                    if (wasActive) {
                        listener.loseFocus();
                        wasActive = false;
                    }
                }

                // 委派给输入管理器
                if (keyInput != null) {
                    keyInput.onSDLEvent(event);
                }
                if (mouseInput != null) {
                    mouseInput.onSDLEvent(event);
                }
                if (joyInput != null) {
                    joyInput.onSDLEvent(event);
                }
            }
        }
    }

    private void updateSizes() {
        if (windowHandle == NULL) {
            return;
        }
        try (MemoryStack stack = MemoryStack.stackPush()) {
            java.nio.IntBuffer w = stack.mallocInt(1);
            java.nio.IntBuffer h = stack.mallocInt(1);

            SDL_GetWindowSizeInPixels(windowHandle, w, h);
            int fbW = Math.max(w.get(0), 1);
            int fbH = Math.max(h.get(0), 1);

            if (fbW != settings.getWidth() || fbH != settings.getHeight()) {
                settings.setResolution(fbW, fbH);
                if (listener != null) {
                    listener.reshape(fbW, fbH);
                }
                if (runtime != null) {
                    runtime.requestResize(fbW, fbH);
                }
            }
        }
    }

    private void destroyContext() {
        renderable.set(false);
        try {
            if (listener != null) {
                listener.destroy();
            }
            if (renderer != null) {
                renderer.cleanup();
            }

            // runtime.cleanup() 内部会级联调用 SdlWindow.destroy(),释放一次 SDL 子系统引用
            if (runtime != null) {
                runtime.cleanup();
            }

            windowHandle = NULL;
            windowId = 0;

            // 匹配 createContext 顶部的第一次 SDL_InitSubSystem,完美平衡引用计数
            SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS);

        } catch (Exception ex) {
            LOGGER.log(Level.SEVERE, "Failed to destroy Vulkan context", ex);
        }
    }

    // =======================================================
    // JmeContext 接口实现
    // =======================================================
    @Override
    public Type getType() {
        return type;
    }

    @Override
    public Timer getTimer() {
        return timer;
    }

    @Override
    public Renderer getRenderer() {
        return renderer;
    }

    @Override
    public MouseInput getMouseInput() {
        return mouseInput;
    }

    @Override
    public KeyInput getKeyInput() {
        return keyInput;
    }

    @Override
    public JoyInput getJoyInput() {
        return joyInput;
    }

    @Override
    public TouchInput getTouchInput() {
        return null;
    }

    @Override
    public void setAutoFlushFrames(boolean enabled) {
        this.autoFlush = enabled;
    }

    @Override
    public void restart() {
        LOGGER.warning("Restart not implemented for Vulkan context yet.");
    }

    @Override
    public Displays getDisplays() {
        return new Displays();
    }

    @Override
    public int getPrimaryDisplay() {
        return 0;
    }

    @Override
    public void setTitle(String title) {
        if (windowHandle != NULL) {
            SDL_SetWindowTitle(windowHandle, title);
        }
    }

    @Override
    public int getFramebufferWidth() {
        return settings.getWidth();
    }

    @Override
    public int getFramebufferHeight() {
        return settings.getHeight();
    }

    @Override
    public int getWindowXPosition() {
        if (windowHandle == NULL) {
            return 0;
        }
        try (MemoryStack stack = MemoryStack.stackPush()) {
            java.nio.IntBuffer x = stack.mallocInt(1);
            SDL_GetWindowPosition(windowHandle, x, null);
            return x.get(0);
        }
    }

    @Override
    public int getWindowYPosition() {
        if (windowHandle == NULL) {
            return 0;
        }
        try (MemoryStack stack = MemoryStack.stackPush()) {
            java.nio.IntBuffer y = stack.mallocInt(1);
            SDL_GetWindowPosition(windowHandle, null, y);
            return y.get(0);
        }
    }

    public long getWindowHandle() {
        return windowHandle;
    }

    public void getMouseInputScale(com.jme3.math.Vector2f scale) {
        if (windowHandle == NULL) {
            scale.set(1f, 1f);
            return;
        }
        try (MemoryStack stack = MemoryStack.stackPush()) {
            java.nio.IntBuffer w = stack.mallocInt(1);
            java.nio.IntBuffer h = stack.mallocInt(1);

            org.lwjgl.sdl.SDLVideo.SDL_GetWindowSize(windowHandle, w, h);
            int winW = Math.max(1, w.get(0));
            int winH = Math.max(1, h.get(0));

            org.lwjgl.sdl.SDLVideo.SDL_GetWindowSizeInPixels(windowHandle, w, h);
            int pixW = Math.max(1, w.get(0));
            int pixH = Math.max(1, h.get(0));

            scale.set((float) pixW / winW, (float) pixH / winH);
        }
    }
}

hello @RiccardoBlb
I would like to ask you about the correct way to load the custom renderer. This is the way I currently load it.

I’m not sure if this is the correct loading method.

If this is not the correct loading method, will JME be able to incorporate certain interfaces in the future that would allow custom renderers to be added?

I can make this configurable, but for the time being you can create a new jme3-yourrenderer module to replace jme-lwjgl3 with a class called com.jme3.system.lwjgl.LwjglDisplay

1 Like