External Publication
Visit Post

jME 3.10.0-beta1

jMonkeyEngine Hub June 2, 2026
Source
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?

Discussion in the ATmosphere

Loading comments...