/* * Copyright (c) 2009-2012 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.input.lwjgl.GlfwKeyInput; import com.jme3.input.lwjgl.GlfwMouseInput; import com.jme3.system.AppSettings; import com.jme3.system.JmeContext; import com.jme3.system.JmeSystem; import org.lwjgl.Sys; import org.lwjgl.glfw.*; import java.awt.*; import java.awt.image.BufferedImage; import java.nio.ByteBuffer; 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.opengl.GL11.GL_FALSE; import static org.lwjgl.opengl.GL11.GL_TRUE; import static org.lwjgl.system.MemoryUtil.NULL; /** * A wrapper class over the GLFW framework in LWJGL 3. * * @author Daniel Johansson */ public abstract class LwjglWindow extends LwjglContext implements Runnable { private static final Logger LOGGER = Logger.getLogger(LwjglWindow.class.getName()); protected AtomicBoolean needClose = new AtomicBoolean(false); protected final AtomicBoolean needRestart = new AtomicBoolean(false); protected boolean wasActive = false; protected boolean autoFlush = true; protected boolean allowSwapBuffers = false; private long window = -1; private final JmeContext.Type type; private GLFWErrorCallback errorCallback; private GLFWWindowSizeCallback windowSizeCallback; private GLFWWindowFocusCallback windowFocusCallback; public LwjglWindow(final JmeContext.Type type) { if (!JmeContext.Type.Display.equals(type) && !JmeContext.Type.OffscreenSurface.equals(type) && !JmeContext.Type.Canvas.equals(type)) { throw new IllegalArgumentException("Unsupported type '" + type.name() + "' provided"); } this.type = type; } /** * @return Type.Display or Type.Canvas */ public JmeContext.Type getType() { return type; } /** * Set the title if its a windowed display * * @param title */ public void setTitle(final String title) { if (created.get() && window != -1) { glfwSetWindowTitle(window, title); } } /** * Restart if its a windowed or full-screen display. */ 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 */ protected void createContext(final AppSettings settings) { glfwSetErrorCallback(errorCallback = new GLFWErrorCallback() { @Override public void invoke(int error, long description) { final String message = Callbacks.errorCallbackDescriptionString(description); listener.handleError(message, new Exception(message)); } }); if (glfwInit() != GL_TRUE) { throw new IllegalStateException("Unable to initialize GLFW"); } glfwDefaultWindowHints(); glfwWindowHint(GLFW_VISIBLE, GL_FALSE); // TODO: Add support for monitor selection long monitor = NULL; if (settings.isFullscreen()) { monitor = glfwGetPrimaryMonitor(); } final ByteBuffer videoMode = glfwGetVideoMode(glfwGetPrimaryMonitor()); if (settings.getWidth() <= 0 || settings.getHeight() <= 0) { settings.setResolution(GLFWvidmode.width(videoMode), GLFWvidmode.height(videoMode)); } window = glfwCreateWindow(settings.getWidth(), settings.getHeight(), settings.getTitle(), monitor, NULL); if (window == NULL) { throw new RuntimeException("Failed to create the GLFW window"); } glfwWindowHint(GLFW_RESIZABLE, settings.isResizable() ? GL_TRUE : GL_FALSE); glfwWindowHint(GLFW_DEPTH_BITS, settings.getDepthBits()); glfwWindowHint(GLFW_STENCIL_BITS, settings.getStencilBits()); glfwWindowHint(GLFW_SAMPLES, settings.getSamples()); glfwWindowHint(GLFW_STEREO, settings.useStereo3D() ? GL_TRUE : GL_FALSE); int frameRateCap = settings.getFrameRate(); if (!autoFlush) { frameRateCap = 20; } if (frameRateCap > 0) { glfwWindowHint(GLFW_REFRESH_RATE, frameRateCap); } // Not sure how else to support bits per pixel if (settings.getBitsPerPixel() == 24) { glfwWindowHint(GLFW_RED_BITS, 8); glfwWindowHint(GLFW_GREEN_BITS, 8); glfwWindowHint(GLFW_BLUE_BITS, 8); } else if (settings.getBitsPerPixel() == 16) { glfwWindowHint(GLFW_RED_BITS, 5); glfwWindowHint(GLFW_GREEN_BITS, 6); glfwWindowHint(GLFW_BLUE_BITS, 5); } glfwWindowHint(GLFW_ALPHA_BITS, settings.getAlphaBits()); glfwSetWindowFocusCallback(window, windowFocusCallback = new GLFWWindowFocusCallback() { @Override public void invoke(final long window, final int focused) { final boolean focus = (focused == GL_TRUE); if (wasActive != focus) { if (!wasActive) { listener.gainFocus(); timer.reset(); } else { listener.loseFocus(); } wasActive = !wasActive; } } }); // Center the window if (!settings.isFullscreen() && Type.Display.equals(type)) { glfwSetWindowPos(window, (GLFWvidmode.width(videoMode) - settings.getWidth()) / 2, (GLFWvidmode.height(videoMode) - settings.getHeight()) / 2); } // Make the OpenGL context current glfwMakeContextCurrent(window); // Enable vsync if (settings.isVSync()) { glfwSwapInterval(1); } else { glfwSwapInterval(0); } // Make the window visible if (Type.Display.equals(type)) { glfwShowWindow(window); } // Add a resize callback which delegates to the listener glfwSetWindowSizeCallback(window, windowSizeCallback = new GLFWWindowSizeCallback() { @Override public void invoke(final long window, final int width, final int height) { settings.setResolution(width, height); listener.reshape(width, height); } }); allowSwapBuffers = settings.isSwapBuffers(); // TODO: When GLFW 3.2 is released and included in LWJGL 3.x then we should hopefully be able to set the window icon. } /** * Destroy the context. */ protected void destroyContext() { try { if (renderer != null) { renderer.cleanup(); } errorCallback.release(); windowSizeCallback.release(); windowFocusCallback.release(); if (window != 0) { glfwDestroyWindow(window); } } catch (Exception ex) { listener.handleError("Failed to destroy context", ex); } } public void create(boolean waitFor) { if (created.get()) { LOGGER.warning("create() called when display is already created!"); return; } new Thread(this, THREAD_NAME).start(); if (waitFor) { waitFor(true); } } /** * Does LWJGL display initialization in the OpenGL thread */ protected boolean initInThread() { try { if (!JmeSystem.isLowPermissions()) { // Enable uncaught exception handler only for current thread Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { public void uncaughtException(Thread thread, Throwable 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 LwjglTimer(); // 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(); } catch (Exception ex) { try { if (window != -1) { //glfwSetWindowShouldClose(window, GL_TRUE); glfwDestroyWindow(window); } } 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)) { try { createContext(settings); } catch (Exception ex) { LOGGER.log(Level.SEVERE, "Failed to set display settings!", ex); } LOGGER.fine("Display restarted."); } if (!created.get()) { throw new IllegalStateException(); } listener.update(); // All this does is call swap buffers // 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); } } glfwPollEvents(); // Subclasses just call GLObjectManager clean up objects here // it is safe .. for now. if (renderer != null) { renderer.postFrame(); } } /** * De-initialize in the OpenGL thread. */ protected void deinitInThread() { destroyContext(); listener.destroy(); LOGGER.fine("Display destroyed."); super.internalDestroy(); } public void run() { if (listener == null) { throw new IllegalStateException("SystemListener is not set on context!" + "Must set with JmeContext.setSystemListener()."); } registerNatives(); loadNatives(); LOGGER.log(Level.FINE, "Using LWJGL {0}", Sys.getVersion()); if (!initInThread()) { LOGGER.log(Level.SEVERE, "Display initialization failed. Cannot continue."); return; } while (true) { if (glfwWindowShouldClose(window) == GL_TRUE) { listener.requestClose(false); } runLoop(); if (needClose.get()) { break; } } deinitInThread(); } public JoyInput getJoyInput() { if (joyInput == null) { joyInput = new GlfwJoystickInput(); } return joyInput; } public MouseInput getMouseInput() { if (mouseInput == null) { mouseInput = new GlfwMouseInput(this); } return mouseInput; } public KeyInput getKeyInput() { if (keyInput == null) { keyInput = new GlfwKeyInput(this); } return keyInput; } public TouchInput getTouchInput() { return null; } public void setAutoFlushFrames(boolean enabled) { this.autoFlush = enabled; } public void destroy(boolean waitFor) { needClose.set(true); if (waitFor) { waitFor(false); } } public long getWindowHandle() { return window; } private ByteBuffer[] imagesToByteBuffers(Object[] images) { ByteBuffer[] out = new ByteBuffer[images.length]; for (int i = 0; i < images.length; i++) { BufferedImage image = (BufferedImage) images[i]; out[i] = imageToByteBuffer(image); } return out; } private ByteBuffer imageToByteBuffer(BufferedImage image) { if (image.getType() != BufferedImage.TYPE_INT_ARGB_PRE) { BufferedImage convertedImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE); Graphics2D g = convertedImage.createGraphics(); double width = image.getWidth() * (double) 1; double height = image.getHeight() * (double) 1; g.drawImage(image, (int) ((convertedImage.getWidth() - width) / 2), (int) ((convertedImage.getHeight() - height) / 2), (int) (width), (int) (height), null); g.dispose(); image = convertedImage; } byte[] imageBuffer = new byte[image.getWidth() * image.getHeight() * 4]; int counter = 0; for (int i = 0; i < image.getHeight(); i++) { for (int j = 0; j < image.getWidth(); j++) { int colorSpace = image.getRGB(j, i); imageBuffer[counter + 0] = (byte) ((colorSpace << 8) >> 24); imageBuffer[counter + 1] = (byte) ((colorSpace << 16) >> 24); imageBuffer[counter + 2] = (byte) ((colorSpace << 24) >> 24); imageBuffer[counter + 3] = (byte) (colorSpace >> 24); counter += 4; } } return ByteBuffer.wrap(imageBuffer); } }