diff --git a/jme3-xr/build.gradle b/jme3-xr/build.gradle new file mode 100644 index 0000000000..b56dd97778 --- /dev/null +++ b/jme3-xr/build.gradle @@ -0,0 +1,30 @@ +dependencies { + api project(':jme3-core') + api project(':jme3-lwjgl3') + api project(':jme3-desktop') + api project(':jme3-effects') + + // https://mvnrepository.com/artifact/net.java.dev.jna/jna + implementation 'net.java.dev.jna:jna:5.10.0' + implementation 'com.nativelibs4java:jnaerator-runtime:0.12' + + // Native OpenXR/LWJGL support + api "org.lwjgl:lwjgl-openxr:${lwjgl3Version}" +// implementation "org.lwjgl:lwjgl-openxr:${lwjgl3Version}:natives-linux" +// implementation "org.lwjgl:lwjgl-openxr:${lwjgl3Version}:natives-macos" + runtimeOnly "org.lwjgl:lwjgl-openxr:${lwjgl3Version}:natives-windows" + runtimeOnly "org.lwjgl:lwjgl-openxr:${lwjgl3Version}:natives-linux" +// runtimeOnly "org.lwjgl:lwjgl-openxr:${lwjgl3Version}:natives-macos" + + // Necessary by lwjgl-openxr + api "org.joml:joml:1.10.4" + api "org.lwjgl:lwjgl-egl:${lwjgl3Version}" +// api "org.lwjgl:lwjgl-vulkan:${lwjgl3Version}" +} + +javadoc { + // Disable doclint for JDK8+. + if (JavaVersion.current().isJava8Compatible()){ + options.addStringOption('Xdoclint:none', '-quiet') + } +} diff --git a/jme3-xr/src/main/java/com/jme3/input/xr/Eye.java b/jme3-xr/src/main/java/com/jme3/input/xr/Eye.java new file mode 100644 index 0000000000..8f7159f316 --- /dev/null +++ b/jme3-xr/src/main/java/com/jme3/input/xr/Eye.java @@ -0,0 +1,94 @@ +package com.jme3.input.xr; + +import com.jme3.app.SimpleApplication; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.renderer.ViewPort; +import com.jme3.scene.Geometry; +import com.jme3.scene.shape.Box; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Texture; +import com.jme3.texture.Texture2D; +import com.jme3.texture.FrameBuffer.FrameBufferTarget; +import com.jme3.texture.Image.Format; + +public class Eye { + static int index = 0; + SimpleApplication app; + float posX; + Vector3f tmpVec = new Vector3f(); + Texture2D offTex; + Geometry offGeo; + Camera offCamera; + Vector3f centerPos = new Vector3f(0f, 0f, -5f); + Quaternion centerRot = new Quaternion(); + + public Eye(SimpleApplication app) + { + this.app = app; + setupOffscreenView(app); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setTexture("ColorMap", offTex); + + offGeo = new Geometry("box", new Box(1, 1, 1)); + offGeo.setMaterial(mat); + } + + public void setPosX(float posX) { this.posX = posX; } + public float getPosX() { return posX; } + + /** Moves the camera. + * @param The new absolute center position. */ + public void moveAbs(Vector3f newPos) + { + centerPos.set(newPos); + rotateAbs(offCamera.getRotation()); + } + + /** Rotates the camera, and moves left/right. + * @param The new rotation. */ + public void rotateAbs(Quaternion newRot) + { + tmpVec.set(posX, 0.0f, 0.0f); + newRot.multLocal(tmpVec); + offCamera.setLocation(tmpVec.addLocal(centerPos)); + offCamera.setRotation(newRot); + } + + private void setupOffscreenView(SimpleApplication app) + { + int w = app.getContext().getSettings().getWidth(); + int h = app.getContext().getSettings().getHeight(); + offCamera = new Camera(w, h); + + ViewPort offView = app.getRenderManager().createPreView("OffscreenViewX" + (index++), offCamera); + offView.setClearFlags(true, true, true); + offView.setBackgroundColor(ColorRGBA.DarkGray); + FrameBuffer offBuffer = new FrameBuffer(w, h, 1); + + //setup framebuffer's cam + offCamera.setFrustumPerspective(45f, 1f, 1f, 1000f); + offCamera.lookAt(new Vector3f(0f, 0f, 0f), Vector3f.UNIT_Y); + + //setup framebuffer's texture + offTex = new Texture2D(w, h, Format.RGBA8); + offTex.setMinFilter(Texture.MinFilter.Trilinear); + offTex.setMagFilter(Texture.MagFilter.Bilinear); + + //setup framebuffer to use texture + offBuffer.setDepthTarget(FrameBufferTarget.newTarget(Format.Depth)); + offBuffer.addColorTarget(FrameBufferTarget.newTarget(offTex)); + + //set viewport to render to offscreen framebuffer + offView.setOutputFrameBuffer(offBuffer); + offView.attachScene(app.getRootNode()); + } + + public void render() + { + app.getRenderManager().renderGeometry(offGeo); + } +} diff --git a/jme3-xr/src/main/java/com/jme3/input/xr/XrHmd.java b/jme3-xr/src/main/java/com/jme3/input/xr/XrHmd.java new file mode 100644 index 0000000000..0796571503 --- /dev/null +++ b/jme3-xr/src/main/java/com/jme3/input/xr/XrHmd.java @@ -0,0 +1,105 @@ +package com.jme3.input.xr; + +import java.util.ArrayList; + +import com.jme3.app.SimpleApplication; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.system.AppSettings; +import com.jme3.system.lwjgl.LwjglWindowXr; + +public class XrHmd +{ + public static final float DEFAULT_X_DIST = 0.4f; + public static final float DEFAULT_X_ROT = 0.028f; + public static final float DEFAULT_POS_MULT = 10.0f; + Quaternion tmpQ = new Quaternion(); + float[] tmpArr = new float[3]; + Quaternion tmpQdistRotL = new Quaternion().fromAngles(0, -DEFAULT_X_ROT, 0); + Quaternion tmpQdistRotR = new Quaternion().fromAngles(0, DEFAULT_X_ROT, 0); + SimpleApplication app; + Eye leftEye; + Eye rightEye; + ArrayList hmdListeners = new ArrayList(); + ArrayList contr1Listeners = new ArrayList(); + ArrayList contr2Listeners = new ArrayList(); + + public XrHmd(SimpleApplication app) + { + this.app = app; + leftEye = new Eye(app); + rightEye = new Eye(app); + setXDistance(DEFAULT_X_DIST); + hmdListeners.add((p,r) -> doMoveRotate(p,r)); + } + + Vector3f initPos; + Vector3f diffPos = new Vector3f(); + private void doMoveRotate(Vector3f p, Quaternion r) + { + if (initPos == null) { initPos = new Vector3f(p.getX(), p.getY(), p.getZ()); } + initPos.subtract(p,diffPos); + leftEye.moveAbs(diffPos); + rightEye.moveAbs(diffPos); + tmpQ.set(r); + tmpQ.inverseLocal(); + tmpQ.set(tmpQ.getX(), tmpQ.getY(), -tmpQ.getZ(), tmpQ.getW()); + leftEye.rotateAbs(tmpQ.multLocal(tmpQdistRotL)); + tmpQ.set(r); + tmpQ.inverseLocal(); + tmpQ.set(tmpQ.getX(), tmpQ.getY(), -tmpQ.getZ(), tmpQ.getW()); + rightEye.rotateAbs(tmpQ.multLocal(tmpQdistRotR)); + } + + public Eye getLeftEye() { return leftEye; } + public Eye getRightEye() { return rightEye; } + + public ArrayList getHmdOrientationListeners() { return hmdListeners; } + public ArrayList getContr1ButtonPressedListeners() { return contr1Listeners; } + public ArrayList getContr2ButtonPressedListeners() { return contr2Listeners; } + + Vector3f multPos = new Vector3f(); + public void onUpdateHmdOrientation(Vector3f viewPos, Quaternion viewRot) + { + viewPos.mult(DEFAULT_POS_MULT, multPos); + multPos.setX(0.0f-multPos.getX()); + multPos.setY(0.0f-multPos.getY()); + for (XrListener.OrientationListener l : hmdListeners) { l.onUpdateOrientation(multPos, viewRot); } + } + + /** Must be called in main function before init. + * @param s The appSettings that must be used with app.setSettings(s). */ + public static void setRendererForSettings(AppSettings s) + { + s.setRenderer("CUSTOM" + com.jme3.system.lwjgl.LwjglWindowXr.class.getName()); //see JmeDesktopSystem.newContext(...) + } + + /** Must be called in simpleInitApp-function of SimpleApplication. + * @return The head-mounted-device object. */ + public static XrHmd initHmd(SimpleApplication app) + { + XrHmd xrHmd = new XrHmd(app); + ((LwjglWindowXr)app.getContext()).getXr().setHmd(xrHmd); + return xrHmd; + } + + /** Gets the distance between the eyes. Default is DEFAULT_X_DIST */ + public float getXDistance() { return rightEye.getPosX() * 2f; } + + /** Sets the distance between the eyes. Default is DEFAULT_X_DIST */ + public void setXDistance(float xDist) + { + rightEye.setPosX(xDist / 2f); + leftEye.setPosX(xDist / -2f); + } + + /** Gets the rotation angle between the eyes. Default is DEFAULT_X_ROT */ + public float getXRotation() { return tmpQdistRotR.toAngles(tmpArr)[1]; } + + /** Sets the rotation angle between the eyes. Default is DEFAULT_X_ROT */ + public void setXRotation(float xRot) + { + tmpQdistRotR.fromAngles(0, xRot, 0); + tmpQdistRotL.fromAngles(0, xRot, 0); + } +} diff --git a/jme3-xr/src/main/java/com/jme3/input/xr/XrListener.java b/jme3-xr/src/main/java/com/jme3/input/xr/XrListener.java new file mode 100644 index 0000000000..aa57f52fa2 --- /dev/null +++ b/jme3-xr/src/main/java/com/jme3/input/xr/XrListener.java @@ -0,0 +1,16 @@ +package com.jme3.input.xr; + +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; + +public interface XrListener { + public interface OrientationListener + { + public void onUpdateOrientation(Vector3f pos, Quaternion rot); + } + + public interface ButtonPressedListener + { + public void onButtonPressed(int num); + } +} diff --git a/jme3-xr/src/main/java/com/jme3/system/lwjgl/EmptyKeyInput.java b/jme3-xr/src/main/java/com/jme3/system/lwjgl/EmptyKeyInput.java new file mode 100644 index 0000000000..79469e24dc --- /dev/null +++ b/jme3-xr/src/main/java/com/jme3/system/lwjgl/EmptyKeyInput.java @@ -0,0 +1,40 @@ +package com.jme3.system.lwjgl; + +import static org.lwjgl.glfw.GLFW.glfwGetTime; + +import com.jme3.input.KeyInput; +import com.jme3.input.RawInputListener; + +public class EmptyKeyInput implements KeyInput { + + public EmptyKeyInput() { } + @Override + public void initialize() {} + + @Override + public boolean isInitialized() {return false;} + + @Override + public void update() {} + @Override + public void destroy() { + // TODO Auto-generated method stub + + } + @Override + public void setInputListener(RawInputListener listener) { + // TODO Auto-generated method stub + + } + @Override + public long getInputTimeNanos() { + return (long) (glfwGetTime() * 1000000000); + } + @Override + public String getKeyName(int key) { + // TODO Auto-generated method stub + return null; + } + + public void resetContext() {} +} diff --git a/jme3-xr/src/main/java/com/jme3/system/lwjgl/EmptyMouseInput.java b/jme3-xr/src/main/java/com/jme3/system/lwjgl/EmptyMouseInput.java new file mode 100644 index 0000000000..f58570def6 --- /dev/null +++ b/jme3-xr/src/main/java/com/jme3/system/lwjgl/EmptyMouseInput.java @@ -0,0 +1,52 @@ +package com.jme3.system.lwjgl; + +import static org.lwjgl.glfw.GLFW.glfwGetTime; + +import com.jme3.cursors.plugins.JmeCursor; +import com.jme3.input.MouseInput; +import com.jme3.input.RawInputListener; + +public class EmptyMouseInput implements MouseInput { + + public EmptyMouseInput() { } + @Override + public void initialize() {} + + @Override + public boolean isInitialized() {return false;} + @Override + public void update() { + // TODO Auto-generated method stub + + } + @Override + public void destroy() { + // TODO Auto-generated method stub + + } + @Override + public void setInputListener(RawInputListener listener) { + // TODO Auto-generated method stub + + } + @Override + public long getInputTimeNanos() { + return (long) (glfwGetTime() * 1000000000); + } + @Override + public void setCursorVisible(boolean visible) { + // TODO Auto-generated method stub + + } + @Override + public int getButtonCount() { + // TODO Auto-generated method stub + return 3; + } + @Override + public void setNativeCursor(JmeCursor cursor) { + // TODO Auto-generated method stub + } + + public void resetContext() {} +} diff --git a/jme3-xr/src/main/java/com/jme3/system/lwjgl/LwjglContextXr.java b/jme3-xr/src/main/java/com/jme3/system/lwjgl/LwjglContextXr.java new file mode 100644 index 0000000000..fd1203fc5a --- /dev/null +++ b/jme3-xr/src/main/java/com/jme3/system/lwjgl/LwjglContextXr.java @@ -0,0 +1,352 @@ +/* + * Copyright (c) 2009-2023 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 com.jme3.system.lwjgl; + +import com.jme3.input.lwjgl.GlfwJoystickInput; +import com.jme3.lwjgl3.utils.APIUtil; +import com.jme3.opencl.Context; +import com.jme3.renderer.Renderer; +import com.jme3.renderer.RendererException; +import com.jme3.renderer.lwjgl.LwjglGL; +import com.jme3.renderer.lwjgl.LwjglGLExt; +import com.jme3.renderer.lwjgl.LwjglGLFboEXT; +import com.jme3.renderer.lwjgl.LwjglGLFboGL3; +import com.jme3.renderer.opengl.*; +import com.jme3.system.AppSettings; +import com.jme3.system.JmeContext; +import com.jme3.system.SystemListener; +import com.jme3.system.Timer; +import com.jme3.util.BufferAllocatorFactory; +import com.jme3.util.LWJGLBufferAllocator; +import com.jme3.util.LWJGLBufferAllocator.ConcurrentLWJGLBufferAllocator; +import static com.jme3.util.LWJGLBufferAllocator.PROPERTY_CONCURRENT_BUFFER_ALLOCATOR; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.lwjgl.Version; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.glfw.GLFWJoystickCallback; +import org.lwjgl.opengl.ARBDebugOutput; +import org.lwjgl.opengl.ARBFramebufferObject; +import org.lwjgl.opengl.EXTFramebufferMultisample; +import static org.lwjgl.opengl.GL.createCapabilities; +import static org.lwjgl.opengl.GL11.glGetInteger; +import org.lwjgl.opengl.GLCapabilities; + +/** + * A LWJGL implementation of a graphics context. + */ +public abstract class LwjglContextXr implements JmeContext { + + private static final Logger logger = Logger.getLogger(LwjglContextXr.class.getName()); + + static { + + final String implementation = BufferAllocatorFactory.PROPERTY_BUFFER_ALLOCATOR_IMPLEMENTATION; + + if (System.getProperty(implementation) == null) { + if (Boolean.parseBoolean(System.getProperty(PROPERTY_CONCURRENT_BUFFER_ALLOCATOR, "true"))) { + System.setProperty(implementation, ConcurrentLWJGLBufferAllocator.class.getName()); + } else { + System.setProperty(implementation, LWJGLBufferAllocator.class.getName()); + } + } + } + + private static final Set SUPPORTED_RENDERS = new HashSet<>(Arrays.asList( + AppSettings.LWJGL_OPENGL2, + AppSettings.LWJGL_OPENGL30, + AppSettings.LWJGL_OPENGL31, + AppSettings.LWJGL_OPENGL32, + AppSettings.LWJGL_OPENGL33, + AppSettings.LWJGL_OPENGL40, + AppSettings.LWJGL_OPENGL41, + AppSettings.LWJGL_OPENGL42, + AppSettings.LWJGL_OPENGL43, + AppSettings.LWJGL_OPENGL44, + AppSettings.LWJGL_OPENGL45 + )); + + public static final boolean CL_GL_SHARING_POSSIBLE = true; + + protected final Object createdLock = new Object(); + protected final AtomicBoolean created = new AtomicBoolean(false); + protected final AtomicBoolean renderable = new AtomicBoolean(false); + protected final AppSettings settings = new AppSettings(true); + + protected EmptyKeyInput keyInput; + protected EmptyMouseInput mouseInput; + protected GlfwJoystickInput joyInput; + + protected Timer timer; + + protected Renderer renderer; + protected SystemListener listener; + + protected com.jme3.opencl.lwjgl.LwjglContext clContext; + + /** + * Accesses the listener that receives events related to this context. + * + * @return the pre-existing instance + */ + @Override + public SystemListener getSystemListener() { + return listener; + } + + @Override + public void setSystemListener(final SystemListener listener) { + this.listener = listener; + } + + protected void printContextInitInfo() { + if (logger.isLoggable(Level.INFO)) { + logger.log(Level.INFO, "LWJGL {0} context running on thread {1}\n * Graphics Adapter: GLFW {2}", + APIUtil.toArray(Version.getVersion(), Thread.currentThread().getName(), GLFW.glfwGetVersionString())); + } + } + + protected int determineMaxSamples() { + + // If we already have a valid context, determine samples using current context. + if (GLFW.glfwExtensionSupported("GL_ARB_framebuffer_object")) { + return glGetInteger(ARBFramebufferObject.GL_MAX_SAMPLES); + } else if (GLFW.glfwExtensionSupported("GL_EXT_framebuffer_multisample")) { + return glGetInteger(EXTFramebufferMultisample.GL_MAX_SAMPLES_EXT); + } + + return Integer.MAX_VALUE; + } + + protected int getNumSamplesToUse() { + + int samples = 0; + + if (settings.getSamples() > 1) { + samples = settings.getSamples(); + final int supportedSamples = determineMaxSamples(); + if (supportedSamples < samples) { + logger.log(Level.WARNING, "Couldn't satisfy antialiasing samples requirement: x{0}. " + + "Video hardware only supports: x{1}", APIUtil.toArray(samples, supportedSamples)); + samples = supportedSamples; + } + } + + return samples; + } + + /** + * Reinitializes the relevant details of the context. For internal use only. + */ + protected void reinitContext() { + initContext(false); + } + + /** + * Initializes the LWJGL renderer and input for the first time. For internal + * use only. + */ + protected void initContextFirstTime() { + initContext(true); + } + + /** + * Initializes the LWJGL renderer and input. + * @param first - Whether this is the first time we are initializing and we + * need to create the renderer or not. Otherwise, we'll just reset the + * renderer as needed. + */ + private void initContext(boolean first) { + + settings.setRenderer(AppSettings.LWJGL_OPENGL45); // We force Opengl4 as needed for openxr + final String renderer = settings.getRenderer(); + final GLCapabilities capabilities = createCapabilities(!renderer.equals(AppSettings.LWJGL_OPENGL2)); + + if (!capabilities.OpenGL20) { + throw new RendererException("OpenGL 2.0 or higher is required for jMonkeyEngine"); + } else if (!SUPPORTED_RENDERS.contains(renderer)) { + throw new UnsupportedOperationException("Unsupported renderer: " + renderer); + } + + if (first) { + GL gl = new LwjglGL(); + GLExt glext = new LwjglGLExt(); + GLFbo glfbo; + + if (capabilities.OpenGL30) { + glfbo = new LwjglGLFboGL3(); + } else { + glfbo = new LwjglGLFboEXT(); + } + + if (settings.isGraphicsDebug()) { + gl = (GL) GLDebug.createProxy(gl, gl, GL.class, GL2.class, GL3.class, GL4.class); + glext = (GLExt) GLDebug.createProxy(gl, glext, GLExt.class); + glfbo = (GLFbo) GLDebug.createProxy(gl, glfbo, GLFbo.class); + } + + if (settings.isGraphicsTiming()) { + GLTimingState timingState = new GLTimingState(); + gl = (GL) GLTiming.createGLTiming(gl, timingState, GL.class, GL2.class, GL3.class, GL4.class); + glext = (GLExt) GLTiming.createGLTiming(glext, timingState, GLExt.class); + glfbo = (GLFbo) GLTiming.createGLTiming(glfbo, timingState, GLFbo.class); + } + + if (settings.isGraphicsTrace()) { + gl = (GL) GLTracer.createDesktopGlTracer(gl, GL.class, GL2.class, GL3.class, GL4.class); + glext = (GLExt) GLTracer.createDesktopGlTracer(glext, GLExt.class); + glfbo = (GLFbo) GLTracer.createDesktopGlTracer(glfbo, GLFbo.class); + } + + this.renderer = new GLRenderer(gl, glext, glfbo); + if (this.settings.isGraphicsDebug()) ((GLRenderer)this.renderer).setDebugEnabled(true); + } + this.renderer.initialize(); + + if (capabilities.GL_ARB_debug_output && settings.isGraphicsDebug()) { + ARBDebugOutput.glDebugMessageCallbackARB(new LwjglGLDebugOutputHandler(), 0); + } + + this.renderer.setMainFrameBufferSrgb(settings.isGammaCorrection()); + this.renderer.setLinearizeSrgbImages(settings.isGammaCorrection()); + + if (first) { + // Init input + if (keyInput != null) { + keyInput.initialize(); + } + + if (mouseInput != null) { + mouseInput.initialize(); + } + + if (joyInput != null) { + joyInput.initialize(); + } + + GLFW.glfwSetJoystickCallback(new GLFWJoystickCallback() { + @Override + public void invoke(int jid, int event) { + + // Invoke the disconnected event before we reload the joysticks or lose the reference to it. + // Invoke the connected event after we reload the joysticks to obtain the reference to it. + + if ( event == GLFW.GLFW_CONNECTED ) { + joyInput.reloadJoysticks(); + joyInput.fireJoystickConnectedEvent(jid); + } + else { + joyInput.fireJoystickDisconnectedEvent(jid); + joyInput.reloadJoysticks(); + } + } + }); + } + + renderable.set(true); + } + + public void internalDestroy() { + renderer = null; + timer = null; + renderable.set(false); + synchronized (createdLock) { + created.set(false); + createdLock.notifyAll(); + } + } + + public void internalCreate() { + synchronized (createdLock) { + created.set(true); + createdLock.notifyAll(); + } + initContextFirstTime(); + } + + public void create() { + create(false); + } + + public void destroy() { + destroy(false); + } + + protected void waitFor(boolean createdVal) { + synchronized (createdLock) { + while (created.get() != createdVal) { + try { + createdLock.wait(); + } catch (InterruptedException ignored) { + } + } + } + } + + @Override + public boolean isCreated() { + return created.get(); + } + + @Override + public boolean isRenderable() { + return renderable.get(); + } + + @Override + public void setSettings(AppSettings settings) { + this.settings.copyFrom(settings); + } + + @Override + public AppSettings getSettings() { + return settings; + } + + @Override + public Renderer getRenderer() { + return renderer; + } + + @Override + public Timer getTimer() { + return timer; + } + + @Override + public Context getOpenCLContext() { + return clContext; + } +} diff --git a/jme3-xr/src/main/java/com/jme3/system/lwjgl/LwjglWindowXr.java b/jme3-xr/src/main/java/com/jme3/system/lwjgl/LwjglWindowXr.java new file mode 100644 index 0000000000..1eb341f045 --- /dev/null +++ b/jme3-xr/src/main/java/com/jme3/system/lwjgl/LwjglWindowXr.java @@ -0,0 +1,667 @@ +/* + * Copyright (c) 2009-2023 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 com.jme3.system.lwjgl; + +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.GlfwJoystickInput; +import com.jme3.math.Vector2f; +import com.jme3.system.AppSettings; +import com.jme3.system.JmeContext; +import com.jme3.system.JmeSystem; +import com.jme3.system.NanoTimer; +import com.jme3.util.BufferUtils; +import com.jme3.util.SafeArrayList; +import com.jme3.system.lwjgl.openxr.HelloOpenXRGL; + +import org.lwjgl.Version; +import org.lwjgl.glfw.GLFWErrorCallback; +import org.lwjgl.glfw.GLFWFramebufferSizeCallback; +import org.lwjgl.glfw.GLFWImage; +import org.lwjgl.glfw.GLFWWindowFocusCallback; +import org.lwjgl.glfw.GLFWWindowSizeCallback; +import org.lwjgl.system.Platform; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.system.MemoryUtil.NULL; + +/** + * A wrapper class over the GLFW framework in LWJGL 3. + * + * @author Daniel Johansson + */ +public class LwjglWindowXr extends LwjglContextXr implements Runnable { + + private static final Logger LOGGER = Logger.getLogger(LwjglWindowXr.class.getName()); + + private static final Map RENDER_CONFIGS = new HashMap<>(); + + static { + RENDER_CONFIGS.put(AppSettings.LWJGL_OPENGL30, () -> { + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + }); + RENDER_CONFIGS.put(AppSettings.LWJGL_OPENGL31, () -> { + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1); + }); + RENDER_CONFIGS.put(AppSettings.LWJGL_OPENGL32, () -> { + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); + }); + RENDER_CONFIGS.put(AppSettings.LWJGL_OPENGL33, () -> { + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + }); + RENDER_CONFIGS.put(AppSettings.LWJGL_OPENGL40, () -> { + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + }); + RENDER_CONFIGS.put(AppSettings.LWJGL_OPENGL41, () -> { + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1); + }); + RENDER_CONFIGS.put(AppSettings.LWJGL_OPENGL42, () -> { + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); + }); + RENDER_CONFIGS.put(AppSettings.LWJGL_OPENGL43, () -> { + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + }); + RENDER_CONFIGS.put(AppSettings.LWJGL_OPENGL44, () -> { + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 4); + }); + RENDER_CONFIGS.put(AppSettings.LWJGL_OPENGL45, () -> { + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5); + }); + } + + protected final AtomicBoolean needClose = new AtomicBoolean(false); + protected final AtomicBoolean needRestart = new AtomicBoolean(false); + + private final JmeContext.Type type; + private final SafeArrayList windowSizeListeners = new SafeArrayList<>(WindowSizeListener.class); + + private GLFWErrorCallback errorCallback; + private GLFWWindowSizeCallback windowSizeCallback; + private GLFWFramebufferSizeCallback framebufferSizeCallback; + private GLFWWindowFocusCallback windowFocusCallback; + + HelloOpenXRGL xr; + + private Thread mainThread; + + private long window = NULL; + private int frameRateLimit = -1; + + protected boolean wasActive = false; + protected boolean autoFlush = true; + protected boolean allowSwapBuffers = false; + + // temp variables used for glfw calls + private int width[] = new int[1]; + private int height[] = new int[1]; + + public LwjglWindowXr() { + this.type = JmeContext.Type.Display; + } + + public HelloOpenXRGL getXr() { return xr; } + + /** + * Registers the specified listener to get notified when window size changes. + * + * @param listener The WindowSizeListener to register. + */ + public void registerWindowSizeListener(WindowSizeListener listener) { + windowSizeListeners.add(listener); + } + + /** + * Removes the specified listener from the listeners list. + * + * @param listener The WindowSizeListener to remove. + */ + public void removeWindowSizeListener(WindowSizeListener listener) { + windowSizeListeners.remove(listener); + } + + /** + * @return Type.Display or Type.Canvas + */ + @Override + public JmeContext.Type getType() { + return type; + } + + /** + * Set the title if it's a windowed display + * + * @param title the title to set + */ + @Override + public void setTitle(final String title) { + if (created.get() && window != NULL) { + glfwSetWindowTitle(window, title); + } + } + + /** + * Restart if it's a windowed or full-screen display. + */ + @Override + public void restart() { + if (created.get()) { + needRestart.set(true); + } else { + LOGGER.warning("Display is not created, cannot restart window."); + } + } + + /** + * Apply the settings, changing resolution, etc. + * + * @param settings the settings to apply when creating the context. + */ + protected void createContext(final AppSettings settings) { + + glfwSetErrorCallback(errorCallback = new GLFWErrorCallback() { + @Override + public void invoke(int error, long description) { + final String message = GLFWErrorCallback.getDescription(description); + listener.handleError(message, new Exception(message)); + } + }); +System.out.println("Pre init xr"); +xr = new HelloOpenXRGL(settings); +System.out.println("Post init xr"); +window = xr.getWindow(); + } + + + protected void showWindow() { + glfwShowWindow(window); + } + + /** + * Set custom icons to the window of this application. + * + * @param settings settings for getting the icons + */ + protected void setWindowIcon(final AppSettings settings) { + + final Object[] icons = settings.getIcons(); + if (icons == null) return; + + final GLFWImage[] images = imagesToGLFWImages(icons); + + try (final GLFWImage.Buffer iconSet = GLFWImage.malloc(images.length)) { + + for (int i = images.length - 1; i >= 0; i--) { + final GLFWImage image = images[i]; + iconSet.put(i, image); + } + + glfwSetWindowIcon(window, iconSet); + } + } + + /** + * Convert array of images to array of {@link GLFWImage}. + */ + private GLFWImage[] imagesToGLFWImages(final Object[] images) { + + final GLFWImage[] out = new GLFWImage[images.length]; + + for (int i = 0; i < images.length; i++) { + final BufferedImage image = (BufferedImage) images[i]; + out[i] = imageToGLFWImage(image); + } + + return out; + } + + /** + * Convert the {@link BufferedImage} to the {@link GLFWImage}. + */ + private GLFWImage imageToGLFWImage(BufferedImage image) { + + if (image.getType() != BufferedImage.TYPE_INT_ARGB_PRE) { + + final BufferedImage convertedImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE); + final Graphics2D graphics = convertedImage.createGraphics(); + + final int targetWidth = image.getWidth(); + final int targetHeight = image.getHeight(); + + graphics.drawImage(image, 0, 0, targetWidth, targetHeight, null); + graphics.dispose(); + + image = convertedImage; + } + + final ByteBuffer buffer = BufferUtils.createByteBuffer(image.getWidth() * image.getHeight() * 4); + + for (int i = 0; i < image.getHeight(); i++) { + for (int j = 0; j < image.getWidth(); j++) { + int colorSpace = image.getRGB(j, i); + buffer.put((byte) ((colorSpace << 8) >> 24)); + buffer.put((byte) ((colorSpace << 16) >> 24)); + buffer.put((byte) ((colorSpace << 24) >> 24)); + buffer.put((byte) (colorSpace >> 24)); + } + } + + buffer.flip(); + + final GLFWImage result = GLFWImage.create(); + result.set(image.getWidth(), image.getHeight(), buffer); + + return result; + } + + /** + * Destroy the context. + */ + protected void destroyContext() { + try { + if (renderer != null) { + renderer.cleanup(); + } + + if (errorCallback != null) { + + // We need to specifically set this to null as we might set a new callback before we reinit GLFW + glfwSetErrorCallback(null); + + errorCallback.close(); + errorCallback = null; + } + + if (windowSizeCallback != null) { + windowSizeCallback.close(); + windowSizeCallback = null; + } + + if (framebufferSizeCallback != null) { + framebufferSizeCallback.close(); + framebufferSizeCallback = null; + } + + if (windowFocusCallback != null) { + windowFocusCallback.close(); + windowFocusCallback = null; + } + + if (window != NULL) { + glfwDestroyWindow(window); + window = NULL; + } + + } catch (final Exception ex) { + listener.handleError("Failed to destroy context", ex); + } + } + + @Override + public void create(boolean waitFor) { + if (created.get()) { + LOGGER.warning("create() called when display is already created!"); + return; + } + + if (Platform.get() == Platform.MACOSX) { + // NOTE: this is required for Mac OS X! + mainThread = Thread.currentThread(); + mainThread.setName("jME3 Main"); + if (waitFor) { + LOGGER.warning("create(true) is not supported for macOS!"); + } + run(); + } else { + mainThread = new Thread(this, "jME3 Main"); + mainThread.start(); + if (waitFor) { + waitFor(true); + } + } + + } + + /** + * Does LWJGL display initialization in the OpenGL thread + * + * @return returns {@code true} if the context initialization was successful + */ + protected boolean initInThread() { + try { + if (!JmeSystem.isLowPermissions()) { + // Enable uncaught exception handler only for current thread + Thread.currentThread().setUncaughtExceptionHandler((thread, thrown) -> { + listener.handleError("Uncaught exception thrown in " + thread.toString(), thrown); + if (needClose.get()) { + // listener.handleError() has requested the + // context to close. Satisfy request. + deinitInThread(); + } + }); + } + + timer = new NanoTimer(); + + // For canvas, this will create a PBuffer, + // allowing us to query information. + // When the canvas context becomes available, it will + // be replaced seamlessly. + createContext(settings); + printContextInitInfo(); + + created.set(true); + super.internalCreate(); //Try + } catch (Exception ex) { + try { + if (window != NULL) { + glfwDestroyWindow(window); + window = NULL; + } + } catch (Exception ex2) { + LOGGER.log(Level.WARNING, null, ex2); + } + + listener.handleError("Failed to create display", ex); + return false; // if we failed to create display, do not continue + } + + listener.initialize(); + + return true; + } + + + /** + * execute one iteration of the render loop in the OpenGL thread + */ + protected void runLoop() { + // If a restart is required, lets recreate the context. + if (needRestart.getAndSet(false)) { + restartContext(); + } + + if (!created.get()) { + throw new IllegalStateException(); + } + + + listener.update(); + + // All this does is call glfwSwapBuffers(). + // If the canvas is not active, there's no need to waste time + // doing that. + if (renderable.get()) { + // calls swap buffers, etc. + try { + if (allowSwapBuffers && autoFlush) { + glfwSwapBuffers(window); + } + } catch (Throwable ex) { + listener.handleError("Error while swapping buffers", ex); + } + } + xr.renderFrame(); + + // Subclasses just call GLObjectManager. Clean up objects here. + // It is safe ... for now. + if (renderer != null) { + renderer.postFrame(); + } + + if (autoFlush) { + if (frameRateLimit != getSettings().getFrameRate()) { + setFrameRateLimit(getSettings().getFrameRate()); + } + } else if (frameRateLimit != 20) { + setFrameRateLimit(20); + } + + Sync.sync(frameRateLimit); + + glfwPollEvents(); + } + + private void restartContext() { + try { + destroyContext(); + createContext(settings); + } catch (Exception ex) { + LOGGER.log(Level.SEVERE, "Failed to set display settings!", ex); + } + // Reinitialize context flags and such + reinitContext(); + + // We need to reinit the mouse and keyboard input as they are tied to a window handle + if (keyInput != null && keyInput.isInitialized()) { + keyInput.resetContext(); + } + if (mouseInput != null && mouseInput.isInitialized()) { + mouseInput.resetContext(); + } + + LOGGER.fine("Display restarted."); + } + + private void setFrameRateLimit(int frameRateLimit) { + this.frameRateLimit = frameRateLimit; + } + + /** + * De-initialize in the OpenGL thread. + */ + protected void deinitInThread() { + listener.destroy(); + + destroyContext(); + super.internalDestroy(); + glfwTerminate(); + + LOGGER.fine("Display destroyed."); + } + + @Override + public void run() { + if (listener == null) { + throw new IllegalStateException("SystemListener is not set on context!" + + "Must set with JmeContext.setSystemListener()."); + } + + LOGGER.log(Level.FINE, "Using LWJGL {0}", Version.getVersion()); + + if (!initInThread()) { + LOGGER.log(Level.SEVERE, "Display initialization failed. Cannot continue."); + return; + } + + while (true) { + + runLoop(); + if (needClose.get()) { + break; + } + + if (glfwWindowShouldClose(window)) { //Try + listener.requestClose(false); //Try + } //Try + } + + deinitInThread(); + } + + @Override + public JoyInput getJoyInput() { + if (joyInput == null) { + joyInput = new GlfwJoystickInput(); + } + return joyInput; + } + + @Override + public MouseInput getMouseInput() { + if (mouseInput == null) { + mouseInput = new EmptyMouseInput(); + } + return mouseInput; + } + + @Override + public KeyInput getKeyInput() { + if (keyInput == null) { + keyInput = new EmptyKeyInput(); + } + return keyInput; + } + + @Override + public TouchInput getTouchInput() { + return null; + } + + @Override + public void setAutoFlushFrames(boolean enabled) { + this.autoFlush = enabled; + } + + @Override + public void destroy(boolean waitFor) { + needClose.set(true); + + if (mainThread == Thread.currentThread()) { + // Ignore waitFor. + return; + } + + if (waitFor) { + waitFor(false); + } + } + + public long getWindowHandle() { + return window; + } + + /** + * Get the window content scale, for HiDPI support. + * + * The content scale is the ratio between the current DPI and the platform's default DPI. + * This is especially important for text and any UI elements. If the pixel dimensions of + * your UI scaled by this look appropriate on your machine then it should appear at a + * reasonable size on other machines regardless of their DPI and scaling settings. This + * relies on the system DPI and scaling settings being somewhat correct. + * + * @param store A vector2f to store the result + * @return The window content scale + * @see Window content scale + */ + public Vector2f getWindowContentScale(Vector2f store) { + if (store == null) store = new Vector2f(); + + glfwGetFramebufferSize(window, width, height); + store.set(width[0], height[0]); + + glfwGetWindowSize(window, width, height); + store.x /= width[0]; + store.y /= height[0]; + + return store; + } + + /** + * Returns the height of the framebuffer. + * + * @return the height (in pixels) + */ + @Override + public int getFramebufferHeight() { + glfwGetFramebufferSize(window, width, height); + int result = height[0]; + return result; + } + + /** + * Returns the width of the framebuffer. + * + * @return the width (in pixels) + */ + @Override + public int getFramebufferWidth() { + glfwGetFramebufferSize(window, width, height); + int result = width[0]; + return result; + } + + /** + * Returns the screen X coordinate of the left edge of the content area. + * + * @return the screen X coordinate + */ + @Override + public int getWindowXPosition() { + glfwGetWindowPos(window, width, height); + int result = width[0]; + return result; + } + + /** + * Returns the screen Y coordinate of the top edge of the content area. + * + * @return the screen Y coordinate + */ + @Override + public int getWindowYPosition() { + glfwGetWindowPos(window, width, height); + int result = height[0]; + return result; + } +} diff --git a/jme3-xr/src/main/java/com/jme3/system/lwjgl/openxr/HelloOpenXRGL.java b/jme3-xr/src/main/java/com/jme3/system/lwjgl/openxr/HelloOpenXRGL.java new file mode 100644 index 0000000000..501e744ffe --- /dev/null +++ b/jme3-xr/src/main/java/com/jme3/system/lwjgl/openxr/HelloOpenXRGL.java @@ -0,0 +1,886 @@ +/* + * Copyright LWJGL. All rights reserved. + * License terms: https://www.lwjgl.org/license + * Source: https://github.com/LWJGL/lwjgl3/tree/master/modules/samples/src/test/java/org/lwjgl/demo/openxr + */ +package com.jme3.system.lwjgl.openxr; + +import org.joml.*; +import org.lwjgl.*; +import org.lwjgl.opengl.*; +import org.lwjgl.openxr.*; +import org.lwjgl.system.*; + +import com.jme3.input.xr.XrHmd; +import com.jme3.system.AppSettings; + +import java.nio.*; +import java.util.*; +import java.util.logging.Logger; + +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.opengl.GL20.*; +import static org.lwjgl.opengl.GL30.*; +import static org.lwjgl.openxr.EXTDebugUtils.*; +import static org.lwjgl.openxr.KHROpenGLEnable.*; +import static org.lwjgl.openxr.MNDXEGLEnable.*; +import static org.lwjgl.openxr.XR10.*; +import static org.lwjgl.system.MemoryStack.*; +import static org.lwjgl.system.MemoryUtil.*; + +public class HelloOpenXRGL { + + private static final Logger logger = Logger.getLogger(HelloOpenXRGL.class.getName()); + + long window; + AppSettings appSettings; + + //XR globals + //Init + XrInstance xrInstance; + long systemID; + XrSession xrSession; + boolean missingXrDebug; + boolean useEglGraphicsBinding; + XrDebugUtilsMessengerEXT xrDebugMessenger; + XrSpace xrAppSpace; //The real world space in which the program runs + long glColorFormat; + XrView.Buffer views; //Each view reperesents an eye in the headset with views[0] being left and views[1] being right + Swapchain[] swapchains; //One swapchain per view + XrViewConfigurationView.Buffer viewConfigs; + int viewConfigType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; + + //Runtime + XrEventDataBuffer eventDataBuffer; + int sessionState; + boolean sessionRunning; + + //GL globals + Map depthTextures; //Swapchain images only provide a color texture so we have to create depth textures seperatley + + int swapchainFramebuffer; + int cubeVertexBuffer; + int cubeIndexBuffer; + int quadVertexBuffer; + int cubeVAO; + int quadVAO; + int screenShader; + int textureShader; + int colorShader; + XrHmd xrHmd; + static class Swapchain { + XrSwapchain handle; + int width; + int height; + XrSwapchainImageOpenGLKHR.Buffer images; + } + + //JME3: Split function main(args) to public functions constr+init+render+destroy + public HelloOpenXRGL(AppSettings appSettings) + { + this.appSettings = appSettings; + createOpenXRInstance(); + initializeOpenXRSystem(); + initializeAndBindOpenGL(); + createXRReferenceSpace(); + createXRSwapchains(); + createOpenGLResources(); + eventDataBuffer = XrEventDataBuffer.calloc() + .type$Default(); + } + + /** Returns true for continue */ + public boolean renderFrame() + { + if (pollEvents()) return false; + if (glfwWindowShouldClose(window)) return false; + if (sessionRunning) + { + try { + renderFrameOpenXR(); + } + catch (IllegalStateException e) + { + return false; + } + } + else + { + // Throttle loop since xrWaitFrame won't be called. + try + { + Thread.sleep(250); + } + catch (InterruptedException e) + { + e.printStackTrace(); + return false; + } + } + return true; + } + + public void destroy() + { + glFinish(); + + // Destroy OpenXR + eventDataBuffer.free(); + views.free(); + viewConfigs.free(); + for (Swapchain swapchain : swapchains) { + xrDestroySwapchain(swapchain.handle); + swapchain.images.free(); + } + + xrDestroySpace(xrAppSpace); + if (xrDebugMessenger != null) { + xrDestroyDebugUtilsMessengerEXT(xrDebugMessenger); + } + xrDestroySession(xrSession); + xrDestroyInstance(xrInstance); + + //Destroy OpenGL + for (int texture : depthTextures.values()) { + glDeleteTextures(texture); + } + glDeleteFramebuffers(swapchainFramebuffer); + glDeleteBuffers(cubeVertexBuffer); + glDeleteBuffers(cubeIndexBuffer); + glDeleteBuffers(quadVertexBuffer); + glDeleteVertexArrays(cubeVAO); + glDeleteVertexArrays(quadVAO); + glDeleteProgram(screenShader); + glDeleteProgram(textureShader); + glDeleteProgram(colorShader); + + glfwTerminate(); + } + + //JME3: New public functions for positions/orientations + public void getViewPosition(com.jme3.math.Vector3f store) + { + store.set(viewPos); + } + public void getViewRotation(com.jme3.math.Quaternion store) + { + store.set(viewRot); + } + public void getRenderSize(com.jme3.math.Vector2f store) + { + if (swapchains == null || swapchains.length == 0 || swapchains[0] == null) + { + logger.warning("XR is not ready. Returning default renderSize of 1512x1680"); + store.set(1512, 1680); + } + store.set(swapchains[0].width,swapchains[0].height); + } + public long getWindow() { return window; } + public void setHmd(XrHmd xrHmd) { this.xrHmd = xrHmd; } + + private void createOpenXRInstance() { + try (MemoryStack stack = stackPush()) { + IntBuffer pi = stack.mallocInt(1); + + check(xrEnumerateInstanceExtensionProperties((ByteBuffer)null, pi, null)); + int numExtensions = pi.get(0); + + XrExtensionProperties.Buffer properties = XRHelper.prepareExtensionProperties(stack, numExtensions); + + check(xrEnumerateInstanceExtensionProperties((ByteBuffer)null, pi, properties)); + + System.out.printf("OpenXR loaded with %d extensions:%n", numExtensions); + System.out.println("~~~~~~~~~~~~~~~~~~"); + + boolean missingOpenGL = true; + missingXrDebug = true; + + useEglGraphicsBinding = false; + for (int i = 0; i < numExtensions; i++) { + XrExtensionProperties prop = properties.get(i); + + String extensionName = prop.extensionNameString(); + System.out.println(extensionName); + + if (extensionName.equals(XR_KHR_OPENGL_ENABLE_EXTENSION_NAME)) { + missingOpenGL = false; + } + if (extensionName.equals(XR_EXT_DEBUG_UTILS_EXTENSION_NAME)) { + missingXrDebug = false; + } + if (extensionName.equals(XR_MNDX_EGL_ENABLE_EXTENSION_NAME)) { + useEglGraphicsBinding = true; + } + } + + if (missingOpenGL) { + throw new IllegalStateException("OpenXR library does not provide required extension: " + XR_KHR_OPENGL_ENABLE_EXTENSION_NAME); + } + + if (useEglGraphicsBinding) { + System.out.println("Going to use cross-platform experimental EGL for session creation"); + } else { + System.out.println("Going to use platform-specific session creation"); + } + + PointerBuffer extensions = stack.mallocPointer(2); + extensions.put(stack.UTF8(XR_KHR_OPENGL_ENABLE_EXTENSION_NAME)); + if (useEglGraphicsBinding) { + extensions.put(stack.UTF8(XR_MNDX_EGL_ENABLE_EXTENSION_NAME)); + } else if (!missingXrDebug) { + // At the time of writing this, the OpenXR validation layers don't like EGL + extensions.put(stack.UTF8(XR_EXT_DEBUG_UTILS_EXTENSION_NAME)); + } + extensions.flip(); + System.out.println("~~~~~~~~~~~~~~~~~~"); + + boolean useValidationLayer = false; + + check(xrEnumerateApiLayerProperties(pi, null)); + int numLayers = pi.get(0); + + XrApiLayerProperties.Buffer pLayers = XRHelper.prepareApiLayerProperties(stack, numLayers); + check(xrEnumerateApiLayerProperties(pi, pLayers)); + System.out.println(numLayers + " XR layers are available:"); + for (int index = 0; index < numLayers; index++) { + XrApiLayerProperties layer = pLayers.get(index); + + String layerName = layer.layerNameString(); + System.out.println(layerName); + + // At the time of wring this, the OpenXR validation layers don't like EGL + if (!useEglGraphicsBinding && layerName.equals("XR_APILAYER_LUNARG_core_validation")) { + useValidationLayer = true; + } + } + System.out.println("-----------"); + + PointerBuffer wantedLayers; + if (useValidationLayer) { + wantedLayers = stack.callocPointer(1); + wantedLayers.put(0, stack.UTF8("XR_APILAYER_LUNARG_core_validation")); + System.out.println("Enabling XR core validation"); + } else { + System.out.println("Running without validation layers"); + wantedLayers = null; + } + + XrInstanceCreateInfo createInfo = XrInstanceCreateInfo.malloc(stack) + .type$Default() + .next(NULL) + .createFlags(0) + .applicationInfo(XrApplicationInfo.calloc(stack) + .applicationName(stack.UTF8("HelloOpenXR")) + .apiVersion(XR_CURRENT_API_VERSION)) + .enabledApiLayerNames(wantedLayers) + .enabledExtensionNames(extensions); + + PointerBuffer pp = stack.mallocPointer(1); + System.out.println("Creating OpenXR instance..."); + check(xrCreateInstance(createInfo, pp)); + xrInstance = new XrInstance(pp.get(0), createInfo); + System.out.println("Created OpenXR instance"); + } + } + + public void initializeOpenXRSystem() { + try (MemoryStack stack = stackPush()) { + //Get headset + LongBuffer pl = stack.longs(0); + + check(xrGetSystem( + xrInstance, + XrSystemGetInfo.malloc(stack) + .type$Default() + .next(NULL) + .formFactor(XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY), + pl + )); + + systemID = pl.get(0); + if (systemID == 0) { + throw new IllegalStateException("No compatible headset detected"); + } + System.out.printf("Headset found with System ID: %d\n", systemID); + } + } + + private void initializeAndBindOpenGL() { + try (MemoryStack stack = stackPush()) { + //Initialize OpenXR's OpenGL compatability + XrGraphicsRequirementsOpenGLKHR graphicsRequirements = XrGraphicsRequirementsOpenGLKHR.malloc(stack) + .type$Default() + .next(NULL) + .minApiVersionSupported(0) + .maxApiVersionSupported(0); + + xrGetOpenGLGraphicsRequirementsKHR(xrInstance, systemID, graphicsRequirements); + + int minMajorVersion = XR_VERSION_MAJOR(graphicsRequirements.minApiVersionSupported()); + int minMinorVersion = XR_VERSION_MINOR(graphicsRequirements.minApiVersionSupported()); + + int maxMajorVersion = XR_VERSION_MAJOR(graphicsRequirements.maxApiVersionSupported()); + int maxMinorVersion = XR_VERSION_MINOR(graphicsRequirements.maxApiVersionSupported()); + + System.out.println("The OpenXR runtime supports OpenGL " + minMajorVersion + "." + minMinorVersion + + " to OpenGL " + maxMajorVersion + "." + maxMinorVersion); + + // This example needs at least OpenGL 4.0 + if (maxMajorVersion < 4) { + throw new UnsupportedOperationException("This example requires at least OpenGL 4.0"); + } + int majorVersionToRequest = 4; + int minorVersionToRequest = 0; + + // But when the OpenXR runtime requires a later version, we should respect that. + // As a matter of fact, the runtime on my current laptop does, so this code is actually needed. + if (minMajorVersion == 4) { + minorVersionToRequest = 5; + } + + //Init glfw + if (!glfwInit()) { + throw new IllegalStateException("Failed to initialize GLFW."); + } + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, majorVersionToRequest); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, minorVersionToRequest); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_DOUBLEBUFFER, GL_FALSE); + if (useEglGraphicsBinding) { + glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API); + } + window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL); + glfwMakeContextCurrent(window); + GL.createCapabilities(); + + // Check if OpenGL version is supported by OpenXR runtime + int actualMajorVersion = glGetInteger(GL_MAJOR_VERSION); + int actualMinorVersion = glGetInteger(GL_MINOR_VERSION); + + if (minMajorVersion > actualMajorVersion || (minMajorVersion == actualMajorVersion && minMinorVersion > actualMinorVersion)) { + throw new IllegalStateException( + "The OpenXR runtime supports only OpenGL " + minMajorVersion + "." + minMinorVersion + + " and later, but we got OpenGL " + actualMajorVersion + "." + actualMinorVersion + ); + } + + if (actualMajorVersion > maxMajorVersion || (actualMajorVersion == maxMajorVersion && actualMinorVersion > maxMinorVersion)) { + throw new IllegalStateException( + "The OpenXR runtime supports only OpenGL " + maxMajorVersion + "." + minMajorVersion + + " and earlier, but we got OpenGL " + actualMajorVersion + "." + actualMinorVersion + ); + } + logger.info("OnCreateXr: Win=" + window + " Egl=" + useEglGraphicsBinding); + + //Bind the OpenGL context to the OpenXR instance and create the session + PointerBuffer pp = stack.mallocPointer(1); + check(xrCreateSession( + xrInstance, + XRHelper.createGraphicsBindingOpenGL( + XrSessionCreateInfo.malloc(stack) + .type$Default() + .next(NULL) + .createFlags(0) + .systemId(systemID), + stack, + window, + useEglGraphicsBinding + ), + pp + )); + + xrSession = new XrSession(pp.get(0), xrInstance); + + if (!missingXrDebug && !useEglGraphicsBinding) { + XrDebugUtilsMessengerCreateInfoEXT ciDebugUtils = XrDebugUtilsMessengerCreateInfoEXT.calloc(stack) + .type$Default() + .messageSeverities( + XR_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT | + XR_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | + XR_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT + ) + .messageTypes( + XR_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | + XR_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | + XR_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT | + XR_DEBUG_UTILS_MESSAGE_TYPE_CONFORMANCE_BIT_EXT + ) + .userCallback((messageSeverity, messageTypes, pCallbackData, userData) -> { + XrDebugUtilsMessengerCallbackDataEXT callbackData = XrDebugUtilsMessengerCallbackDataEXT.create(pCallbackData); + System.out.println("XR Debug Utils: " + callbackData.messageString()); + return 0; + }); + + System.out.println("Enabling OpenXR debug utils"); + check(xrCreateDebugUtilsMessengerEXT(xrInstance, ciDebugUtils, pp)); + xrDebugMessenger = new XrDebugUtilsMessengerEXT(pp.get(0), xrInstance); + } + } + } + + public void createXRReferenceSpace() { + try (MemoryStack stack = stackPush()) { + PointerBuffer pp = stack.mallocPointer(1); + + check(xrCreateReferenceSpace( + xrSession, + XrReferenceSpaceCreateInfo.malloc(stack) + .type$Default() + .next(NULL) + .referenceSpaceType(XR_REFERENCE_SPACE_TYPE_LOCAL) + .poseInReferenceSpace(XrPosef.malloc(stack) + .orientation(XrQuaternionf.malloc(stack) + .x(0) + .y(0) + .z(0) + .w(1)) + .position$(XrVector3f.calloc(stack))), + pp + )); + + xrAppSpace = new XrSpace(pp.get(0), xrSession); + } + } + + public void createXRSwapchains() { + try (MemoryStack stack = stackPush()) { + XrSystemProperties systemProperties = XrSystemProperties.calloc(stack) + .type$Default(); + check(xrGetSystemProperties(xrInstance, systemID, systemProperties)); + + System.out.printf("Headset name:%s vendor:%d \n", + memUTF8(memAddress(systemProperties.systemName())), + systemProperties.vendorId()); + + XrSystemTrackingProperties trackingProperties = systemProperties.trackingProperties(); + System.out.printf("Headset orientationTracking:%b positionTracking:%b \n", + trackingProperties.orientationTracking(), + trackingProperties.positionTracking()); + + XrSystemGraphicsProperties graphicsProperties = systemProperties.graphicsProperties(); + System.out.printf("Headset MaxWidth:%d MaxHeight:%d MaxLayerCount:%d \n", + graphicsProperties.maxSwapchainImageWidth(), + graphicsProperties.maxSwapchainImageHeight(), + graphicsProperties.maxLayerCount()); + + IntBuffer pi = stack.mallocInt(1); + + check(xrEnumerateViewConfigurationViews(xrInstance, systemID, viewConfigType, pi, null)); + viewConfigs = XRHelper.fill( + XrViewConfigurationView.calloc(pi.get(0)), + XrViewConfigurationView.TYPE, + XR_TYPE_VIEW_CONFIGURATION_VIEW + ); + + check(xrEnumerateViewConfigurationViews(xrInstance, systemID, viewConfigType, pi, viewConfigs)); + int viewCountNumber = pi.get(0); + + views = XRHelper.fill( + XrView.calloc(viewCountNumber), + XrView.TYPE, + XR_TYPE_VIEW + ); + + if (viewCountNumber > 0) { + check(xrEnumerateSwapchainFormats(xrSession, pi, null)); + LongBuffer swapchainFormats = stack.mallocLong(pi.get(0)); + check(xrEnumerateSwapchainFormats(xrSession, pi, swapchainFormats)); + + long[] desiredSwapchainFormats = { + GL_RGB10_A2, + GL_RGBA16F, + // The two below should only be used as a fallback, as they are linear color formats without enough bits for color + // depth, thus leading to banding. + GL_RGBA8, + GL31.GL_RGBA8_SNORM + }; + + out: + for (long glFormatIter : desiredSwapchainFormats) { + for (int i = 0; i < swapchainFormats.limit(); i++) { + if (glFormatIter == swapchainFormats.get(i)) { + glColorFormat = glFormatIter; + break out; + } + } + } + + if (glColorFormat == 0) { + throw new IllegalStateException("No compatable swapchain / framebuffer format availible"); + } + + swapchains = new Swapchain[viewCountNumber]; + for (int i = 0; i < viewCountNumber; i++) { + XrViewConfigurationView viewConfig = viewConfigs.get(i); + + Swapchain swapchainWrapper = new Swapchain(); + + XrSwapchainCreateInfo swapchainCreateInfo = XrSwapchainCreateInfo.malloc(stack) + .type$Default() + .next(NULL) + .createFlags(0) + .usageFlags(XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT) + .format(glColorFormat) + .sampleCount(viewConfig.recommendedSwapchainSampleCount()) + .width(viewConfig.recommendedImageRectWidth()) + .height(viewConfig.recommendedImageRectHeight()) + .faceCount(1) + .arraySize(1) + .mipCount(1); + + System.out.printf("Headset Eye:%d has Width:%d Height:%d\n", + i, + viewConfig.recommendedImageRectWidth(), + viewConfig.recommendedImageRectHeight()); + appSettings.setWidth(viewConfig.recommendedImageRectWidth()); + appSettings.setHeight(viewConfig.recommendedImageRectHeight()); + PointerBuffer pp = stack.mallocPointer(1); + check(xrCreateSwapchain(xrSession, swapchainCreateInfo, pp)); + + swapchainWrapper.handle = new XrSwapchain(pp.get(0), xrSession); + swapchainWrapper.width = swapchainCreateInfo.width(); + swapchainWrapper.height = swapchainCreateInfo.height(); + + check(xrEnumerateSwapchainImages(swapchainWrapper.handle, pi, null)); + int imageCount = pi.get(0); + + XrSwapchainImageOpenGLKHR.Buffer swapchainImageBuffer = XRHelper.fill( + XrSwapchainImageOpenGLKHR.calloc(imageCount), + XrSwapchainImageOpenGLKHR.TYPE, + XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR + ); + + check(xrEnumerateSwapchainImages(swapchainWrapper.handle, pi, XrSwapchainImageBaseHeader.create(swapchainImageBuffer))); + swapchainWrapper.images = swapchainImageBuffer; + swapchains[i] = swapchainWrapper; + } + } + } + } + + private void createOpenGLResources() { + swapchainFramebuffer = glGenFramebuffers(); + depthTextures = new HashMap<>(0); + for (Swapchain swapchain : swapchains) { + for (XrSwapchainImageOpenGLKHR swapchainImage : swapchain.images) { + int texture = glGenTextures(); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32, swapchain.width, swapchain.height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, (ByteBuffer)null); + depthTextures.put(swapchainImage, texture); + } + } + glBindTexture(GL_TEXTURE_2D, 0); + } + + private boolean pollEvents() { + glfwPollEvents(); + XrEventDataBaseHeader event = readNextOpenXREvent(); + if (event == null) { + return false; + } + + do { + switch (event.type()) { + case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING: { + XrEventDataInstanceLossPending instanceLossPending = XrEventDataInstanceLossPending.create(event); + System.err.printf("XrEventDataInstanceLossPending by %d\n", instanceLossPending.lossTime()); + //*requestRestart = true; + return true; + } + case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: { + XrEventDataSessionStateChanged sessionStateChangedEvent = XrEventDataSessionStateChanged.create(event); + return OpenXRHandleSessionStateChangedEvent(sessionStateChangedEvent/*, requestRestart*/); + } + case XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED: + break; + case XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING: + default: { + System.out.printf("Ignoring event type %d\n", event.type()); + break; + } + } + event = readNextOpenXREvent(); + } + while (event != null); + + return false; + } + + private XrEventDataBaseHeader readNextOpenXREvent() { + // It is sufficient to just clear the XrEventDataBuffer header to + // XR_TYPE_EVENT_DATA_BUFFER rather than recreate it every time + eventDataBuffer.clear(); + eventDataBuffer.type$Default(); + int result = xrPollEvent(xrInstance, eventDataBuffer); + if (result == XR_SUCCESS) { + XrEventDataBaseHeader header = XrEventDataBaseHeader.create(eventDataBuffer.address()); + if (header.type() == XR_TYPE_EVENT_DATA_EVENTS_LOST) { + XrEventDataEventsLost dataEventsLost = XrEventDataEventsLost.create(header); + System.out.printf("%d events lost\n", dataEventsLost.lostEventCount()); + } + return header; + } + if (result == XR_EVENT_UNAVAILABLE) { + return null; + } + throw new IllegalStateException(String.format("[XrResult failure %d in xrPollEvent]", result)); + } + + boolean OpenXRHandleSessionStateChangedEvent(XrEventDataSessionStateChanged stateChangedEvent) { + int oldState = sessionState; + sessionState = stateChangedEvent.state(); + + System.out.printf("XrEventDataSessionStateChanged: state %s->%s session=%d time=%d\n", oldState, sessionState, stateChangedEvent.session(), stateChangedEvent.time()); + + if ((stateChangedEvent.session() != NULL) && (stateChangedEvent.session() != xrSession.address())) { + System.err.println("XrEventDataSessionStateChanged for unknown session"); + return false; + } + + switch (sessionState) { + case XR_SESSION_STATE_READY: { + assert (xrSession != null); + try (MemoryStack stack = stackPush()) { + check(xrBeginSession( + xrSession, + XrSessionBeginInfo.malloc(stack) + .type$Default() + .next(NULL) + .primaryViewConfigurationType(viewConfigType) + )); + sessionRunning = true; + return false; + } + } + case XR_SESSION_STATE_STOPPING: { + assert (xrSession != null); + sessionRunning = false; + check(xrEndSession(xrSession)); + return false; + } + case XR_SESSION_STATE_EXITING: { + // Do not attempt to restart because user closed this session. + //*requestRestart = false; + return true; + } + case XR_SESSION_STATE_LOSS_PENDING: { + // Poll for a new instance. + //*requestRestart = true; + return true; + } + default: + return false; + } + } + + private void renderFrameOpenXR() { + try (MemoryStack stack = stackPush()) { + XrFrameState frameState = XrFrameState.calloc(stack) + .type$Default(); + + check(xrWaitFrame( + xrSession, + XrFrameWaitInfo.calloc(stack) + .type$Default(), + frameState + )); + + check(xrBeginFrame( + xrSession, + XrFrameBeginInfo.calloc(stack) + .type$Default() + )); + + XrCompositionLayerProjection layerProjection = XrCompositionLayerProjection.calloc(stack) + .type$Default(); + + PointerBuffer layers = stack.callocPointer(1); + + boolean didRender = false; + if (frameState.shouldRender()) { + if (renderLayerOpenXR(stack, frameState.predictedDisplayTime(), layerProjection)) { + layers.put(0, layerProjection); + didRender = true; + } else { + System.out.println("Didn't render"); + } + } else { + System.out.println("Shouldn't render"); + } + + check(xrEndFrame( + xrSession, + XrFrameEndInfo.malloc(stack) + .type$Default() + .next(NULL) + .displayTime(frameState.predictedDisplayTime()) + .environmentBlendMode(XR_ENVIRONMENT_BLEND_MODE_OPAQUE) + .layers(didRender ? layers : null) + .layerCount(didRender ? layers.remaining() : 0) + )); + } + } + + private boolean renderLayerOpenXR(MemoryStack stack, long predictedDisplayTime, XrCompositionLayerProjection layer) { + XrViewState viewState = XrViewState.calloc(stack) + .type$Default(); + + IntBuffer pi = stack.mallocInt(1); + check(xrLocateViews( + xrSession, + XrViewLocateInfo.malloc(stack) + .type$Default() + .next(NULL) + .viewConfigurationType(viewConfigType) + .displayTime(predictedDisplayTime) + .space(xrAppSpace), + viewState, + pi, + views + )); + + if ((viewState.viewStateFlags() & XR_VIEW_STATE_POSITION_VALID_BIT) == 0 || + (viewState.viewStateFlags() & XR_VIEW_STATE_ORIENTATION_VALID_BIT) == 0) { + return false; // There is no valid tracking poses for the views. + } + + int viewCountOutput = pi.get(0); + assert (viewCountOutput == views.capacity()); + assert (viewCountOutput == viewConfigs.capacity()); + assert (viewCountOutput == swapchains.length); + + XrCompositionLayerProjectionView.Buffer projectionLayerViews = XRHelper.fill( + XrCompositionLayerProjectionView.calloc(viewCountOutput, stack), + XrCompositionLayerProjectionView.TYPE, + XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW + ); + + // Render view to the appropriate part of the swapchain image. + for (int viewIndex = 0; viewIndex < viewCountOutput; viewIndex++) { + // Each view has a separate swapchain which is acquired, rendered to, and released. + Swapchain viewSwapchain = swapchains[viewIndex]; + + check(xrAcquireSwapchainImage( + viewSwapchain.handle, + XrSwapchainImageAcquireInfo.calloc(stack) + .type$Default(), + pi + )); + int swapchainImageIndex = pi.get(0); + + check(xrWaitSwapchainImage( + viewSwapchain.handle, + XrSwapchainImageWaitInfo.malloc(stack) + .type$Default() + .next(NULL) + .timeout(XR_INFINITE_DURATION) + )); + + XrCompositionLayerProjectionView projectionLayerView = projectionLayerViews.get(viewIndex) + .pose(views.get(viewIndex).pose()) + .fov(views.get(viewIndex).fov()) + .subImage(si -> si + .swapchain(viewSwapchain.handle) + .imageRect(rect -> rect + .offset(offset -> offset + .x(0) + .y(0)) + .extent(extent -> extent + .width(viewSwapchain.width) + .height(viewSwapchain.height) + ))); + + OpenGLRenderView(projectionLayerView, viewSwapchain.images.get(swapchainImageIndex), viewIndex); + + check(xrReleaseSwapchainImage( + viewSwapchain.handle, + XrSwapchainImageReleaseInfo.calloc(stack) + .type$Default() + )); + } + + layer.space(xrAppSpace); + layer.views(projectionLayerViews); + return true; + } + + private static Matrix4f projectionMatrix = new Matrix4f(); //TODO: Do we need this? + private static Matrix4f viewMatrix = new Matrix4f(); //TODO: Do we need this? + private static com.jme3.math.Vector3f viewPos = new com.jme3.math.Vector3f(); + private static com.jme3.math.Quaternion viewRot = new com.jme3.math.Quaternion(); + + private void OpenGLRenderView(XrCompositionLayerProjectionView layerView, XrSwapchainImageOpenGLKHR swapchainImage, int viewIndex) { + glBindFramebuffer(GL_FRAMEBUFFER, swapchainFramebuffer); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, swapchainImage.image(), 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTextures.get(swapchainImage), 0); + + XrRect2Di imageRect = layerView.subImage().imageRect(); + glViewport( + imageRect.offset().x(), + imageRect.offset().y(), + imageRect.extent().width(), + imageRect.extent().height() + ); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + + glFrontFace(GL_CCW); + glCullFace(GL_BACK); + glEnable(GL_DEPTH_TEST); + + XrPosef pose = layerView.pose(); + XrVector3f pos = pose.position$(); + XrQuaternionf orientation = pose.orientation(); + XRHelper.applyProjectionToMatrix(projectionMatrix.identity(), layerView.fov(), 0.1f, 100f, false); + viewMatrix.translationRotateScaleInvert( + pos.x(), pos.y(), pos.z(), + orientation.x(), orientation.y(), orientation.z(), orientation.w(), + 1, 1, 1 + ); + viewPos.set(pos.x(), pos.y(), pos.z()); + viewRot.set(orientation.x(), orientation.y(), orientation.z(), orientation.w()); + xrHmd.onUpdateHmdOrientation(viewPos, viewRot); + + glDisable(GL_CULL_FACE); // Disable back-face culling so we can see the inside of the world-space cube and backside of the plane + + if (viewIndex == 0) { xrHmd.getLeftEye().render(); } + else if (viewIndex == 1) { xrHmd.getRightEye().render(); } + + glEnable(GL_CULL_FACE); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + if (viewIndex == swapchains.length - 1) { + glFlush(); + } + } + + public void check(int result) throws IllegalStateException { + if (XR_SUCCEEDED(result)) { + return; + } + + if (xrInstance != null) { + ByteBuffer str = stackMalloc(XR_MAX_RESULT_STRING_SIZE); + if (xrResultToString(xrInstance, result, str) >= 0) { + throw new XrResultException(memUTF8(str, memLengthNT1(str))); + } + } + + throw new XrResultException("XR method returned " + result); + } + + @SuppressWarnings("serial") + public static class XrResultException extends RuntimeException { + public XrResultException(String s) { + super(s); + } + } + +} diff --git a/jme3-xr/src/main/java/com/jme3/system/lwjgl/openxr/XRHelper.java b/jme3-xr/src/main/java/com/jme3/system/lwjgl/openxr/XRHelper.java new file mode 100644 index 0000000000..4a523b87b9 --- /dev/null +++ b/jme3-xr/src/main/java/com/jme3/system/lwjgl/openxr/XRHelper.java @@ -0,0 +1,205 @@ +/* + * Copyright LWJGL. All rights reserved. + * License terms: https://www.lwjgl.org/license + * Source: https://github.com/LWJGL/lwjgl3/tree/master/modules/samples/src/test/java/org/lwjgl/demo/openxr + */ +package com.jme3.system.lwjgl.openxr; + +import org.joml.Math; +import org.joml.*; +import org.lwjgl.egl.*; +import org.lwjgl.openxr.*; +import org.lwjgl.system.*; +import org.lwjgl.system.linux.*; + +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.glfw.GLFWNativeEGL.*; +import static org.lwjgl.glfw.GLFWNativeGLX.*; +import static org.lwjgl.glfw.GLFWNativeWGL.*; +import static org.lwjgl.glfw.GLFWNativeWayland.*; +import static org.lwjgl.glfw.GLFWNativeWin32.*; +import static org.lwjgl.glfw.GLFWNativeX11.*; +import static org.lwjgl.opengl.GLX.*; +import static org.lwjgl.opengl.GLX13.*; +import static org.lwjgl.openxr.XR10.*; +import static org.lwjgl.system.MemoryUtil.*; +import static org.lwjgl.system.windows.User32.*; + +/** + * A helper class with some static methods to help applications with OpenXR related tasks that are cumbersome in + * some way. + */ +final class XRHelper { + + private XRHelper() { + } + + static > T fill(T buffer, int offset, int value) { + long ptr = buffer.address() + offset; + int stride = buffer.sizeof(); + for (int i = 0; i < buffer.limit(); i++) { + memPutInt(ptr + i * stride, value); + } + return buffer; + } + + /** + * Allocates an {@link XrApiLayerProperties.Buffer} onto the given stack with the given number of layers and + * sets the type of each element in the buffer to {@link XR10#XR_TYPE_API_LAYER_PROPERTIES XR_TYPE_API_LAYER_PROPERTIES}. + * + *

Note: you can't use the buffer after the stack is gone!

+ * + * @param stack the stack to allocate the buffer on + * @param numLayers the number of elements the buffer should get + * + * @return the created buffer + */ + static XrApiLayerProperties.Buffer prepareApiLayerProperties(MemoryStack stack, int numLayers) { + return fill( + XrApiLayerProperties.calloc(numLayers, stack), + XrApiLayerProperties.TYPE, + XR_TYPE_API_LAYER_PROPERTIES + ); + } + + /** + * Allocates an {@link XrExtensionProperties.Buffer} onto the given stack with the given number of extensions + * and sets the type of each element in the buffer to {@link XR10#XR_TYPE_EXTENSION_PROPERTIES XR_TYPE_EXTENSION_PROPERTIES}. + * + *

Note: you can't use the buffer after the stack is gone!

+ * + * @param stack the stack onto which to allocate the buffer + * @param numExtensions the number of elements the buffer should get + * + * @return the created buffer + */ + static XrExtensionProperties.Buffer prepareExtensionProperties(MemoryStack stack, int numExtensions) { + return fill( + XrExtensionProperties.calloc(numExtensions, stack), + XrExtensionProperties.TYPE, + XR_TYPE_EXTENSION_PROPERTIES + ); + } + + /** + * Applies an off-center asymmetric perspective projection transformation to the given {@link Matrix4f}, + * such that it represents a projection matrix with the given fov, nearZ (a.k.a. near plane), + * farZ (a.k.a. far plane). + * + * @param m The matrix to apply the perspective projection transformation to + * @param fov The desired Field of View for the projection matrix. You should normally use the value of + * {@link XrCompositionLayerProjectionView#fov}. + * @param nearZ The nearest Z value that the user should see (also known as the near plane) + * @param farZ The furthest Z value that the user should see (also known as far plane) + * @param zZeroToOne True if the z-axis of the coordinate system goes from 0 to 1 (Vulkan). + * False if the z-axis of the coordinate system goes from -1 to 1 (OpenGL). + * + * @return the provided matrix + */ + static Matrix4f applyProjectionToMatrix(Matrix4f m, XrFovf fov, float nearZ, float farZ, boolean zZeroToOne) { + float distToLeftPlane = Math.tan(fov.angleLeft()); + float distToRightPlane = Math.tan(fov.angleRight()); + float distToBottomPlane = Math.tan(fov.angleDown()); + float distToTopPlane = Math.tan(fov.angleUp()); + return m.frustum(distToLeftPlane * nearZ, distToRightPlane * nearZ, distToBottomPlane * nearZ, distToTopPlane * nearZ, nearZ, farZ, zZeroToOne); + } + + /** + * Appends the right XrGraphicsBinding** struct to the next chain of sessionCreateInfo. OpenGL + * session creation is poorly standardized in OpenXR, so the right graphics binding struct depends on the OS and + * the windowing system, among others. There are basically 4 graphics binding structs for this: + *
    + *
  • XrGraphicsBindingOpenGLWin32KHR, which can only be used on Windows computers.
  • + *
  • XrGraphicsBindingOpenGLXlibKHR, which can only be used on Linux computers with the X11 windowing system.
  • + *
  • + * XrGraphicsBindingOpenGLWaylandKHR, which can only be used on Linux computers with the + * Wayland windowing system. But, no OpenXR runtime has implemented the specification for this struct and + * the Wayland developers have claimed that the specification doesn't make sense and can't be implemented + * as such. For this reason, this method won't use this struct. + *
  • + *
  • + * XrGraphicsBindingEGLMNDX, which is cross-platform, but can only be used if the experimental + * OpenXR extension XR_MNDX_egl_enable is enabled. But, since the extension is experimental, it + * is not widely supported (at the time of writing this only by the Monado OpenXR runtime). Nevertheless, + * this is the only way to create an OpenXR session on the Wayland windowing system (or on systems + * without well-known windowing system). + *
  • + *
+ * + * The parameter useEGL determines which graphics binding struct this method will choose: + *
    + *
  • + * If useEGL is true, this method will use XrGraphicsBindingEGLMNDX. + * The caller must ensure that the extension XR_MNDX_egl_enable has been enabled. + *
  • + *
  • + * If useEGL is false, this method will try to use a platform-specific struct. + * If no such struct exists, it will throw an IllegalStateException. + *
  • + *
+ * + * @param sessionCreateInfo The XrSessionCreateInfo whose next chain should be populated + * @param stack The MemoryStack onto which this method should allocate the graphics binding struct + * @param window The GLFW window + * @param useEGL Whether this method should use XrGraphicsBindingEGLMNDX + * @return sessionCreateInfo (after appending a graphics binding to it) + * @throws IllegalStateException If the current OS and/or windowing system needs EGL, but useEGL is false + */ + static XrSessionCreateInfo createGraphicsBindingOpenGL( + XrSessionCreateInfo sessionCreateInfo, MemoryStack stack, long window, boolean useEGL + ) throws IllegalStateException { + if (useEGL) { + System.out.println("Using XrGraphicsBindingEGLMNDX to create the session..."); + return sessionCreateInfo.next( + XrGraphicsBindingEGLMNDX.malloc(stack) + .type$Default() + .next(NULL) + .getProcAddress(EGL.getCapabilities().eglGetProcAddress) + .display(glfwGetEGLDisplay()) + .config(glfwGetEGLConfig(window)) + .context(glfwGetEGLContext(window)) + ); + } + switch (Platform.get()) { + case LINUX: + int platform = glfwGetPlatform(); + if (platform == GLFW_PLATFORM_X11) { + long display = glfwGetX11Display(); + long glxConfig = glfwGetGLXFBConfig(window); + + XVisualInfo visualInfo = glXGetVisualFromFBConfig(display, glxConfig); + if (visualInfo == null) { + throw new IllegalStateException("Failed to get visual info"); + } + long visualid = visualInfo.visualid(); + + System.out.println("Using XrGraphicsBindingOpenGLXlibKHR to create the session"); + return sessionCreateInfo.next( + XrGraphicsBindingOpenGLXlibKHR.malloc(stack) + .type$Default() + .xDisplay(display) + .visualid((int)visualid) + .glxFBConfig(glxConfig) + .glxDrawable(glXGetCurrentDrawable()) + .glxContext(glfwGetGLXContext(window)) + ); + } else { + throw new IllegalStateException( + "X11 is the only Linux windowing system with explicit OpenXR support. All other Linux systems must use EGL." + ); + } + case WINDOWS: + System.out.println("Using XrGraphicsBindingOpenGLWin32KHR to create the session"); + return sessionCreateInfo.next( + XrGraphicsBindingOpenGLWin32KHR.malloc(stack) + .type$Default() + .hDC(GetDC(glfwGetWin32Window(window))) + .hGLRC(glfwGetWGLContext(window)) + ); + default: + throw new IllegalStateException( + "Windows and Linux are the only platforms with explicit OpenXR support. All other platforms must use EGL." + ); + } + } +} diff --git a/settings.gradle b/settings.gradle index 3f6acceb8e..f12c54f4fc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,7 @@ include 'jme3-lwjgl' if (JavaVersion.current().isJava8Compatible()) { include 'jme3-lwjgl3' include 'jme3-vr' + include 'jme3-xr' } // Other external dependencies