Procházet zdrojové kódy

Merge branch 'renderer-fbreadasync' into experimental

Kirill Vainer před 9 roky
rodič
revize
c50839796f

+ 280 - 0
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<PixelBuffer> pboPool = new ArrayList<PixelBuffer>();
+    private final List<FrameBufferReadRequest> pending = Collections.synchronizedList(new ArrayList<FrameBufferReadRequest>());
+    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<FrameBufferReadRequest> 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<ByteBuffer> 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;
+    }
+}

+ 98 - 0
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<ByteBuffer> {
+    
+    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);
+        }
+    }
+}
+

+ 27 - 5
jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java

@@ -64,6 +64,7 @@ import java.util.EnumMap;
 import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.List;
+import java.util.concurrent.Future;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.regex.Matcher;
@@ -100,6 +101,7 @@ public final class GLRenderer implements Renderer {
     private final GLExt glext;
     private final GLFbo glfbo;
     private final TextureUtil texUtil;
+    private final AsyncFrameReader frameReader;
 
     public GLRenderer(GL gl, GLExt glext, GLFbo glfbo) {
         this.gl = gl;
@@ -109,6 +111,7 @@ public final class GLRenderer implements Renderer {
         this.glfbo = glfbo;
         this.glext = glext;
         this.texUtil = new TextureUtil(gl, gl2, glext);
+        this.frameReader = new AsyncFrameReader(this, gl, glext, context);
     }
 
     @Override
@@ -879,6 +882,7 @@ public final class GLRenderer implements Renderer {
 
     public void postFrame() {
         objManager.deleteUnused(this);
+        frameReader.updateReadRequests();
         gl.resetStats();
     }
 
@@ -1685,11 +1689,11 @@ public final class GLRenderer implements Renderer {
         }
     }
 
-    public void readFrameBuffer(FrameBuffer fb, ByteBuffer byteBuf) {
-        readFrameBufferWithGLFormat(fb, byteBuf, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE);
+    public Future<ByteBuffer> readFrameBufferLater(FrameBuffer fb, ByteBuffer byteBuf) {
+      return frameReader.readFrameBufferLater(fb, byteBuf);
     }
 
-    private void readFrameBufferWithGLFormat(FrameBuffer fb, ByteBuffer byteBuf, int glFormat, int dataType) {
+    void readFrameBufferWithGLFormat(FrameBuffer fb, ByteBuffer byteBuf, int glFormat, int dataType, int pboId) {
         if (fb != null) {
             RenderBuffer rb = fb.getColorBuffer();
             if (rb == null) {
@@ -1708,12 +1712,30 @@ public final class GLRenderer implements Renderer {
             setFrameBuffer(null);
         }
 
-        gl.glReadPixels(vpX, vpY, vpW, vpH, glFormat, dataType, byteBuf);
+        if (context.boundPixelPackPBO != pboId) {
+            gl.glBindBuffer(GLExt.GL_PIXEL_PACK_BUFFER_ARB, pboId);
+            context.boundPixelPackPBO = pboId;
+        }
+
+        if (byteBuf == null) {
+            gl.glReadPixels(vpX, vpY, vpW, vpH, glFormat, dataType, 0);
+        } else {
+            gl.glReadPixels(vpX, vpY, vpW, vpH, glFormat, dataType, byteBuf);
+        }
+
+        if (context.boundPixelPackPBO != 0) {
+            gl.glBindBuffer(GLExt.GL_PIXEL_PACK_BUFFER_ARB, 0);
+            context.boundPixelPackPBO = 0;
+        }
     }
 
     public void readFrameBufferWithFormat(FrameBuffer fb, ByteBuffer byteBuf, Image.Format format) {
         GLImageFormat glFormat = texUtil.getImageFormatWithError(format, false);
-        readFrameBufferWithGLFormat(fb, byteBuf, glFormat.format, glFormat.dataType);
+        readFrameBufferWithGLFormat(fb, byteBuf, glFormat.format, glFormat.dataType, 0);
+    }
+
+    public void readFrameBuffer(FrameBuffer fb, ByteBuffer byteBuf) {
+        readFrameBufferWithFormat(fb, byteBuf, Image.Format.RGBA8);
     }
 
     private void deleteRenderBuffer(FrameBuffer fb, RenderBuffer rb) {

+ 188 - 82
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<ViewPort> viewPorts = new ArrayList<ViewPort>(); 
@@ -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<Future<ByteBuffer>> pendingFrames = new ArrayBlockingQueue<Future<ByteBuffer>>(NUM_FRAMES);
+    private final ArrayBlockingQueue<ByteBuffer> bufferPool = new ArrayBlockingQueue<ByteBuffer>(NUM_FRAMES);
+    private final ArrayList<FrameBuffer> fbs = new ArrayList<FrameBuffer>(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<ByteBuffer> 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<ByteBuffer> 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) {

+ 18 - 14
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<AwtPanel> panels = new ArrayList<AwtPanel>();
-    protected AwtPanel inputSource;
+    protected ArrayList<JmePanel> panels = new ArrayList<JmePanel>();
+    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();
         }
     }

+ 48 - 0
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();
+}

+ 382 - 0
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<ViewPort> viewPorts = new ArrayList<ViewPort>(); 
+    
+    // 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<Future<ByteBuffer>> pendingFrames = new ArrayBlockingQueue<Future<ByteBuffer>>(NUM_FRAMES);
+    private final ArrayBlockingQueue<ByteBuffer> bufferPool = new ArrayBlockingQueue<ByteBuffer>(NUM_FRAMES);
+    private final ArrayList<FrameBuffer> fbs = new ArrayList<FrameBuffer>(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<ByteBuffer> 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<ByteBuffer> 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() {
+    }
+}