diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/AsyncFrameReader.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/AsyncFrameReader.java new file mode 100755 index 000000000..3b41c6b41 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/AsyncFrameReader.java @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2009-2015 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.renderer.opengl; + +import com.jme3.renderer.RenderContext; +import com.jme3.renderer.RendererException; +import com.jme3.texture.FrameBuffer; +import com.jme3.util.BufferUtils; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * @author Kirill Vainer + */ +final class AsyncFrameReader { + + private final ArrayList pboPool = new ArrayList(); + private final List pending = Collections.synchronizedList(new ArrayList()); + private final GLRenderer renderer; + private final GL gl; + private final GLExt glext; + private final IntBuffer intBuf = BufferUtils.createIntBuffer(1); + private final RenderContext context; + private final Thread glThread; + + AsyncFrameReader(GLRenderer renderer, GL gl, GLExt glext, RenderContext context) { + this.renderer = renderer; + this.gl = gl; + this.glext = glext; + this.context = context; + this.glThread = Thread.currentThread(); + } + + private PixelBuffer acquirePixelBuffer(int dataSize) { + PixelBuffer pb; + + if (pboPool.isEmpty()) { + // create PBO + pb = new PixelBuffer(); + intBuf.clear(); + gl.glGenBuffers(intBuf); + pb.id = intBuf.get(0); + } else { + // reuse PBO. + pb = pboPool.remove(pboPool.size() - 1); + } + + // resize or allocate PBO if required. + if (pb.size != dataSize) { + if (context.boundPixelPackPBO != pb.id) { + gl.glBindBuffer(GLExt.GL_PIXEL_PACK_BUFFER_ARB, pb.id); + context.boundPixelPackPBO = pb.id; + } + gl.glBufferData(GLExt.GL_PIXEL_PACK_BUFFER_ARB, dataSize, GL.GL_STREAM_READ); + } + + pb.size = dataSize; + + return pb; + } + + private void readFrameBufferFromPBO(FrameBufferReadRequest fbrr) { + // assumes waitForCompletion was already called! + if (context.boundPixelPackPBO != fbrr.pb.id) { + gl.glBindBuffer(GLExt.GL_PIXEL_PACK_BUFFER_ARB, fbrr.pb.id); + context.boundPixelPackPBO = fbrr.pb.id; + } + gl.glGetBufferSubData(GLExt.GL_PIXEL_PACK_BUFFER_ARB, 0, fbrr.targetBuf); + } + + private boolean waitForCompletion(FrameBufferReadRequest fbrr, long time, TimeUnit unit, boolean flush) { + int flags = flush ? GLExt.GL_SYNC_FLUSH_COMMANDS_BIT : 0; + long nanos = unit.toNanos(time); + switch (glext.glClientWaitSync(fbrr.fence, flags, nanos)) { + case GLExt.GL_ALREADY_SIGNALED: + case GLExt.GL_CONDITION_SATISFIED: + return true; + case GLExt.GL_TIMEOUT_EXPIRED: + return false; + case GLExt.GL_WAIT_FAILED: + throw new RendererException("Waiting for fence failed"); + default: + throw new RendererException("Unexpected result from glClientWaitSync"); + } + } + + private void signalFinished(FrameBufferReadRequest fbrr) { + fbrr.lock.lock(); + try { + fbrr.done = true; + fbrr.cond.signalAll(); + } finally { + fbrr.lock.unlock(); + } + } + + void signalCancelled(FrameBufferReadRequest fbrr) { + fbrr.lock.lock(); + try { + fbrr.cancelled = true; + fbrr.cond.signalAll(); + } finally { + fbrr.lock.unlock(); + } + } + + public void updateReadRequests() { + // Update requests in the order they were made (e.g. earliest first) + for (Iterator it = pending.iterator(); it.hasNext();) { + FrameBufferReadRequest fbrr = it.next(); + + // Check status for the user... (non-blocking) + if (!fbrr.cancelled && !fbrr.done) { + // Request a flush if we know clients are waiting + // (to speed up the process, or make it take finite time ..) + boolean flush = false; // fbrr.clientsWaiting.get() > 0; + if (waitForCompletion(fbrr, 0, TimeUnit.NANOSECONDS, flush)) { + if (!fbrr.cancelled) { + // Operation completed. + // Read data into user's ByteBuffer + readFrameBufferFromPBO(fbrr); + + // Signal any waiting threads that we are done. + // Also, set the done flag. + signalFinished(fbrr); + } + } + } + + if (fbrr.cancelled || fbrr.done) { + // Cleanup + // Return the pixel buffer back into the pool. + if (!pboPool.contains(fbrr.pb)) { + pboPool.add(fbrr.pb); + } + + // Remove this request from the pending requests list. + it.remove(); + + // Get rid of the fence + glext.glDeleteSync(fbrr.fence); + + fbrr.pb = null; + fbrr.fence = null; + } + } + } + + ByteBuffer getFrameBufferData(FrameBufferReadRequest fbrr, long time, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + + if (fbrr.cancelled) { + throw new CancellationException(); + } + + if (fbrr.done) { + return fbrr.targetBuf; + } + + if (glThread == Thread.currentThread()) { + // Running on GL thread, hence can use GL commands .. + try { + // Wait until we reach the fence.. + + // PROBLEM: if the user is holding any locks, + // they will not be released here, + // causing a potential deadlock! + if (!waitForCompletion(fbrr, time, unit, true)) { + throw new TimeoutException(); + } + + // Command stream reached this point. + if (fbrr.cancelled) { + // User not interested in this anymore. + throw new CancellationException(); + } else { + // Read data into user's ByteBuffer + readFrameBufferFromPBO(fbrr); + } + + // Mark it as done, so future get() calls always return. + signalFinished(fbrr); + + return fbrr.targetBuf; + } catch (RendererException ex) { + throw new ExecutionException(ex); + } + } else { + long nanos = unit.toNanos(time); + + fbrr.lock.lock(); + try { + // Not running on GL thread, indicate that we are running + // so GL thread can request GPU to finish quicker ... + fbrr.clientsWaiting.getAndIncrement(); + + // Wait until we finish + while (!fbrr.done && !fbrr.cancelled) { + if (nanos <= 0L) { + throw new TimeoutException(); + } + + nanos = fbrr.cond.awaitNanos(nanos); + } + + if (fbrr.cancelled) { + throw new CancellationException(); + } + + return fbrr.targetBuf; + } finally { + fbrr.lock.unlock(); + fbrr.clientsWaiting.getAndDecrement(); + } + } + } + + public Future readFrameBufferLater(FrameBuffer fb, ByteBuffer byteBuf) { + // Create & allocate a PBO (or reuse an existing one if available) + FrameBufferReadRequest fbrr = new FrameBufferReadRequest(this); + fbrr.targetBuf = byteBuf; + + int desiredSize = fb.getWidth() * fb.getHeight() * 4; + + if (byteBuf.remaining() != desiredSize) { + throw new IllegalArgumentException("Ensure buffer size matches framebuffer size"); + } + + fbrr.pb = acquirePixelBuffer(desiredSize); + + // Read into PBO (asynchronous) +// renderer.readFrameBufferWithGLFormat(fb, null, GL2.GL_BGRA, GL2.GL_UNSIGNED_BYTE, fbrr.pb.id); + + // Insert fence into command stream. + fbrr.fence = glext.glFenceSync(GLExt.GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + + // Insert into FIFO + pending.add(fbrr); + + return fbrr; + } +} diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/FrameBufferReadRequest.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/FrameBufferReadRequest.java new file mode 100755 index 000000000..24021cdb3 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/FrameBufferReadRequest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2009-2015 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.renderer.opengl; + +import java.nio.ByteBuffer; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +class PixelBuffer { + int id = -1; + int size = -1; +} + +class FrameBufferReadRequest implements Future { + + AsyncFrameReader reader; + Object fence; + PixelBuffer pb; + ByteBuffer targetBuf; + boolean cancelled; + boolean done; + + final ReentrantLock lock = new ReentrantLock(); + final Condition cond = lock.newCondition(); + final AtomicInteger clientsWaiting = new AtomicInteger(0); + + public FrameBufferReadRequest(AsyncFrameReader reader) { + this.reader = reader; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (isDone()) { + return false; + } + reader.signalCancelled(this); + return true; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public boolean isDone() { + return done; + } + + @Override + public ByteBuffer get(long l, TimeUnit tu) throws InterruptedException, ExecutionException, TimeoutException { + return reader.getFrameBufferData(this, l, tu); + } + + @Override + public ByteBuffer get() throws InterruptedException, ExecutionException { + try { + return get(1, TimeUnit.SECONDS); + } catch (TimeoutException ex) { + throw new ExecutionException(ex); + } + } +} + diff --git a/jme3-desktop/src/main/java/com/jme3/system/awt/AwtPanel.java b/jme3-desktop/src/main/java/com/jme3/system/awt/AwtPanel.java index 0314cf683..5162047b2 100644 --- a/jme3-desktop/src/main/java/com/jme3/system/awt/AwtPanel.java +++ b/jme3-desktop/src/main/java/com/jme3/system/awt/AwtPanel.java @@ -34,6 +34,7 @@ package com.jme3.system.awt; import com.jme3.post.SceneProcessor; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; +import com.jme3.renderer.opengl.GLRenderer; import com.jme3.renderer.queue.RenderQueue; import com.jme3.texture.FrameBuffer; import com.jme3.texture.Image.Format; @@ -47,20 +48,19 @@ import java.awt.image.AffineTransformOp; import java.awt.image.BufferStrategy; import java.awt.image.BufferedImage; import java.nio.ByteBuffer; -import java.nio.IntBuffer; import java.util.ArrayList; import java.util.Arrays; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; -public class AwtPanel extends Canvas implements SceneProcessor { +public class AwtPanel extends Canvas implements JmePanel, SceneProcessor { private boolean attachAsMain = false; private BufferedImage img; - private FrameBuffer fb; - private boolean srgb = false; - private ByteBuffer byteBuf; - private IntBuffer intBuf; +// private FrameBuffer fb; private RenderManager rm; private PaintMode paintMode; private ArrayList viewPorts = new ArrayList(); @@ -75,35 +75,47 @@ public class AwtPanel extends Canvas implements SceneProcessor { // Reshape vars private int newWidth = 1; private int newHeight = 1; - private AtomicBoolean reshapeNeeded = new AtomicBoolean(false); + private AtomicBoolean reshapeNeeded = new AtomicBoolean(true); private final Object lock = new Object(); - public AwtPanel(PaintMode paintMode){ - this(paintMode, false); - } + // Buffer pool and pending buffers + private int NUM_FRAMES = 3; + private final ArrayBlockingQueue> pendingFrames = new ArrayBlockingQueue>(NUM_FRAMES); + private final ArrayBlockingQueue bufferPool = new ArrayBlockingQueue(NUM_FRAMES); + private final ArrayList fbs = new ArrayList(NUM_FRAMES); + private int frameIndex = 0; + + + private final ComponentAdapter resizeListener = new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + onResize(e); + } + }; public AwtPanel(PaintMode paintMode, boolean srgb){ this.paintMode = paintMode; - this.srgb = srgb; + + invalidatePendingFrames(); + if (paintMode == PaintMode.Accelerated){ setIgnoreRepaint(true); } - addComponentListener(new ComponentAdapter(){ - @Override - public void componentResized(ComponentEvent e) { - synchronized (lock){ - int newWidth2 = Math.max(getWidth(), 1); - int newHeight2 = Math.max(getHeight(), 1); - if (newWidth != newWidth2 || newHeight != newHeight2){ - newWidth = newWidth2; - newHeight = newHeight2; - reshapeNeeded.set(true); - System.out.println("EDT: componentResized " + newWidth + ", " + newHeight); - } - } + addComponentListener(resizeListener); + } + + public void onResize(ComponentEvent e) { + synchronized (lock) { + int newWidth2 = Math.max(getWidth(), 1); + int newHeight2 = Math.max(getHeight(), 1); + if (newWidth != newWidth2 || newHeight != newHeight2) { + newWidth = newWidth2; + newHeight = newHeight2; + reshapeNeeded.set(true); + System.out.println("EDT: componentResized " + newWidth + ", " + newHeight); } - }); + } } @Override @@ -128,13 +140,13 @@ public class AwtPanel extends Canvas implements SceneProcessor { super.removeNotify(); } - @Override - public void paint(Graphics g){ - Graphics2D g2d = (Graphics2D) g; - synchronized (lock){ - g2d.drawImage(img, transformOp, 0, 0); - } - } +// @Override +// public void paint(Graphics g){ +// Graphics2D g2d = (Graphics2D) g; +// synchronized (lock){ +// g2d.drawImage(img, transformOp, 0, 0); +// } +// } public boolean checkVisibilityState(){ if (!hasNativePeer.get()){ @@ -157,24 +169,85 @@ public class AwtPanel extends Canvas implements SceneProcessor { return currentShowing; } - public void repaintInThread(){ - // Convert screenshot. - byteBuf.clear(); - rm.getRenderer().readFrameBuffer(fb, byteBuf); +// public void repaintInThread(){ +// // Convert screenshot. +// byteBuf.clear(); +// rm.getRenderer().readFrameBuffer(fb, byteBuf); +// +// synchronized (lock){ +// // All operations on img must be synchronized +// // as it is accessed from EDT. +// Screenshots.convertScreenShot2(intBuf, img); +// repaint(); +// } +// } + + public ByteBuffer acquireNextFrame() { + if (pendingFrames.isEmpty()) { + System.out.println("!!! No pending frames, returning null."); + return null; + } - synchronized (lock){ - // All operations on img must be synchronized - // as it is accessed from EDT. - Screenshots.convertScreenShot2(intBuf, img); - repaint(); + try { + ByteBuffer nextFrame = null; + +// while (!pendingFrames.isEmpty() && pendingFrames.peek().isDone()) { +// nextFrame = pendingFrames.take().get(); +// } +// +// if (nextFrame != null) { +// return nextFrame; +// } +// +// if (pendingFrames.remainingCapacity() == 0) { + // Force it to finish .. + return pendingFrames.take().get(); +// } + + // Some frames are pending, none are finished though. +// return null; + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } catch (ExecutionException ex) { + throw new RuntimeException(ex); } } - public void drawFrameInThread(){ - // Convert screenshot. - byteBuf.clear(); - rm.getRenderer().readFrameBuffer(fb, byteBuf); - Screenshots.convertScreenShot2(intBuf, img); + public void readNextFrame() { + if (bufferPool.isEmpty()) { + System.out.println("??? Too many pending frames!"); + return; // need to draw more frames .. + } + + try { + int size = fbs.get(frameIndex).getWidth() * fbs.get(frameIndex).getHeight() * 4; + ByteBuffer byteBuf = bufferPool.take(); + byteBuf = BufferUtils.ensureLargeEnough(byteBuf, size); + byteBuf.clear(); + + GLRenderer renderer = (GLRenderer) rm.getRenderer(); + Future future = renderer.readFrameBufferLater(fbs.get(frameIndex), byteBuf); + if (!pendingFrames.offer(future)) { + throw new AssertionError(); + } + + frameIndex ++; + if (frameIndex >= NUM_FRAMES) { + frameIndex = 0; + } + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + + public void drawFrameInThread(ByteBuffer byteBuf){ + // Convert the frame into the image so it can be rendered. + Screenshots.convertScreenShot2(byteBuf.asIntBuffer(), img); + + // return the frame back to its rightful owner. + if (!bufferPool.offer(byteBuf)) { + throw new AssertionError(); + } synchronized (lock){ // All operations on strategy should be synchronized (?) @@ -235,44 +308,65 @@ public class AwtPanel extends Canvas implements SceneProcessor { if (this.rm == null){ // First time called in OGL thread this.rm = rm; - reshapeInThread(1, 1); +// reshapeInThread(1, 1); + } + } + + private void updateAccelerated() { + readNextFrame(); + ByteBuffer byteBuf = acquireNextFrame(); + if (byteBuf != null) { + drawFrameInThread(byteBuf); + } + } + + private void invalidatePendingFrames() { + // NOTE: all pending read requests are invalid! + for (Future pendingRequest : pendingFrames) { + pendingRequest.cancel(true); + } + pendingFrames.clear(); + bufferPool.clear(); + + // Populate buffer pool. + int cap = bufferPool.remainingCapacity(); + for (int i = 0; i < cap; i++) { + bufferPool.add(BufferUtils.createByteBuffer(1 * 1 * 4)); } } private void reshapeInThread(int width, int height) { - byteBuf = BufferUtils.ensureLargeEnough(byteBuf, width * height * 4); - intBuf = byteBuf.asIntBuffer(); - - if (fb != null) { + invalidatePendingFrames(); + + for (FrameBuffer fb : fbs) { fb.dispose(); - fb = null; } + fbs.clear(); - fb = new FrameBuffer(width, height, 1); - fb.setDepthBuffer(Format.Depth); - fb.setColorBuffer(Format.RGB8); - fb.setSrgb(srgb); - - if (attachAsMain){ - rm.getRenderer().setMainFrameBufferOverride(fb); + for (int i = 0; i < NUM_FRAMES; i++) { + FrameBuffer fb = new FrameBuffer(width, height, 1); + fb.setDepthBuffer(Format.Depth); + fb.setColorBuffer(Format.RGBA8); + fbs.add(fb); } + +// if (attachAsMain){ +// rm.getRenderer().setMainFrameBufferOverride(fb); +// } + synchronized (lock){ img = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR); } -// synchronized (lock){ -// img = (BufferedImage) getGraphicsConfiguration().createCompatibleImage(width, height); -// } - AffineTransform tx = AffineTransform.getScaleInstance(1, -1); tx.translate(0, -img.getHeight()); transformOp = new AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); for (ViewPort vp : viewPorts){ - if (!attachAsMain){ - vp.setOutputFrameBuffer(fb); - } +// if (!attachAsMain){ +// vp.setOutputFrameBuffer(fb); +// } vp.getCamera().resize(width, height, true); // NOTE: Hack alert. This is done ONLY for custom framebuffers. @@ -283,8 +377,9 @@ public class AwtPanel extends Canvas implements SceneProcessor { } } + @Override public boolean isInitialized() { - return fb != null; + return rm != null; } public void preFrame(float tpf) { @@ -298,8 +393,21 @@ public class AwtPanel extends Canvas implements SceneProcessor { // For "PaintMode.OnDemand" only. repaintRequest.set(true); } + + @Override + public Component getComponent() { + return this; + } + + @Override + public void onFrameBegin() { + if (attachAsMain && rm != null){ + rm.getRenderer().setMainFrameBufferOverride(fbs.get(frameIndex)); + } + } - void onFrameEnd() { + @Override + public void onFrameEnd() { if (reshapeNeeded.getAndSet(false)) { reshapeInThread(newWidth, newHeight); } else { @@ -309,26 +417,24 @@ public class AwtPanel extends Canvas implements SceneProcessor { switch (paintMode) { case Accelerated: - drawFrameInThread(); - break; - case Repaint: - repaintInThread(); - break; - case OnRequest: - if (repaintRequest.getAndSet(false)) { - repaintInThread(); - } + updateAccelerated(); break; +// case Repaint: +// repaintInThread(); +// break; +// case OnRequest: +// if (repaintRequest.getAndSet(false)) { +// repaintInThread(); +// } +// break; } } } public void postFrame(FrameBuffer out) { - if (!attachAsMain && out != fb){ - throw new IllegalStateException("Why did you change the output framebuffer?"); - } - - // onFrameEnd(); +// if (!attachAsMain && out != fb){ +// throw new IllegalStateException("Why did you change the output framebuffer?"); +// } } public void reshape(ViewPort vp, int w, int h) { diff --git a/jme3-desktop/src/main/java/com/jme3/system/awt/AwtPanelsContext.java b/jme3-desktop/src/main/java/com/jme3/system/awt/AwtPanelsContext.java index 897632a00..cbd60a533 100644 --- a/jme3-desktop/src/main/java/com/jme3/system/awt/AwtPanelsContext.java +++ b/jme3-desktop/src/main/java/com/jme3/system/awt/AwtPanelsContext.java @@ -46,8 +46,8 @@ public class AwtPanelsContext implements JmeContext { protected JmeContext actualContext; protected AppSettings settings = new AppSettings(true); protected SystemListener listener; - protected ArrayList panels = new ArrayList(); - protected AwtPanel inputSource; + protected ArrayList panels = new ArrayList(); + protected JmePanel inputSource; protected AwtMouseInput mouseInput = new AwtMouseInput(); protected AwtKeyInput keyInput = new AwtKeyInput(); @@ -92,13 +92,13 @@ public class AwtPanelsContext implements JmeContext { } } - public void setInputSource(AwtPanel panel){ + public void setInputSource(JmePanel panel){ if (!panels.contains(panel)) throw new IllegalArgumentException(); inputSource = panel; - mouseInput.setInputSource(panel); - keyInput.setInputSource(panel); + mouseInput.setInputSource(panel.getComponent()); + keyInput.setInputSource(panel.getComponent()); } public Type getType() { @@ -148,14 +148,14 @@ public class AwtPanelsContext implements JmeContext { public AwtPanelsContext(){ } - public AwtPanel createPanel(PaintMode paintMode){ - AwtPanel panel = new AwtPanel(paintMode); + public JmePanel createPanel(PaintMode paintMode){ + JmePanel panel = new SwingPanel(paintMode, true); panels.add(panel); return panel; } - public AwtPanel createPanel(PaintMode paintMode, boolean srgb){ - AwtPanel panel = new AwtPanel(paintMode, srgb); + public JmePanel createPanel(PaintMode paintMode, boolean srgb){ + JmePanel panel = new SwingPanel(paintMode, srgb); panels.add(panel); return panel; } @@ -168,18 +168,18 @@ public class AwtPanelsContext implements JmeContext { // Check if throttle required boolean needThrottle = true; - for (AwtPanel panel : panels){ + for (JmePanel panel : panels){ if (panel.isActiveDrawing()){ needThrottle = false; break; } } - if (lastThrottleState != needThrottle){ + if (lastThrottleState != needThrottle) { lastThrottleState = needThrottle; - if (lastThrottleState){ + if (lastThrottleState) { System.out.println("OGL: Throttling update loop."); - }else{ + } else { System.out.println("OGL: Ceased throttling update loop."); } } @@ -191,9 +191,13 @@ public class AwtPanelsContext implements JmeContext { } } + for (JmePanel panel : panels){ + panel.onFrameBegin(); + } + listener.update(); - for (AwtPanel panel : panels){ + for (JmePanel panel : panels){ panel.onFrameEnd(); } } diff --git a/jme3-desktop/src/main/java/com/jme3/system/awt/JmePanel.java b/jme3-desktop/src/main/java/com/jme3/system/awt/JmePanel.java new file mode 100755 index 000000000..21bbbb753 --- /dev/null +++ b/jme3-desktop/src/main/java/com/jme3/system/awt/JmePanel.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2009-2015 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.awt; + +import com.jme3.renderer.ViewPort; +import java.awt.Component; + +public interface JmePanel { + + public void attachTo(boolean overrideMainFramebuffer, ViewPort ... vps); + + public boolean isActiveDrawing(); + + public void onFrameBegin(); + + public void onFrameEnd(); + + public Component getComponent(); +} diff --git a/jme3-desktop/src/main/java/com/jme3/system/awt/SwingPanel.java b/jme3-desktop/src/main/java/com/jme3/system/awt/SwingPanel.java new file mode 100755 index 000000000..8fbf7944e --- /dev/null +++ b/jme3-desktop/src/main/java/com/jme3/system/awt/SwingPanel.java @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2009-2015 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.awt; + +import com.jme3.post.SceneProcessor; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; +import com.jme3.renderer.opengl.GLRenderer; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Image; +import com.jme3.util.BufferUtils; +import com.jme3.util.Screenshots; +import java.awt.AWTException; +import java.awt.BufferCapabilities; +import java.awt.Canvas; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.ImageCapabilities; +import java.awt.RenderingHints; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.geom.AffineTransform; +import java.awt.image.AffineTransformOp; +import java.awt.image.BufferStrategy; +import java.awt.image.BufferedImage; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JPanel; + +public class SwingPanel extends JPanel implements JmePanel, SceneProcessor { + + private boolean attachAsMain = false; + + private BufferedImage img; +// private FrameBuffer fb; + private RenderManager rm; + private PaintMode paintMode; + private ArrayList viewPorts = new ArrayList(); + + // Visibility/drawing vars + private AffineTransformOp transformOp; + private AtomicBoolean hasNativePeer = new AtomicBoolean(false); + private AtomicBoolean showing = new AtomicBoolean(false); + private AtomicBoolean repaintRequest = new AtomicBoolean(false); + + // Reshape vars + private int newWidth = 1; + private int newHeight = 1; + private AtomicBoolean reshapeNeeded = new AtomicBoolean(true); + private final Object lock = new Object(); + + // Buffer pool and pending buffers + private final int NUM_FRAMES = 2; + private final ArrayBlockingQueue> pendingFrames = new ArrayBlockingQueue>(NUM_FRAMES); + private final ArrayBlockingQueue bufferPool = new ArrayBlockingQueue(NUM_FRAMES); + private final ArrayList fbs = new ArrayList(NUM_FRAMES); + private int frameIndex = 0; + + private final ComponentAdapter resizeListener = new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + onResize(e); + } + }; + + public SwingPanel(PaintMode paintMode, boolean srgb){ + this.paintMode = paintMode; + invalidatePendingFrames(); + addComponentListener(resizeListener); + } + + public void onResize(ComponentEvent e) { + synchronized (lock) { + int newWidth2 = Math.max(getWidth(), 1); + int newHeight2 = Math.max(getHeight(), 1); + if (newWidth != newWidth2 || newHeight != newHeight2) { + newWidth = newWidth2; + newHeight = newHeight2; + reshapeNeeded.set(true); + System.out.println("EDT: componentResized " + newWidth + ", " + newHeight); + } + } + } + + @Override + public Component getComponent() { + return this; + } + + @Override + public void addNotify(){ + super.addNotify(); + + synchronized (lock){ + hasNativePeer.set(true); + System.out.println("EDT: addNotify"); + } + + requestFocusInWindow(); + } + + @Override + public void removeNotify(){ + synchronized (lock){ + hasNativePeer.set(false); + System.out.println("EDT: removeNotify"); + } + + super.removeNotify(); + } + + public boolean checkVisibilityState() { + if (!hasNativePeer.get()) { + return false; + } + + boolean currentShowing = isShowing(); + if (showing.getAndSet(currentShowing) != currentShowing) { + if (currentShowing) { + System.out.println("OGL: Enter showing state."); + } else { + System.out.println("OGL: Exit showing state."); + } + } + return currentShowing; + } + + @Override + public void paintComponent(Graphics g){ + Graphics2D g2d = (Graphics2D) g; + + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_SPEED); + + ByteBuffer byteBuf = null; + + synchronized (lock){ + if (pendingFrames.size() > NUM_FRAMES - 1) { + byteBuf = acquireNextFrame(); + } + + if (byteBuf != null) { + // Convert the frame into the image so it can be rendered. + Screenshots.convertScreenShot2(byteBuf.asIntBuffer(), img); + + try { + // return the frame back to its rightful owner. + bufferPool.put(byteBuf); + } catch (InterruptedException ex) { + Logger.getLogger(SwingPanel.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + g2d.drawImage(img, transformOp, 0, 0); + } + + public ByteBuffer acquireNextFrame() { + if (pendingFrames.isEmpty()) { + System.out.println("!!! No pending frames, returning null."); + return null; + } + + try { + return pendingFrames.take().get(); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } catch (ExecutionException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Grabs an available buffer from the available frames pool, + * reads the OpenGL backbuffer into it, then adds it to the pending frames pool. + */ + public void readNextFrame() { + if (bufferPool.isEmpty()) { + System.out.println("??? Too many pending frames!"); + return; // need to draw more frames .. + } + + try { + int size = fbs.get(frameIndex).getWidth() * fbs.get(frameIndex).getHeight() * 4; + ByteBuffer byteBuf = bufferPool.take(); + byteBuf = BufferUtils.ensureLargeEnough(byteBuf, size); + byteBuf.clear(); + + GLRenderer renderer = (GLRenderer) rm.getRenderer(); +// Future future = renderer.readFrameBufferLater(fbs.get(frameIndex), byteBuf); +// if (!pendingFrames.offer(future)) { +// throw new AssertionError(); +// } + + frameIndex ++; + if (frameIndex >= NUM_FRAMES) { + frameIndex = 0; + } + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public boolean isActiveDrawing() { + return paintMode != PaintMode.OnRequest && showing.get(); + } + + public void attachTo(boolean overrideMainFramebuffer, ViewPort ... vps){ + if (viewPorts.size() > 0){ + for (ViewPort vp : viewPorts){ + vp.setOutputFrameBuffer(null); + } + viewPorts.get(viewPorts.size()-1).removeProcessor(this); + } + + viewPorts.addAll(Arrays.asList(vps)); + viewPorts.get(viewPorts.size()-1).addProcessor(this); + + this.attachAsMain = overrideMainFramebuffer; + } + + public void initialize(RenderManager rm, ViewPort vp) { + if (this.rm == null){ + // First time called in OGL thread + this.rm = rm; +// reshapeInThread(1, 1); + } + } + + private void invalidatePendingFrames() { + // NOTE: all pending read requests are invalid! + for (Future pendingRequest : pendingFrames) { + pendingRequest.cancel(true); + } + pendingFrames.clear(); + bufferPool.clear(); + + // Populate buffer pool. + int cap = bufferPool.remainingCapacity(); + for (int i = 0; i < cap; i++) { + bufferPool.add(BufferUtils.createByteBuffer(1 * 1 * 4)); + } + } + + private void reshapeInThread(int width, int height) { + invalidatePendingFrames(); + + for (FrameBuffer fb : fbs) { + fb.dispose(); + } + + fbs.clear(); + + for (int i = 0; i < NUM_FRAMES; i++) { + FrameBuffer fb = new FrameBuffer(width, height, 1); + fb.setDepthBuffer(Image.Format.Depth); + fb.setColorBuffer(Image.Format.RGBA8); + fbs.add(fb); + } + + synchronized (lock){ + img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + + AffineTransform tx = AffineTransform.getScaleInstance(1, -1); + tx.translate(0, -img.getHeight()); + transformOp = new AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); + } + + if (attachAsMain) { + rm.notifyReshape(width, height); + } else { + for (ViewPort vp : viewPorts){ + vp.getCamera().resize(width, height, true); + + // NOTE: Hack alert. This is done ONLY for custom framebuffers. + // Main framebuffer should use RenderManager.notifyReshape(). + for (SceneProcessor sp : vp.getProcessors()){ + sp.reshape(vp, width, height); + } + } + } + } + + @Override + public boolean isInitialized() { + return rm != null; + } + + @Override + public void preFrame(float tpf) { + } + + @Override + public void postQueue(RenderQueue rq) { + } + + @Override + public void invalidate(){ + // For "PaintMode.OnDemand" only. + repaintRequest.set(true); + } + + @Override + public void onFrameBegin() { + if (attachAsMain && rm != null){ + rm.getRenderer().setMainFrameBufferOverride(fbs.get(frameIndex)); + } + } + + @Override + public void onFrameEnd() { + if (reshapeNeeded.getAndSet(false)) { + reshapeInThread(newWidth, newHeight); + } else { + if (!checkVisibilityState()) { + return; + } + + switch (paintMode) { + case Accelerated: + case Repaint: + readNextFrame(); + repaint(); + break; + case OnRequest: + if (repaintRequest.getAndSet(false)) { + readNextFrame(); + repaint(); + } + break; + } + } + } + + public void postFrame(FrameBuffer out) { + } + + public void reshape(ViewPort vp, int w, int h) { + } + + public void cleanup() { + } +}