/* * Copyright 2016 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.engine.subsystem.lwjgl; import com.google.common.collect.Lists; import com.google.common.collect.Queues; import org.lwjgl.LWJGLException; import org.lwjgl.opengl.ContextAttribs; import org.lwjgl.opengl.Display; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; import org.lwjgl.opengl.GL43; import org.lwjgl.opengl.GLContext; import org.lwjgl.opengl.KHRDebugCallback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.assets.AssetFactory; import org.terasology.assets.module.ModuleAssetDataProducer; import org.terasology.assets.module.ModuleAwareAssetTypeManager; import org.terasology.config.Config; import org.terasology.config.RenderingConfig; import org.terasology.context.Context; import org.terasology.engine.GameEngine; import org.terasology.engine.GameThread; import org.terasology.engine.modes.GameState; import org.terasology.engine.subsystem.DisplayDevice; import org.terasology.engine.subsystem.RenderingSubsystemFactory; import org.terasology.rendering.ShaderManager; import org.terasology.rendering.ShaderManagerLwjgl; import org.terasology.rendering.assets.animation.MeshAnimation; import org.terasology.rendering.assets.animation.MeshAnimationData; import org.terasology.rendering.assets.animation.MeshAnimationImpl; import org.terasology.rendering.assets.atlas.Atlas; import org.terasology.rendering.assets.atlas.AtlasData; import org.terasology.rendering.assets.font.Font; import org.terasology.rendering.assets.font.FontData; import org.terasology.rendering.assets.font.FontImpl; import org.terasology.rendering.assets.material.Material; import org.terasology.rendering.assets.material.MaterialData; import org.terasology.rendering.assets.mesh.Mesh; import org.terasology.rendering.assets.mesh.MeshData; import org.terasology.rendering.assets.shader.Shader; import org.terasology.rendering.assets.shader.ShaderData; import org.terasology.rendering.assets.skeletalmesh.SkeletalMesh; import org.terasology.rendering.assets.skeletalmesh.SkeletalMeshData; import org.terasology.rendering.assets.texture.PNGTextureFormat; import org.terasology.rendering.assets.texture.Texture; import org.terasology.rendering.assets.texture.TextureData; import org.terasology.rendering.assets.texture.TextureUtil; import org.terasology.rendering.assets.texture.subtexture.Subtexture; import org.terasology.rendering.assets.texture.subtexture.SubtextureData; import org.terasology.rendering.nui.internal.CanvasRenderer; import org.terasology.rendering.nui.internal.LwjglCanvasRenderer; import org.terasology.rendering.opengl.GLSLMaterial; import org.terasology.rendering.opengl.GLSLShader; import org.terasology.rendering.opengl.OpenGLMesh; import org.terasology.rendering.opengl.OpenGLSkeletalMesh; import org.terasology.rendering.opengl.OpenGLTexture; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.BlockingDeque; import java.util.function.Consumer; import static org.lwjgl.opengl.GL11.GL_CULL_FACE; import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST; import static org.lwjgl.opengl.GL11.GL_LEQUAL; import static org.lwjgl.opengl.GL11.GL_NORMALIZE; import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D; import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_S; import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_T; import static org.lwjgl.opengl.GL11.glBindTexture; import static org.lwjgl.opengl.GL11.glDeleteTextures; import static org.lwjgl.opengl.GL11.glDepthFunc; import static org.lwjgl.opengl.GL11.glEnable; import static org.lwjgl.opengl.GL11.glGenTextures; import static org.lwjgl.opengl.GL11.glTexParameterf; import static org.lwjgl.opengl.GL11.glViewport; public class LwjglGraphics extends BaseLwjglSubsystem { private static final Logger logger = LoggerFactory.getLogger(LwjglGraphics.class); private GLBufferPool bufferPool = new GLBufferPool(false); private BlockingDeque<Runnable> displayThreadActions = Queues.newLinkedBlockingDeque(); private Context context; private RenderingConfig config; private GameEngine engine; private LwjglDisplayDevice lwjglDisplay; @Override public String getName() { return "Graphics"; } @Override public void initialise(GameEngine gameEngine, Context rootContext) { logger.info("Starting initialization of LWJGL"); this.engine = gameEngine; this.context = rootContext; this.config = context.get(Config.class).getRendering(); lwjglDisplay = new LwjglDisplayDevice(context); context.put(DisplayDevice.class, lwjglDisplay); logger.info("Initial initialization complete"); } @Override public void registerCoreAssetTypes(ModuleAwareAssetTypeManager assetTypeManager) { // cast lambdas explicitly to avoid inconsistent compiler behavior wrt. type inference assetTypeManager.registerCoreAssetType(Font.class, (AssetFactory<Font, FontData>) FontImpl::new, "fonts"); assetTypeManager.registerCoreAssetType(Texture.class, (AssetFactory<Texture, TextureData>) (urn, assetType, data) -> (new OpenGLTexture(urn, assetType, data, this)), "textures", "fonts"); assetTypeManager.registerCoreFormat(Texture.class, new PNGTextureFormat(Texture.FilterMode.NEAREST, path -> { if (path.getName(1).toString().equals(ModuleAssetDataProducer.OVERRIDE_FOLDER)) { return path.getName(3).toString().equals("textures"); } else { return path.getName(2).toString().equals("textures"); } })); assetTypeManager.registerCoreFormat(Texture.class, new PNGTextureFormat(Texture.FilterMode.LINEAR, path -> { if (path.getName(1).toString().equals(ModuleAssetDataProducer.OVERRIDE_FOLDER)) { return path.getName(3).toString().equals("fonts"); } else { return path.getName(2).toString().equals("fonts"); } })); assetTypeManager.registerCoreAssetType(Shader.class, (AssetFactory<Shader, ShaderData>) GLSLShader::new, "shaders"); assetTypeManager.registerCoreAssetType(Material.class, (AssetFactory<Material, MaterialData>) GLSLMaterial::new, "materials"); assetTypeManager.registerCoreAssetType(Mesh.class, (AssetFactory<Mesh, MeshData>) (urn, assetType, data) -> new OpenGLMesh(urn, assetType, bufferPool, data), "mesh"); assetTypeManager.registerCoreAssetType(SkeletalMesh.class, (AssetFactory<SkeletalMesh, SkeletalMeshData>) (urn, assetType, data) -> new OpenGLSkeletalMesh(urn, assetType, data, bufferPool), "skeletalMesh"); assetTypeManager.registerCoreAssetType(MeshAnimation.class, (AssetFactory<MeshAnimation, MeshAnimationData>) MeshAnimationImpl::new, "animations"); assetTypeManager.registerCoreAssetType(Atlas.class, (AssetFactory<Atlas, AtlasData>) Atlas::new, "atlas"); assetTypeManager.registerCoreAssetType(Subtexture.class, (AssetFactory<Subtexture, SubtextureData>) Subtexture::new); } @Override public void postInitialise(Context rootContext) { context.put(RenderingSubsystemFactory.class, new LwjglRenderingSubsystemFactory(bufferPool)); initDisplay(); initOpenGL(context); context.put(CanvasRenderer.class, new LwjglCanvasRenderer(context)); } @Override public void postUpdate(GameState currentState, float delta) { Display.update(); if (!displayThreadActions.isEmpty()) { List<Runnable> actions = Lists.newArrayListWithExpectedSize(displayThreadActions.size()); displayThreadActions.drainTo(actions); actions.forEach(Runnable::run); } int frameLimit = context.get(Config.class).getRendering().getFrameLimit(); if (frameLimit > 0) { Display.sync(frameLimit); } currentState.render(); if (Display.wasResized()) { glViewport(0, 0, Display.getWidth(), Display.getHeight()); } if (lwjglDisplay.isCloseRequested()) { engine.shutdown(); } } @Override public void preShutdown() { if (Display.isCreated() && !Display.isFullscreen() && Display.isVisible()) { config.setWindowPosX(Display.getX()); config.setWindowPosY(Display.getY()); } } @Override public void shutdown() { Display.destroy(); } private void initDisplay() { logger.info("Initializing display (if last line in log then likely the game crashed from an issue with your video card)"); try { lwjglDisplay.setDisplayModeSetting(config.getDisplayModeSetting(), false); Display.setTitle("Terasology" + " | " + "Alpha"); try { String root = "org/terasology/icons/"; ClassLoader classLoader = getClass().getClassLoader(); BufferedImage icon16 = ImageIO.read(classLoader.getResourceAsStream(root + "gooey_sweet_16.png")); BufferedImage icon32 = ImageIO.read(classLoader.getResourceAsStream(root + "gooey_sweet_32.png")); BufferedImage icon64 = ImageIO.read(classLoader.getResourceAsStream(root + "gooey_sweet_64.png")); BufferedImage icon128 = ImageIO.read(classLoader.getResourceAsStream(root + "gooey_sweet_128.png")); Display.setIcon(new ByteBuffer[]{ TextureUtil.convertToByteBuffer(icon16), TextureUtil.convertToByteBuffer(icon32), TextureUtil.convertToByteBuffer(icon64), TextureUtil.convertToByteBuffer(icon128) }); } catch (IOException | IllegalArgumentException e) { logger.warn("Could not set icon", e); } if (config.getDebug().isEnabled()) { try { ContextAttribs ctxAttribs = new ContextAttribs().withDebug(true); Display.create(config.getPixelFormat(), ctxAttribs); try { GL43.glDebugMessageCallback(new KHRDebugCallback(new DebugCallback())); } catch (IllegalStateException e) { logger.warn("Unable to specify DebugCallback to receive debugging messages from the GL."); } } catch (LWJGLException e) { logger.warn("Unable to create an OpenGL debug context. Maybe your graphics card does not support it.", e); Display.create(config.getPixelFormat()); // Create a normal context instead } } else { Display.create(config.getPixelFormat()); } Display.setVSyncEnabled(config.isVSync()); } catch (LWJGLException e) { throw new RuntimeException("Can not initialize graphics device.", e); } } private void initOpenGL(Context currentContext) { logger.info("Initializing OpenGL"); checkOpenGL(); glViewport(0, 0, Display.getWidth(), Display.getHeight()); initOpenGLParams(); currentContext.put(ShaderManager.class, new ShaderManagerLwjgl()); } private void checkOpenGL() { boolean[] requiredCapabilities = { GLContext.getCapabilities().OpenGL12, GLContext.getCapabilities().OpenGL14, GLContext.getCapabilities().OpenGL15, GLContext.getCapabilities().OpenGL20, GLContext.getCapabilities().OpenGL21, // needed as we use GLSL 1.20 GLContext.getCapabilities().GL_ARB_framebuffer_object, // Extensions eventually included in GLContext.getCapabilities().GL_ARB_texture_float, // OpenGl 3.0 according to GLContext.getCapabilities().GL_ARB_half_float_pixel}; // http://en.wikipedia.org/wiki/OpenGL#OpenGL_3.0 String[] capabilityNames = {"OpenGL12", "OpenGL14", "OpenGL15", "OpenGL20", "OpenGL21", "GL_ARB_framebuffer_object", "GL_ARB_texture_float", "GL_ARB_half_float_pixel"}; boolean canRunTheGame = true; String missingCapabilitiesMessage = ""; for (int index = 0; index < requiredCapabilities.length; index++) { if (!requiredCapabilities[index]) { missingCapabilitiesMessage += " - " + capabilityNames[index] + "\n"; canRunTheGame = false; } } if (!canRunTheGame) { String completeErrorMessage = completeErrorMessage(missingCapabilitiesMessage); throw new IllegalStateException(completeErrorMessage); } } private String completeErrorMessage(String errorMessage) { return "\n" + "\nThe following OpenGL versions/extensions are required but are not supported by your GPU driver:\n" + "\n" + errorMessage + "\n" + "GPU Information:\n" + "\n" + " Vendor: " + GL11.glGetString(GL11.GL_VENDOR) + "\n" + " Model: " + GL11.glGetString(GL11.GL_RENDERER) + "\n" + " Driver: " + GL11.glGetString(GL11.GL_VERSION) + "\n" + "\n" + "Try updating the driver to the latest version available.\n" + "If that fails you might need to use a different GPU (graphics card). Sorry!\n"; } public static void initOpenGLParams() { glEnable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); glEnable(GL_NORMALIZE); glDepthFunc(GL_LEQUAL); } public void asynchToDisplayThread(Runnable action) { if (GameThread.isCurrentThread()) { action.run(); } else { displayThreadActions.add(action); } } public void createTexture3D(ByteBuffer alignedBuffer, Texture.WrapMode wrapMode, Texture.FilterMode filterMode, int size, Consumer<Integer> idConsumer) { asynchToDisplayThread(() -> { int id = glGenTextures(); reloadTexture3D(id, alignedBuffer, wrapMode, filterMode, size); idConsumer.accept(id); }); } public void reloadTexture3D(int id, ByteBuffer alignedBuffer, Texture.WrapMode wrapMode, Texture.FilterMode filterMode, int size) { asynchToDisplayThread(() -> { glBindTexture(GL12.GL_TEXTURE_3D, id); glTexParameterf(GL12.GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, LwjglGraphicsUtil.getGLMode(wrapMode)); glTexParameterf(GL12.GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, LwjglGraphicsUtil.getGLMode(wrapMode)); glTexParameterf(GL12.GL_TEXTURE_3D, GL12.GL_TEXTURE_WRAP_R, LwjglGraphicsUtil.getGLMode(wrapMode)); GL11.glTexParameteri(GL12.GL_TEXTURE_3D, GL11.GL_TEXTURE_MIN_FILTER, LwjglGraphicsUtil.getGlMinFilter(filterMode)); GL11.glTexParameteri(GL12.GL_TEXTURE_3D, GL11.GL_TEXTURE_MAG_FILTER, LwjglGraphicsUtil.getGlMagFilter(filterMode)); GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 4); GL11.glTexParameteri(GL12.GL_TEXTURE_3D, GL12.GL_TEXTURE_MAX_LEVEL, 0); GL12.glTexImage3D(GL12.GL_TEXTURE_3D, 0, GL11.GL_RGBA, size, size, size, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, alignedBuffer); }); } public void createTexture2D(ByteBuffer[] buffers, Texture.WrapMode wrapMode, Texture.FilterMode filterMode, int width, int height, Consumer<Integer> idConsumer) { asynchToDisplayThread(() -> { int id = glGenTextures(); reloadTexture2D(id, buffers, wrapMode, filterMode, width, height); idConsumer.accept(id); }); } public void reloadTexture2D(int id, ByteBuffer[] buffers, Texture.WrapMode wrapMode, Texture.FilterMode filterMode, int width, int height) { asynchToDisplayThread(() -> { glBindTexture(GL11.GL_TEXTURE_2D, id); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, LwjglGraphicsUtil.getGLMode(wrapMode)); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, LwjglGraphicsUtil.getGLMode(wrapMode)); GL11.glTexParameteri(GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, LwjglGraphicsUtil.getGlMinFilter(filterMode)); GL11.glTexParameteri(GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, LwjglGraphicsUtil.getGlMagFilter(filterMode)); GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 4); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, buffers.length - 1); if (buffers.length > 0) { for (int i = 0; i < buffers.length; i++) { GL11.glTexImage2D(GL11.GL_TEXTURE_2D, i, GL11.GL_RGBA, width >> i, height >> i, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffers[i]); } } else { GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, width, height, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null); } }); } public void disposeTexture(int id) { asynchToDisplayThread(() -> glDeleteTextures(id)); } }