commit 809dcae323a988fae93745560b24b8a713757621
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Thu May 2 23:33:12 2019 +0100

    Finally move the EffectQueues to their own package.

diff --git a/src/main/java/org/distorted/library/effectqueue/EffectQueue.java b/src/main/java/org/distorted/library/effectqueue/EffectQueue.java
new file mode 100644
index 0000000..3a4d414
--- /dev/null
+++ b/src/main/java/org/distorted/library/effectqueue/EffectQueue.java
@@ -0,0 +1,417 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted is free software: you can redistribute it and/or modify                             //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted is distributed in the hope that it will be useful,                                  //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.effectqueue;
+
+import org.distorted.library.effect.Effect;
+import org.distorted.library.effect.EffectName;
+import org.distorted.library.effect.EffectType;
+import org.distorted.library.main.Distorted;
+import org.distorted.library.main.DistortedMaster;
+import org.distorted.library.message.EffectListener;
+import org.distorted.library.message.EffectMessage;
+import org.distorted.library.message.EffectMessageSender;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Not part of public API, do not document
+ *
+ * @y.exclude
+ */
+public abstract class EffectQueue implements DistortedMaster.Slave
+  {
+  static final int MAIN_VARIANTS = 3; // Number of Main program variants (ATM 3: MAIN, MAIN OIT, PREPROCESS)
+
+  private static final int CREATE = 0;
+  private static final int ATTACH = 1;
+  private static final int DETACH = 2;
+  private static final int DETALL = 3;
+
+  int mNumEffects;              // 'ToBe' will be more than mNumEffects if doWork() hasn't
+  private int mNumEffectsToBe;  // added them yet (or less if it hasn't removed some yet)
+  float[] mUniforms;
+  long[] mCurrentDuration;
+  Effect[] mEffects;
+  int[] mName;
+  long mTime=0;
+  ArrayList<EffectListener> mListeners =null;
+  int mNumListeners=0;  // ==mListeners.length(), but we only create mListeners if the first one gets added
+  long mDistortedEffectsID;
+
+  private static int[] mMax = new int[EffectType.LENGTH];
+  private static long mNextID;
+  private static HashMap<ArrayList<Long>,Long> mMapID = new HashMap<>(); // maps lists of Effect IDs (longs) to a
+                                                                         // single long - the queue ID.
+  private long mID;
+  private int mIndex;
+  private boolean mCreated;
+
+  private class Job
+    {
+    int type;
+    int num;
+    boolean notify;
+    Effect effect;
+
+    Job(int t, int m, boolean n, Effect e)
+      {
+      type  = t;
+      num   = m;
+      notify= n;
+      effect= e;
+      }
+    }
+
+  private ArrayList<Job> mJobs = new ArrayList<>();
+
+  static
+    {
+    onDestroy();
+    }
+  
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+  EffectQueue(long id, int numUniforms, int index)
+    {
+    mCreated            = false;
+    mID                 = 0;
+    mNumEffects         = 0;
+    mNumEffectsToBe     = 0;
+    mDistortedEffectsID = id;
+    mIndex              = index;
+
+    mJobs.add(new Job(CREATE,numUniforms,false,null));  // create the stuff that depends on max number
+    DistortedMaster.newSlave(this);                     // of uniforms later, on first render.
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void allocateQueues(EffectQueue[] queues, EffectQueue[] from, int flags, long id)
+    {
+    queues[0] = (flags & Distorted.CLONE_MATRIX     ) != 0 ? from[0] : new EffectQueueMatrix(id);
+    queues[1] = (flags & Distorted.CLONE_VERTEX     ) != 0 ? from[1] : new EffectQueueVertex(id);
+    queues[2] = (flags & Distorted.CLONE_FRAGMENT   ) != 0 ? from[2] : new EffectQueueFragment(id);
+    queues[3] = (flags & Distorted.CLONE_POSTPROCESS) != 0 ? from[3] : new EffectQueuePostprocess(id);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void compute(EffectQueue[] queues, long currTime, float halfW, float halfH, float halfZ )
+    {
+    ((EffectQueueMatrix     )queues[0]).compute(currTime);
+    ((EffectQueueVertex     )queues[1]).compute(currTime,halfW,halfH,halfZ);
+    ((EffectQueueFragment   )queues[2]).compute(currTime,halfW,halfH,halfZ);
+    ((EffectQueuePostprocess)queues[3]).compute(currTime);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void send( EffectQueue[] queues, int width, int height, float distance, float mipmap,
+                           float[] projection, float inflate, float halfW, float halfH, float halfZ, int variant )
+    {
+    ((EffectQueueMatrix  )queues[0]).send(width, height, distance, mipmap, projection, halfW, halfH, halfZ, variant);
+    ((EffectQueueVertex  )queues[1]).send(inflate, variant);
+    ((EffectQueueFragment)queues[2]).send(variant);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static float[] getMVP(EffectQueue[] queues)
+    {
+    return ((EffectQueueMatrix)queues[0]).getMVP();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void getUniforms(int programH, int variant)
+    {
+    EffectQueueFragment.uniforms(programH,variant);
+    EffectQueueVertex  .uniforms(programH,variant);
+    EffectQueueMatrix  .uniforms(programH,variant);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Every effect queue has an ID, which should be the same iff two queues hold the same effects.
+// (this is a speedup: then both queues can be applied once, which seriously speeds up stuff -
+// especially important in case of postprocessing)
+
+  private void regenerateID()
+    {
+    if( mNumEffects>0 )
+      {
+      ArrayList<Long> list = new ArrayList<>();
+      for (int i = 0; i < mNumEffects; i++) list.add(mEffects[i].getID());
+      Long id = mMapID.get(list);
+
+      if( id!=null )
+        {
+        mID = id;
+        }
+      else
+        {
+        mMapID.put(list,mNextID);
+        mID = mNextID++;
+        }
+      }
+    else
+      {
+      mID = 0;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public long getID()
+    {
+    return mID;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static boolean setMax(int index, int m)
+    {
+    if( !Distorted.isInitialized() || m<=mMax[index] )
+      {
+      mMax[index] = m<0 ? 0:m;
+      return true;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getMax(int index)
+    {
+    return mMax[index];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void registerForMessages(EffectListener el)
+    {
+    if( mListeners==null ) mListeners = new ArrayList<>();
+
+    if( !mListeners.contains(el) )
+      {
+      mListeners.add(el);
+      mNumListeners++;
+      }
+    }
+ 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void deregisterForMessages(EffectListener el)
+    {
+    if( mListeners.remove(el) )
+      {
+      mNumListeners--;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void onDestroy()
+    {
+    mNextID = 1;
+    mMapID.clear();
+    EffectType.reset(mMax);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// this assumes 0<=effect<mNumEffects
+
+  protected void remove(int effect)
+    {
+    mNumEffects--;
+
+    long removedID = mEffects[effect].getID();
+
+    for(int j=effect; j<mNumEffects; j++ )
+      {
+      mEffects[j]         = mEffects[j+1];
+      mCurrentDuration[j] = mCurrentDuration[j+1];
+      mName[j]            = mName[j+1];
+      }
+
+    mEffects[mNumEffects] = null;
+
+    for(int i=0; i<mNumListeners; i++)
+      EffectMessageSender.newMessage( mListeners.get(i), EffectMessage.EFFECT_REMOVED, removedID, mDistortedEffectsID);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized int removeByName(EffectName name)
+    {
+    int ret = 0;
+
+    for(int i=0; i<mNumEffects; i++)
+      {
+      if( mEffects[i].getName() == name )
+        {
+        mJobs.add(new Job(DETACH,0,true,mEffects[i]));
+        ret++;
+        }
+      }
+
+    if( ret>0 )
+      {
+      DistortedMaster.newSlave(this);
+      mNumEffectsToBe-=ret;
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized int removeById(long id)
+    {
+    for(int i=0; i<mNumEffects; i++)
+      {
+      if( mEffects[i].getID() == id )
+        {
+        mJobs.add(new Job(DETACH,0,true,mEffects[i]));
+        DistortedMaster.newSlave(this);
+        mNumEffectsToBe--;
+        return 1;
+        }
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized int removeEffect(Effect effect)
+    {
+    for(int i=0; i<mNumEffects; i++)
+      {
+      if( mEffects[i]==effect )
+        {
+        mJobs.add(new Job(DETACH,0,true,mEffects[i]));
+        DistortedMaster.newSlave(this);
+        mNumEffectsToBe--;
+        return 1;
+        }
+      }
+   
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// we do want to notify Listeners if they called 'abortAll' themselves but don't want to notify
+// them if it is the library itself which is releasing resources.
+
+  public synchronized int abortAll(boolean notify)
+    {
+    mJobs.add(new Job(DETALL,0,notify,null));
+    DistortedMaster.newSlave(this);
+    mNumEffectsToBe = 0;
+    return mNumEffects;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  
+  public boolean add(Effect effect)
+    {
+    if( mMax[mIndex]>mNumEffectsToBe || !mCreated )
+      {
+      mJobs.add(new Job(ATTACH,0,false,effect));
+      DistortedMaster.newSlave(this);
+      mNumEffectsToBe++;
+      return true;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void doWork()
+    {
+    boolean changed = false;
+    int num = mJobs.size();
+    Job job;
+
+    for(int i=0; i<num; i++)
+      {
+      job = mJobs.remove(0);
+
+      switch(job.type)
+        {
+        case CREATE: int max = mMax[mIndex];
+                     if( max>0 )
+                       {
+                       mUniforms        = new float[max*job.num];
+                       mCurrentDuration = new long[max];
+                       mEffects         = new Effect[max];
+                       mName            = new int[max];
+                       }
+                     mCreated = true;
+
+                     break;
+        case ATTACH: if( mMax[mIndex]>mNumEffects ) // it is possible that we have first
+                       {                            // added effects and then lowered mMax
+                       mCurrentDuration[mNumEffects] = 0;
+                       mEffects[mNumEffects] = job.effect;
+                       mName[mNumEffects] = job.effect.getName().ordinal();
+                       mNumEffects++;
+                       changed = true;
+                       }
+                     else
+                       {
+                       android.util.Log.e("queue", "failed to add effect "+job.effect.getName());
+                       }
+                     break;
+        case DETACH: for(int j=0; j<mNumEffects; j++)
+                       {
+                       if (mEffects[j] == job.effect)
+                         {
+                         remove(j);
+                         changed = true;
+                         break;
+                         }
+                       }
+                     break;
+        case DETALL: for(int j=0; j<mNumEffects; j++ )
+                       {
+                       changed = true;
+
+                       if( job.notify )
+                         {
+                         for(int k=0; k<mNumListeners; k++)
+                           EffectMessageSender.newMessage( mListeners.get(k), EffectMessage.EFFECT_REMOVED, mEffects[j].getID(), mDistortedEffectsID);
+                         }
+
+                       mEffects[j] = null;
+                       }
+
+                     mNumEffects= 0;
+                     break;
+        }
+      }
+
+    if( changed && mIndex==EffectType.POSTPROCESS.ordinal() ) regenerateID();
+    }
+  }
diff --git a/src/main/java/org/distorted/library/effectqueue/EffectQueueFragment.java b/src/main/java/org/distorted/library/effectqueue/EffectQueueFragment.java
new file mode 100644
index 0000000..b112f49
--- /dev/null
+++ b/src/main/java/org/distorted/library/effectqueue/EffectQueueFragment.java
@@ -0,0 +1,94 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted is free software: you can redistribute it and/or modify                             //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted is distributed in the hope that it will be useful,                                  //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.effectqueue;
+
+import android.opengl.GLES31;
+
+import org.distorted.library.effect.EffectType;
+import org.distorted.library.effect.FragmentEffect;
+import org.distorted.library.message.EffectMessage;
+import org.distorted.library.message.EffectMessageSender;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class EffectQueueFragment extends EffectQueue
+  {
+  private static final int NUM_UNIFORMS = FragmentEffect.NUM_UNIFORMS;
+  private static final int INDEX = EffectType.FRAGMENT.ordinal();
+
+  private static int[] mNumEffectsH = new int[MAIN_VARIANTS];
+  private static int[] mNameH       = new int[MAIN_VARIANTS];
+  private static int[] mUniformsH   = new int[MAIN_VARIANTS];
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+  EffectQueueFragment(long id)
+    { 
+    super(id,NUM_UNIFORMS,INDEX);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void uniforms(int mProgramH, int variant)
+    {
+    mNumEffectsH[variant]= GLES31.glGetUniformLocation( mProgramH, "fNumEffects");
+    mNameH[variant]      = GLES31.glGetUniformLocation( mProgramH, "fName");
+    mUniformsH[variant]  = GLES31.glGetUniformLocation( mProgramH, "fUniforms");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  
+  void compute(long currTime,float halfX, float halfY, float halfZ)
+    { 
+    if( currTime==mTime ) return;
+    if( mTime==0 ) mTime = currTime;
+    long step = (currTime-mTime);
+
+    for(int i=0; i<mNumEffects; i++)
+      {
+      mCurrentDuration[i] += step;
+
+      if( mEffects[i].compute(mUniforms, NUM_UNIFORMS*i, mCurrentDuration[i], step) )
+        {
+        for(int j=0; j<mNumListeners; j++)
+          EffectMessageSender.newMessage( mListeners.get(j), EffectMessage.EFFECT_FINISHED, mEffects[i].getID(), mDistortedEffectsID);
+        }
+
+      mUniforms[NUM_UNIFORMS*i+5] -= halfX;
+      mUniforms[NUM_UNIFORMS*i+6] -= halfY;
+      mUniforms[NUM_UNIFORMS*i+7] -= halfZ;
+      }
+
+    mTime = currTime;  
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  
+  void send(int variant)
+    {
+    GLES31.glUniform1i( mNumEffectsH[variant], mNumEffects);
+
+    if( mNumEffects>0 )
+      {
+      GLES31.glUniform1iv( mNameH[variant]    ,                 mNumEffects, mName    ,0);
+      GLES31.glUniform4fv( mUniformsH[variant],(NUM_UNIFORMS/4)*mNumEffects, mUniforms,0);
+      }  
+    }
+  }
diff --git a/src/main/java/org/distorted/library/effectqueue/EffectQueueMatrix.java b/src/main/java/org/distorted/library/effectqueue/EffectQueueMatrix.java
new file mode 100644
index 0000000..d2c5cac
--- /dev/null
+++ b/src/main/java/org/distorted/library/effectqueue/EffectQueueMatrix.java
@@ -0,0 +1,163 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted is free software: you can redistribute it and/or modify                             //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted is distributed in the hope that it will be useful,                                  //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.effectqueue;
+
+import android.opengl.GLES31;
+import android.opengl.Matrix;
+
+import org.distorted.library.effect.EffectType;
+import org.distorted.library.effect.MatrixEffect;
+import org.distorted.library.message.EffectMessage;
+import org.distorted.library.message.EffectMessageSender;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class EffectQueueMatrix extends EffectQueue
+  {   
+  private static final int NUM_UNIFORMS = MatrixEffect.NUM_UNIFORMS;
+  private static final int INDEX = EffectType.MATRIX.ordinal();
+
+  private static float[] mMVPMatrix = new float[16];
+  private static float[] mViewMatrix= new float[16];
+
+  private static int[] mObjDH      = new int[MAIN_VARIANTS];
+  private static int[] mMVPMatrixH = new int[MAIN_VARIANTS];
+  private static int[] mMVMatrixH  = new int[MAIN_VARIANTS];
+
+  private static float[] mTmpMatrix = new float[16];
+  private static float[] mTmpResult = new float[4];
+  private static float[] mTmpPoint  = new float[4];
+  private static float mMinX,mMaxX,mMinY,mMaxY;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+  EffectQueueMatrix(long id)
+    { 
+    super(id,NUM_UNIFORMS,INDEX );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void uniforms(int mProgramH, int variant)
+    {
+    mObjDH[variant]     = GLES31.glGetUniformLocation(mProgramH, "u_objD");
+    mMVPMatrixH[variant]= GLES31.glGetUniformLocation(mProgramH, "u_MVPMatrix");
+    mMVMatrixH[variant] = GLES31.glGetUniformLocation(mProgramH, "u_MVMatrix");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void magnifyDir()
+    {
+    Matrix.multiplyMV(mTmpResult,0,mTmpMatrix,0,mTmpPoint,0);
+    float nx = mTmpResult[0]/mTmpResult[3];
+    float ny = mTmpResult[1]/mTmpResult[3];
+
+    if( nx<mMinX ) mMinX = nx;
+    if( nx>mMaxX ) mMaxX = nx;
+    if( ny<mMinY ) mMinY = ny;
+    if( ny>mMaxY ) mMaxY = ny;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// return a float which describes how much larger an object must be so that it appears to be (about)
+// 'marginInPixels' pixels larger in each direction. Used in Postprocessing.
+
+  float magnify(float[] projection, int width, int height, float mipmap, float halfX, float halfY, float halfZ, float marginInPixels)
+    {
+    mMinX = Integer.MAX_VALUE;
+    mMaxX = Integer.MIN_VALUE;
+    mMinY = Integer.MAX_VALUE;
+    mMaxY = Integer.MIN_VALUE;
+
+    mTmpPoint[3] = 1.0f;
+
+    Matrix.multiplyMM(mTmpMatrix, 0, projection, 0, mViewMatrix, 0);
+
+    mTmpPoint[0] = +halfX; mTmpPoint[1] = +halfY; mTmpPoint[2] = +halfZ; magnifyDir();
+    mTmpPoint[0] = +halfX; mTmpPoint[1] = +halfY; mTmpPoint[2] = -halfZ; magnifyDir();
+    mTmpPoint[0] = +halfX; mTmpPoint[1] = -halfY; mTmpPoint[2] = +halfZ; magnifyDir();
+    mTmpPoint[0] = +halfX; mTmpPoint[1] = -halfY; mTmpPoint[2] = -halfZ; magnifyDir();
+    mTmpPoint[0] = -halfX; mTmpPoint[1] = +halfY; mTmpPoint[2] = +halfZ; magnifyDir();
+    mTmpPoint[0] = -halfX; mTmpPoint[1] = +halfY; mTmpPoint[2] = -halfZ; magnifyDir();
+    mTmpPoint[0] = -halfX; mTmpPoint[1] = -halfY; mTmpPoint[2] = +halfZ; magnifyDir();
+    mTmpPoint[0] = -halfX; mTmpPoint[1] = -halfY; mTmpPoint[2] = -halfZ; magnifyDir();
+
+    float xLenInPixels = width *(mMaxX-mMinX)/2;
+    float yLenInPixels = height*(mMaxY-mMinY)/2;
+
+    // already margin / avg(xLen,yLen) is the size of the halo.
+    // Here we need a bit more because we are marking not only the halo, but a little bit around
+    // it as well so that the (blur for example) will be smooth on the edges. Thus the 2.0f.
+    // ( 4.0 because there is an extra 2.0 from the avg(xLen,yLen) )
+    //
+    // mMipmap ( 1.0, 0.5, 0.25, 0.125 ) - we need to make the size of the halo independent
+    // of postprocessing effect quality.
+
+    return mipmap*4.0f*marginInPixels/( xLenInPixels+yLenInPixels );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void compute(long currTime)
+    {
+    if( currTime==mTime ) return;
+    if( mTime==0 ) mTime = currTime;
+    long step = (currTime-mTime);
+
+    for(int i=0; i<mNumEffects; i++)
+      {
+      mCurrentDuration[i] += step;
+
+      if( mEffects[i].compute(mUniforms, NUM_UNIFORMS*i, mCurrentDuration[i], step) )
+        {
+        for(int j=0; j<mNumListeners; j++)
+          EffectMessageSender.newMessage( mListeners.get(j), EffectMessage.EFFECT_FINISHED, mEffects[i].getID(), mDistortedEffectsID);
+        }
+      }
+     
+    mTime = currTime;  
+    }  
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float[] getMVP()
+    {
+    return mMVPMatrix;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void send(int width, int height, float distance, float mipmap, float[] projection, float halfX, float halfY, float halfZ, int variant)
+    {
+    Matrix.setIdentityM(mViewMatrix, 0);
+    Matrix.translateM(mViewMatrix, 0, -width*0.5f, -height*0.5f, -distance);
+    if( mipmap!=1 ) Matrix.scaleM(mViewMatrix, 0, mipmap, mipmap, mipmap);
+
+    for(int i=0; i<mNumEffects; i++) ((MatrixEffect)mEffects[i]).apply(mViewMatrix,mUniforms,i);
+
+    Matrix.translateM(mViewMatrix, 0, halfX,halfY,halfZ);
+    Matrix.multiplyMM(mMVPMatrix, 0, projection, 0, mViewMatrix, 0);
+
+    GLES31.glUniform3f( mObjDH[variant] , halfX, halfY, halfZ);
+    GLES31.glUniformMatrix4fv(mMVMatrixH[variant] , 1, false, mViewMatrix, 0);
+    GLES31.glUniformMatrix4fv(mMVPMatrixH[variant], 1, false, mMVPMatrix , 0);
+    }
+  }
diff --git a/src/main/java/org/distorted/library/effectqueue/EffectQueuePostprocess.java b/src/main/java/org/distorted/library/effectqueue/EffectQueuePostprocess.java
new file mode 100644
index 0000000..fcd4649
--- /dev/null
+++ b/src/main/java/org/distorted/library/effectqueue/EffectQueuePostprocess.java
@@ -0,0 +1,234 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted is free software: you can redistribute it and/or modify                             //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted is distributed in the hope that it will be useful,                                  //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.effectqueue;
+
+import android.content.res.Resources;
+import android.opengl.GLES31;
+import android.util.Log;
+
+import org.distorted.library.R;
+import org.distorted.library.effect.EffectType;
+import org.distorted.library.effect.PostprocessEffect;
+import org.distorted.library.effect.VertexEffect;
+import org.distorted.library.main.Distorted;
+import org.distorted.library.main.DistortedFramebuffer;
+import org.distorted.library.main.DistortedNode;
+import org.distorted.library.main.DistortedOutputSurface;
+import org.distorted.library.main.DistortedRenderState;
+import org.distorted.library.main.DistortedSurface;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.library.message.EffectMessage;
+import org.distorted.library.message.EffectMessageSender;
+import org.distorted.library.program.DistortedProgram;
+
+import java.io.InputStream;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Not part of public API, do not document
+ *
+ * @y.exclude
+ */
+public class EffectQueuePostprocess extends EffectQueue
+  {
+  private static final int NUM_UNIFORMS = PostprocessEffect.NUM_UNIFORMS;
+  private static final int INDEX = EffectType.POSTPROCESS.ordinal();
+
+  private int mHalo;
+  private float mR, mG, mB, mA;
+
+  private static DistortedProgram mPreProgram;
+  private static int mPreColorH;
+  private static int mPreTextureH;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  EffectQueuePostprocess(long id)
+    { 
+    super(id,NUM_UNIFORMS,INDEX );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void compute(long currTime)
+    {
+    if( currTime==mTime ) return;
+    if( mTime==0 ) mTime = currTime;
+    long step = (currTime-mTime);
+
+    mR = mG = mB = mA = 0.0f;
+    mHalo = 0;
+    int halo;
+
+    for(int i=0; i<mNumEffects; i++)
+      {
+      mCurrentDuration[i] += step;
+
+      // first zero out the 'alpha' because BLUR effect will not overwrite this (it is a 1D effect)
+      // and if previously there was a GLOW effect here then mA would be non-zero and we don't want
+      // that (see preprocess())
+      mUniforms[NUM_UNIFORMS*i+4]=0.0f;
+
+      if( mEffects[i].compute(mUniforms, NUM_UNIFORMS*i, mCurrentDuration[i], step) )
+        {
+        for(int j=0; j<mNumListeners; j++)
+          EffectMessageSender.newMessage( mListeners.get(j), EffectMessage.EFFECT_FINISHED, mEffects[i].getID(), mDistortedEffectsID);
+        }
+
+      halo = (int)mUniforms[NUM_UNIFORMS*i];
+      if( halo>mHalo ) mHalo = halo;
+      }
+
+    // TODO  (now only really works in case of 1 effect!)
+    if( mNumEffects>0 )
+      {
+      mR = mUniforms[1];
+      mG = mUniforms[2];
+      mB = mUniforms[3];
+      mA = mUniforms[4];
+      }
+
+    mTime = currTime;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void createPrograms(Resources resources)
+    {
+    final InputStream mainVertStream = resources.openRawResource(R.raw.main_vertex_shader);
+    final InputStream mainFragStream = resources.openRawResource(R.raw.preprocess_fragment_shader);
+
+    int numV = VertexEffect.getNumEnabled();
+
+    String mainVertHeader= Distorted.GLSL_VERSION + ("#define NUM_VERTEX "   + ( numV>0 ? Distorted.getMax(EffectType.VERTEX  ) : 0 ) + "\n");
+    String mainFragHeader= Distorted.GLSL_VERSION + "\n";
+
+    String enabledEffectV= VertexEffect.getGLSL();
+
+    try
+      {
+      mPreProgram = new DistortedProgram(mainVertStream, mainFragStream, mainVertHeader, mainFragHeader,
+                                         enabledEffectV, null, Distorted.GLSL, null);
+      }
+    catch(Exception e)
+      {
+      Log.e("POSTPROCESS", e.getClass().getSimpleName()+" trying to compile PRE program: "+e.getMessage());
+      throw new RuntimeException(e.getMessage());
+      }
+
+    int preProgramH = mPreProgram.getProgramHandle();
+    EffectQueueVertex.getUniforms( preProgramH,2 );
+    EffectQueueMatrix.getUniforms( preProgramH,2 );
+    mPreColorH  = GLES31.glGetUniformLocation( preProgramH, "u_Color"  );
+    mPreTextureH= GLES31.glGetUniformLocation( preProgramH, "u_Texture");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// TODO  (now only really works in case of 1 effect!)
+
+  public int getQuality()
+    {
+    return mNumEffects>0 ? ((PostprocessEffect)mEffects[0]).getQuality() : 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// TODO  (now only really works in case of 1 effect!)
+
+  public boolean getRender()
+    {
+    return mNumEffects>0 ? ((PostprocessEffect)mEffects[0]).getRender() : false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int preprocess(DistortedOutputSurface buffer, DistortedNode node, float distance, float mipmap, float[] projection)
+    {
+    buffer.setAsOutput();
+    DistortedSurface input = node.getInternalSurface();
+
+    if( input.setAsInput() )
+      {
+      MeshBase mesh = node.getMesh();
+
+      float halfW = input.getWidth() / 2.0f;
+      float halfH = input.getHeight()/ 2.0f;
+      float halfZ = halfW*mesh.getZFactor();
+
+      int width = buffer.getWidth();
+      int height = buffer.getHeight();
+
+      DistortedRenderState.setUpStencilMark(mA!=0.0f);
+      DistortedRenderState.disableBlending();
+
+      GLES31.glViewport(0, 0, width, height );
+
+      mPreProgram.useProgram();
+
+      mesh.bindVertexAttribs(mPreProgram);
+
+      EffectQueue[] queues = node.getEffects().getQueues();
+      EffectQueueMatrix matrix = (EffectQueueMatrix)queues[0];
+      EffectQueueVertex vertex = (EffectQueueVertex)queues[1];
+
+      float inflate=0.0f;
+
+      matrix.send(width, height, distance, mipmap, projection, halfW, halfH, halfZ, 2);
+
+      if( mHalo!=0.0f )
+        {
+        inflate = matrix.magnify(projection, width, height, mipmap, halfW, halfH, halfZ, mHalo);
+        }
+
+      vertex.send(inflate,2);
+
+      if( mA!=0.0f )
+        {
+        GLES31.glUniform4f(mPreColorH, mR, mG, mB, mA);
+        GLES31.glUniform1i(mPreTextureH, 0);
+        }
+
+      GLES31.glDrawArrays(GLES31.GL_TRIANGLE_STRIP, 0, mesh.getNumVertices() );
+
+      DistortedRenderState.restoreBlending();
+      DistortedRenderState.unsetUpStencilMark();
+
+      return 1;
+      }
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int postprocess(DistortedFramebuffer buffer)
+    {
+    int numRenders = 0;
+
+    GLES31.glDisable(GLES31.GL_BLEND);
+
+    for(int i=0; i<mNumEffects; i++)
+      {
+      numRenders += ((PostprocessEffect)mEffects[i]).apply(mUniforms,NUM_UNIFORMS*i, buffer);
+      }
+
+    GLES31.glEnable(GLES31.GL_BLEND);
+
+    return numRenders;
+    }
+  }
diff --git a/src/main/java/org/distorted/library/effectqueue/EffectQueueVertex.java b/src/main/java/org/distorted/library/effectqueue/EffectQueueVertex.java
new file mode 100644
index 0000000..7ee0d1c
--- /dev/null
+++ b/src/main/java/org/distorted/library/effectqueue/EffectQueueVertex.java
@@ -0,0 +1,97 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted is free software: you can redistribute it and/or modify                             //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted is distributed in the hope that it will be useful,                                  //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.effectqueue;
+
+import android.opengl.GLES31;
+
+import org.distorted.library.effect.EffectType;
+import org.distorted.library.effect.VertexEffect;
+import org.distorted.library.message.EffectMessage;
+import org.distorted.library.message.EffectMessageSender;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class EffectQueueVertex extends EffectQueue
+  { 
+  private static final int NUM_UNIFORMS = VertexEffect.NUM_UNIFORMS;
+  private static final int INDEX = EffectType.VERTEX.ordinal();
+
+  private static int[] mNumEffectsH = new int[MAIN_VARIANTS];
+  private static int[] mNameH       = new int[MAIN_VARIANTS];
+  private static int[] mUniformsH   = new int[MAIN_VARIANTS];
+  private static int[] mInflateH    = new int[MAIN_VARIANTS];
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+  EffectQueueVertex(long id)
+    { 
+    super(id,NUM_UNIFORMS,INDEX);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void uniforms(int mProgramH, int variant)
+    {
+    mNumEffectsH[variant]= GLES31.glGetUniformLocation( mProgramH, "vNumEffects");
+    mNameH[variant]      = GLES31.glGetUniformLocation( mProgramH, "vName");
+    mUniformsH[variant]  = GLES31.glGetUniformLocation( mProgramH, "vUniforms");
+    mInflateH[variant]   = GLES31.glGetUniformLocation( mProgramH, "u_Inflate");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  
+  void compute(long currTime,float halfX, float halfY, float halfZ)
+    {
+    if( currTime==mTime ) return;
+    if( mTime==0 ) mTime = currTime;
+    long step = (currTime-mTime);
+
+    for(int i=0; i<mNumEffects; i++)
+      {
+      mCurrentDuration[i] += step;
+
+      if( mEffects[i].compute(mUniforms, NUM_UNIFORMS*i, mCurrentDuration[i], step) )
+        {
+        for(int j=0; j<mNumListeners; j++)
+          EffectMessageSender.newMessage( mListeners.get(j), EffectMessage.EFFECT_FINISHED, mEffects[i].getID(), mDistortedEffectsID);
+        }
+
+      mUniforms[NUM_UNIFORMS*i+5] -= halfX;
+      mUniforms[NUM_UNIFORMS*i+6] -= halfY;
+      mUniforms[NUM_UNIFORMS*i+7] -= halfZ;
+      }
+
+    mTime = currTime;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void send(float inflate, int variant)
+    {
+    GLES31.glUniform1i( mNumEffectsH[variant], mNumEffects);
+    GLES31.glUniform1f( mInflateH[variant]   , inflate    );
+
+    if( mNumEffects>0 )
+      {
+      GLES31.glUniform1iv( mNameH[variant]    ,                 mNumEffects, mName    ,0);
+      GLES31.glUniform4fv( mUniformsH[variant],(NUM_UNIFORMS/4)*mNumEffects, mUniforms,0);
+      }
+    }
+  }
diff --git a/src/main/java/org/distorted/library/main/Distorted.java b/src/main/java/org/distorted/library/main/Distorted.java
index 11f61a9..961c5cc 100644
--- a/src/main/java/org/distorted/library/main/Distorted.java
+++ b/src/main/java/org/distorted/library/main/Distorted.java
@@ -28,6 +28,8 @@ import android.util.Log;
 
 import org.distorted.library.R;
 import org.distorted.library.effect.Effect;
+import org.distorted.library.effectqueue.EffectQueue;
+import org.distorted.library.effectqueue.EffectQueuePostprocess;
 import org.distorted.library.effect.EffectType;
 import org.distorted.library.effect.FragmentEffect;
 import org.distorted.library.effect.PostprocessEffect;
@@ -100,10 +102,6 @@ public class Distorted
    * https://community.arm.com/graphics/f/discussions/10285/opengl-es-3-1-on-mali-t880-flashes
    */
   static final int FBO_QUEUE_SIZE = 4;
-  /**
-   * Number of Main program variants (ATM 3: MAIN, MAIN OIT, PREPROCESS)
-   */
-  static final int MAIN_VARIANTS = 3;
 
   private static boolean mInitialized=false;
 
@@ -200,7 +198,7 @@ public class Distorted
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  static void createPrograms(Resources resources)
+  private static void createPrograms(Resources resources)
     {
     // MAIN PROGRAM ////////////////////////////////////
     final InputStream mainVertStream = resources.openRawResource(R.raw.main_vertex_shader);
@@ -289,7 +287,7 @@ public class Distorted
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  static void createProgramsOIT(Resources resources)
+  private static void createProgramsOIT(Resources resources)
     {
     // MAIN OIT PROGRAM ////////////////////////////////
     final InputStream mainVertStream = resources.openRawResource(R.raw.main_vertex_shader);
@@ -406,7 +404,6 @@ public class Distorted
     mOITRenderSizeH         = GLES31.glGetUniformLocation( oitRenderProgramH, "u_Size");
     }
 
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   private static void displayNormals(EffectQueue[] queues, MeshBase mesh)
@@ -445,15 +442,20 @@ public class Distorted
 
     mesh.bindVertexAttribs(Distorted.mMainOITProgram);
 
-    float inflate = mesh.getInflate();
+    float inflate     = mesh.getInflate();
+    int width         = surface.mWidth;
+    int height        = surface.mHeight;
+    float distance    = surface.mDistance;
+    float mipmap      = surface.mMipmap;
+    float[] projection= surface.mProjectionMatrix;
 
-    EffectQueue.send(queues, surface, inflate, halfW, halfH, halfZ, 1 );
+    EffectQueue.send(queues, width, height, distance, mipmap, projection, inflate, halfW, halfH, halfZ, 1 );
     GLES31.glDrawArrays(GLES31.GL_TRIANGLE_STRIP, 0, mesh.getNumVertices() );
 
     if( mesh.getShowNormals() )
       {
       Distorted.mMainProgram.useProgram();
-      EffectQueue.send(queues, surface, inflate, halfW, halfH, halfZ, 0 );
+      EffectQueue.send(queues, width, height, distance, mipmap, projection, inflate, halfW, halfH, halfZ, 0 );
       displayNormals(queues,mesh);
       }
     }
@@ -469,7 +471,15 @@ public class Distorted
     Distorted.mMainProgram.useProgram();
     GLES31.glUniform1i(Distorted.mMainTextureH, 0);
     mesh.bindVertexAttribs(Distorted.mMainProgram);
-    EffectQueue.send(queues, surface, mesh.getInflate(), halfW, halfH, halfZ, 0 );
+
+    float inflate     = mesh.getInflate();
+    int width         = surface.mWidth;
+    int height        = surface.mHeight;
+    float distance    = surface.mDistance;
+    float mipmap      = surface.mMipmap;
+    float[] projection= surface.mProjectionMatrix;
+
+    EffectQueue.send(queues, width, height, distance, mipmap, projection, inflate, halfW, halfH, halfZ, 0 );
     GLES31.glDrawArrays(GLES31.GL_TRIANGLE_STRIP, 0, mesh.getNumVertices() );
 
     if( mesh.getShowNormals() ) displayNormals(queues,mesh);
@@ -581,7 +591,7 @@ public class Distorted
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
-// Pass1 of the OIT algorithm. Clear per-pixel head-poiners.
+// Pass1 of the OIT algorithm. Clear per-pixel head-pointers.
 
   static void oitClear(DistortedOutputSurface surface, int counter)
     {
diff --git a/src/main/java/org/distorted/library/main/DistortedEffects.java b/src/main/java/org/distorted/library/main/DistortedEffects.java
index 10443b4..e117bd3 100644
--- a/src/main/java/org/distorted/library/main/DistortedEffects.java
+++ b/src/main/java/org/distorted/library/main/DistortedEffects.java
@@ -21,6 +21,7 @@ package org.distorted.library.main;
 
 import org.distorted.library.effect.Effect;
 import org.distorted.library.effect.EffectName;
+import org.distorted.library.effectqueue.EffectQueue;
 import org.distorted.library.effect.EffectType;
 import org.distorted.library.message.EffectListener;
 
@@ -37,8 +38,10 @@ public class DistortedEffects
   private EffectQueue[] mQueues;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  EffectQueue[] getQueues()
+/**
+ * @y.exclude
+ */
+  public EffectQueue[] getQueues()
     {
     return mQueues;
     }
@@ -98,7 +101,6 @@ public class DistortedEffects
  * 
  * @param el A class implementing the EffectListener interface that wants to get notifications.
  */
-  @SuppressWarnings("unused")
   public void registerForMessages(EffectListener el)
     {
     for( int i=0; i<EffectType.LENGTH; i++)
@@ -113,7 +115,6 @@ public class DistortedEffects
  * 
  * @param el A class implementing the EffectListener interface that no longer wants to get notifications.
  */
-  @SuppressWarnings("unused")
   public void deregisterForMessages(EffectListener el)
     {
     for( int i=0; i<EffectType.LENGTH; i++)
diff --git a/src/main/java/org/distorted/library/main/DistortedOutputSurface.java b/src/main/java/org/distorted/library/main/DistortedOutputSurface.java
index 6ee376a..2acbecd 100644
--- a/src/main/java/org/distorted/library/main/DistortedOutputSurface.java
+++ b/src/main/java/org/distorted/library/main/DistortedOutputSurface.java
@@ -23,6 +23,7 @@ import android.opengl.GLES31;
 import android.opengl.Matrix;
 
 import org.distorted.library.effect.EffectQuality;
+import org.distorted.library.effectqueue.EffectQueuePostprocess;
 import org.distorted.library.mesh.MeshBase;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -377,8 +378,9 @@ public abstract class DistortedOutputSurface extends DistortedSurface implements
 
   int renderChildren(long time, int numChildren, DistortedChildrenList children, int fbo, boolean oit)
     {
-    int quality=0, numRenders=0, bucketChange=0;
+    int numRenders=0, bucketChange=0;
     DistortedNode child;
+    DistortedFramebuffer buffer=null;
     EffectQueuePostprocess lastQueue=null, currQueue;
     long lastBucket=0, currBucket;
     boolean renderDirectly=false;
@@ -431,22 +433,22 @@ public abstract class DistortedOutputSurface extends DistortedSurface implements
             }
           else
             {
-            for(int j=bucketChange; j<i; j++) numRenders += lastQueue.preprocess( mBuffer[quality],children.getChild(j) );
-            numRenders += lastQueue.postprocess(mBuffer[quality]);
+            for(int j=bucketChange; j<i; j++) numRenders += lastQueue.preprocess( buffer,children.getChild(j), buffer.mDistance, buffer.mMipmap, buffer.mProjectionMatrix );
+            numRenders += lastQueue.postprocess(buffer);
 
             if( oit )
               {
-              numRenders += oitBuild(time, mBuffer[quality], fbo);
+              numRenders += oitBuild(time, buffer, fbo);
               GLES31.glMemoryBarrier(GLES31.GL_SHADER_STORAGE_BARRIER_BIT | GLES31.GL_ATOMIC_COUNTER_BARRIER_BIT);
               }
             else
               {
-              numRenders += blitWithDepth(time, mBuffer[quality],fbo);
+              numRenders += blitWithDepth(time, buffer, fbo);
               }
-            mBuffer[quality].clearBuffer(fbo);
+            buffer.clearBuffer(fbo);
             }
 
-          quality= currQueue.getQuality();
+          buffer= mBuffer[currQueue.getQuality()];
           bucketChange= i;
           renderDirectly = currQueue.getRender();
           }
@@ -467,24 +469,24 @@ public abstract class DistortedOutputSurface extends DistortedSurface implements
           }
         else
           {
-          mBuffer[quality].setAsOutput(time);
-          child.drawNoBlend(time, mBuffer[quality]);
+          buffer.setAsOutput(time);
+          child.drawNoBlend(time, buffer);
           }
 
         if( i==numChildren-1 )
           {
-          for(int j=bucketChange; j<numChildren; j++) numRenders += currQueue.preprocess( mBuffer[quality],children.getChild(j) );
-          numRenders += currQueue.postprocess(mBuffer[quality]);
+          for(int j=bucketChange; j<numChildren; j++) numRenders += currQueue.preprocess( buffer,children.getChild(j), buffer.mDistance, buffer.mMipmap, buffer.mProjectionMatrix );
+          numRenders += currQueue.postprocess(buffer);
 
           if( oit )
             {
-            numRenders += oitBuild(time, mBuffer[quality], fbo);
+            numRenders += oitBuild(time, buffer, fbo);
             GLES31.glMemoryBarrier(GLES31.GL_SHADER_STORAGE_BARRIER_BIT | GLES31.GL_ATOMIC_COUNTER_BARRIER_BIT);
-            mBuffer[quality].clearBuffer(fbo);
+            buffer.clearBuffer(fbo);
             }
           else
             {
-            numRenders += blitWithDepth(time, mBuffer[quality],fbo);
+            numRenders += blitWithDepth(time, buffer,fbo);
             }
           }
         } // end else (postprocessed child)
@@ -550,6 +552,19 @@ public abstract class DistortedOutputSurface extends DistortedSurface implements
     DistortedRenderState.colorDepthStencilRestore();
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setAsOutput(long time)
+    {
+    GLES31.glBindFramebuffer(GLES31.GL_FRAMEBUFFER, mFBOH[mCurrFBO]);
+
+    if( mTime[mCurrFBO]!=time )
+      {
+      mTime[mCurrFBO] = time;
+      clear();
+      }
+    }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // PUBLIC API
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -600,26 +615,6 @@ public abstract class DistortedOutputSurface extends DistortedSurface implements
     return numRenders;
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Bind this Surface as a Framebuffer we can render to.
- *
- * @param time Present time, in milliseconds. The point: looking at this param the library can figure
- *             out if this is the first time during present frame that this FBO is being set as output.
- *             If so, the library, in addition to binding the Surface for output, also clears the
- *             Surface's color and depth attachments.
- */
-  public void setAsOutput(long time)
-    {
-    GLES31.glBindFramebuffer(GLES31.GL_FRAMEBUFFER, mFBOH[mCurrFBO]);
-
-    if( mTime[mCurrFBO]!=time )
-      {
-      mTime[mCurrFBO] = time;
-      clear();
-      }
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 /**
  * Bind this Surface as a Framebuffer we can render to.
diff --git a/src/main/java/org/distorted/library/main/DistortedRenderState.java b/src/main/java/org/distorted/library/main/DistortedRenderState.java
index a8589df..e67f1b4 100644
--- a/src/main/java/org/distorted/library/main/DistortedRenderState.java
+++ b/src/main/java/org/distorted/library/main/DistortedRenderState.java
@@ -186,7 +186,7 @@ public class DistortedRenderState
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  static void disableBlending()
+  public static void disableBlending()
     {
     sState.blend = cState.blend;
 
@@ -199,7 +199,7 @@ public class DistortedRenderState
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  static void restoreBlending()
+  public static void restoreBlending()
     {
     if (sState.blend != cState.blend)
       {
@@ -427,7 +427,7 @@ public class DistortedRenderState
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  static void setUpStencilMark(boolean color)
+  public static void setUpStencilMark(boolean color)
     {
     sState.stencilTest = cState.stencilTest;
 
@@ -502,7 +502,7 @@ public class DistortedRenderState
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  static void unsetUpStencilMark()
+  public static void unsetUpStencilMark()
     {
     if( sState.stencilTest!=cState.stencilTest )
       {
diff --git a/src/main/java/org/distorted/library/main/EffectQueue.java b/src/main/java/org/distorted/library/main/EffectQueue.java
deleted file mode 100644
index cfa81ad..0000000
--- a/src/main/java/org/distorted/library/main/EffectQueue.java
+++ /dev/null
@@ -1,422 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted is free software: you can redistribute it and/or modify                             //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted is distributed in the hope that it will be useful,                                  //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library.main;
-
-import org.distorted.library.effect.Effect;
-import org.distorted.library.effect.EffectName;
-import org.distorted.library.effect.EffectType;
-import org.distorted.library.message.EffectListener;
-import org.distorted.library.message.EffectMessage;
-import org.distorted.library.message.EffectMessageSender;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-abstract class EffectQueue implements DistortedMaster.Slave
-  {
-  private static final int CREATE = 0;
-  private static final int ATTACH = 1;
-  private static final int DETACH = 2;
-  private static final int DETALL = 3;
-
-  int mNumEffects;              // 'ToBe' will be more than mNumEffects if doWork() hasn't
-  private int mNumEffectsToBe;  // added them yet (or less if it hasn't removed some yet)
-  float[] mUniforms;
-  long[] mCurrentDuration;
-  Effect[] mEffects;
-  int[] mName;
-  long mTime=0;
-  ArrayList<EffectListener> mListeners =null;
-  int mNumListeners=0;  // ==mListeners.length(), but we only create mListeners if the first one gets added
-  long mDistortedEffectsID;
-
-  private static int[] mMax = new int[EffectType.LENGTH];
-  private static long mNextID;
-  private static HashMap<ArrayList<Long>,Long> mMapID = new HashMap<>(); // maps lists of Effect IDs (longs) to a
-                                                                         // single long - the queue ID.
-  private long mID;
-  private int mIndex;
-  private boolean mCreated;
-
-  private class Job
-    {
-    int type;
-    int num;
-    boolean notify;
-    Effect effect;
-
-    Job(int t, int m, boolean n, Effect e)
-      {
-      type  = t;
-      num   = m;
-      notify= n;
-      effect= e;
-      }
-    }
-
-  private ArrayList<Job> mJobs = new ArrayList<>();
-
-  static
-    {
-    onDestroy();
-    }
-  
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-  EffectQueue(long id, int numUniforms, int index)
-    {
-    mCreated            = false;
-    mID                 = 0;
-    mNumEffects         = 0;
-    mNumEffectsToBe     = 0;
-    mDistortedEffectsID = id;
-    mIndex              = index;
-
-    mJobs.add(new Job(CREATE,numUniforms,false,null));  // create the stuff that depends on max number
-    DistortedMaster.newSlave(this);                     // of uniforms later, on first render.
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void allocateQueues(EffectQueue[] queues, EffectQueue[] from, int flags, long id)
-    {
-    queues[0] = (flags & Distorted.CLONE_MATRIX     ) != 0 ? from[0] : new EffectQueueMatrix(id);
-    queues[1] = (flags & Distorted.CLONE_VERTEX     ) != 0 ? from[1] : new EffectQueueVertex(id);
-    queues[2] = (flags & Distorted.CLONE_FRAGMENT   ) != 0 ? from[2] : new EffectQueueFragment(id);
-    queues[3] = (flags & Distorted.CLONE_POSTPROCESS) != 0 ? from[3] : new EffectQueuePostprocess(id);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void compute(EffectQueue[] queues, long currTime, float halfW, float halfH, float halfZ )
-    {
-    ((EffectQueueMatrix     )queues[0]).compute(currTime);
-    ((EffectQueueVertex     )queues[1]).compute(currTime,halfW,halfH,halfZ);
-    ((EffectQueueFragment   )queues[2]).compute(currTime,halfW,halfH,halfZ);
-    ((EffectQueuePostprocess)queues[3]).compute(currTime);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void send(EffectQueue[] queues, DistortedOutputSurface surface, float inflate, float halfW, float halfH, float halfZ, int variant )
-    {
-    ((EffectQueueMatrix  )queues[0]).send(surface,halfW,halfH,halfZ, variant);
-    ((EffectQueueVertex  )queues[1]).send(inflate, variant);
-    ((EffectQueueFragment)queues[2]).send(variant);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static float[] getMVP(EffectQueue[] queues)
-    {
-    return ((EffectQueueMatrix)queues[0]).getMVP();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void getUniforms(int programH, int variant)
-    {
-    EffectQueueFragment.getUniforms(programH,variant);
-    EffectQueueVertex  .getUniforms(programH,variant);
-    EffectQueueMatrix  .getUniforms(programH,variant);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Every effect queue has an ID, which should be the same iff two queues hold the same effects.
-// (this is a speedup: then both queues can be applied once, which seriously speeds up stuff -
-// especially important in case of postprocessing)
-
-  private void regenerateID()
-    {
-    if( mNumEffects>0 )
-      {
-      ArrayList<Long> list = new ArrayList<>();
-      for (int i = 0; i < mNumEffects; i++) list.add(mEffects[i].getID());
-      Long id = mMapID.get(list);
-
-      if( id!=null )
-        {
-        mID = id;
-        }
-      else
-        {
-        mMapID.put(list,mNextID);
-        mID = mNextID++;
-        }
-      }
-    else
-      {
-      mID = 0;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  @SuppressWarnings("unused")
-  int getNumEffects()
-    {
-    return mNumEffects;  
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  long getID()
-    {
-    return mID;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static boolean setMax(int index, int m)
-    {
-    if( !Distorted.isInitialized() || m<=mMax[index] )
-      {
-      mMax[index] = m<0 ? 0:m;
-      return true;
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static int getMax(int index)
-    {
-    return mMax[index];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void registerForMessages(EffectListener el)
-    {
-    if( mListeners==null ) mListeners = new ArrayList<>();
-
-    if( !mListeners.contains(el) )
-      {
-      mListeners.add(el);
-      mNumListeners++;
-      }
-    }
- 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void deregisterForMessages(EffectListener el)
-    {
-    if( mListeners.remove(el) )
-      {
-      mNumListeners--;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void onDestroy()
-    {
-    mNextID = 1;
-    mMapID.clear();
-    EffectType.reset(mMax);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// this assumes 0<=effect<mNumEffects
-
-  protected void remove(int effect)
-    {
-    mNumEffects--;
-
-    long removedID = mEffects[effect].getID();
-
-    for(int j=effect; j<mNumEffects; j++ )
-      {
-      mEffects[j]         = mEffects[j+1];
-      mCurrentDuration[j] = mCurrentDuration[j+1];
-      mName[j]            = mName[j+1];
-      }
-
-    mEffects[mNumEffects] = null;
-
-    for(int i=0; i<mNumListeners; i++)
-      EffectMessageSender.newMessage( mListeners.get(i), EffectMessage.EFFECT_REMOVED, removedID, mDistortedEffectsID);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized int removeByName(EffectName name)
-    {
-    int ret = 0;
-
-    for(int i=0; i<mNumEffects; i++)
-      {
-      if( mEffects[i].getName() == name )
-        {
-        mJobs.add(new Job(DETACH,0,true,mEffects[i]));
-        ret++;
-        }
-      }
-
-    if( ret>0 )
-      {
-      DistortedMaster.newSlave(this);
-      mNumEffectsToBe-=ret;
-      }
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized int removeById(long id)
-    {
-    for(int i=0; i<mNumEffects; i++)
-      {
-      if( mEffects[i].getID() == id )
-        {
-        mJobs.add(new Job(DETACH,0,true,mEffects[i]));
-        DistortedMaster.newSlave(this);
-        mNumEffectsToBe--;
-        return 1;
-        }
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized int removeEffect(Effect effect)
-    {
-    for(int i=0; i<mNumEffects; i++)
-      {
-      if( mEffects[i]==effect )
-        {
-        mJobs.add(new Job(DETACH,0,true,mEffects[i]));
-        DistortedMaster.newSlave(this);
-        mNumEffectsToBe--;
-        return 1;
-        }
-      }
-   
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// we do want to notify Listeners if they called 'abortAll' themselves but don't want to notify
-// them if it is the library itself which is releasing resources.
-
-  synchronized int abortAll(boolean notify)
-    {
-    mJobs.add(new Job(DETALL,0,notify,null));
-    DistortedMaster.newSlave(this);
-    mNumEffectsToBe = 0;
-    return mNumEffects;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-  
-  boolean add(Effect effect)
-    {
-    if( mMax[mIndex]>mNumEffectsToBe || !mCreated )
-      {
-      mJobs.add(new Job(ATTACH,0,false,effect));
-      DistortedMaster.newSlave(this);
-      mNumEffectsToBe++;
-      return true;
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * This is not really part of the public API. Has to be public only because it is a part of the
- * DistortedSlave interface, which should really be a class that we extend here instead but
- * Java has no multiple inheritance.
- *
- * @y.exclude
- */
-  public void doWork()
-    {
-    boolean changed = false;
-    int num = mJobs.size();
-    Job job;
-
-    for(int i=0; i<num; i++)
-      {
-      job = mJobs.remove(0);
-
-      switch(job.type)
-        {
-        case CREATE: int max = mMax[mIndex];
-                     if( max>0 )
-                       {
-                       mUniforms        = new float[max*job.num];
-                       mCurrentDuration = new long[max];
-                       mEffects         = new Effect[max];
-                       mName            = new int[max];
-                       }
-                     mCreated = true;
-
-                     break;
-        case ATTACH: if( mMax[mIndex]>mNumEffects ) // it is possible that we have first
-                       {                            // added effects and then lowered mMax
-                       mCurrentDuration[mNumEffects] = 0;
-                       mEffects[mNumEffects] = job.effect;
-                       mName[mNumEffects] = job.effect.getName().ordinal();
-                       mNumEffects++;
-                       changed = true;
-                       }
-                     else
-                       {
-                       android.util.Log.e("queue", "failed to add effect "+job.effect.getName());
-                       }
-                     break;
-        case DETACH: for(int j=0; j<mNumEffects; j++)
-                       {
-                       if (mEffects[j] == job.effect)
-                         {
-                         remove(j);
-                         changed = true;
-                         break;
-                         }
-                       }
-                     break;
-        case DETALL: for(int j=0; j<mNumEffects; j++ )
-                       {
-                       changed = true;
-
-                       if( job.notify )
-                         {
-                         for(int k=0; k<mNumListeners; k++)
-                           EffectMessageSender.newMessage( mListeners.get(k), EffectMessage.EFFECT_REMOVED, mEffects[j].getID(), mDistortedEffectsID);
-                         }
-
-                       mEffects[j] = null;
-                       }
-
-                     mNumEffects= 0;
-                     break;
-        }
-      }
-
-    if( changed && mIndex==EffectType.POSTPROCESS.ordinal() ) regenerateID();
-    }
-  }
diff --git a/src/main/java/org/distorted/library/main/EffectQueueFragment.java b/src/main/java/org/distorted/library/main/EffectQueueFragment.java
deleted file mode 100644
index 47273c0..0000000
--- a/src/main/java/org/distorted/library/main/EffectQueueFragment.java
+++ /dev/null
@@ -1,94 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted is free software: you can redistribute it and/or modify                             //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted is distributed in the hope that it will be useful,                                  //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library.main;
-
-import android.opengl.GLES31;
-
-import org.distorted.library.effect.EffectType;
-import org.distorted.library.effect.FragmentEffect;
-import org.distorted.library.message.EffectMessage;
-import org.distorted.library.message.EffectMessageSender;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-class EffectQueueFragment extends EffectQueue
-  {
-  private static final int NUM_UNIFORMS = FragmentEffect.NUM_UNIFORMS;
-  private static final int INDEX = EffectType.FRAGMENT.ordinal();
-
-  private static int[] mNumEffectsH = new int[Distorted.MAIN_VARIANTS];
-  private static int[] mNameH       = new int[Distorted.MAIN_VARIANTS];
-  private static int[] mUniformsH   = new int[Distorted.MAIN_VARIANTS];
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-  EffectQueueFragment(long id)
-    { 
-    super(id,NUM_UNIFORMS,INDEX);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void getUniforms(int mProgramH, int variant)
-    {
-    mNumEffectsH[variant]= GLES31.glGetUniformLocation( mProgramH, "fNumEffects");
-    mNameH[variant]      = GLES31.glGetUniformLocation( mProgramH, "fName");
-    mUniformsH[variant]  = GLES31.glGetUniformLocation( mProgramH, "fUniforms");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-  
-  void compute(long currTime,float halfX, float halfY, float halfZ)
-    { 
-    if( currTime==mTime ) return;
-    if( mTime==0 ) mTime = currTime;
-    long step = (currTime-mTime);
-
-    for(int i=0; i<mNumEffects; i++)
-      {
-      mCurrentDuration[i] += step;
-
-      if( mEffects[i].compute(mUniforms, NUM_UNIFORMS*i, mCurrentDuration[i], step) )
-        {
-        for(int j=0; j<mNumListeners; j++)
-          EffectMessageSender.newMessage( mListeners.get(j), EffectMessage.EFFECT_FINISHED, mEffects[i].getID(), mDistortedEffectsID);
-        }
-
-      mUniforms[NUM_UNIFORMS*i+5] -= halfX;
-      mUniforms[NUM_UNIFORMS*i+6] -= halfY;
-      mUniforms[NUM_UNIFORMS*i+7] -= halfZ;
-      }
-
-    mTime = currTime;  
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-  
-  void send(int variant)
-    {
-    GLES31.glUniform1i( mNumEffectsH[variant], mNumEffects);
-
-    if( mNumEffects>0 )
-      {
-      GLES31.glUniform1iv( mNameH[variant]    ,                 mNumEffects, mName    ,0);
-      GLES31.glUniform4fv( mUniformsH[variant],(NUM_UNIFORMS/4)*mNumEffects, mUniforms,0);
-      }  
-    }
-  }
diff --git a/src/main/java/org/distorted/library/main/EffectQueueMatrix.java b/src/main/java/org/distorted/library/main/EffectQueueMatrix.java
deleted file mode 100644
index e85f67d..0000000
--- a/src/main/java/org/distorted/library/main/EffectQueueMatrix.java
+++ /dev/null
@@ -1,164 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted is free software: you can redistribute it and/or modify                             //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted is distributed in the hope that it will be useful,                                  //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library.main;
-
-import android.opengl.GLES31;
-import android.opengl.Matrix;
-
-import org.distorted.library.effect.EffectType;
-import org.distorted.library.effect.MatrixEffect;
-import org.distorted.library.message.EffectMessage;
-import org.distorted.library.message.EffectMessageSender;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-class EffectQueueMatrix extends EffectQueue
-  {   
-  private static final int NUM_UNIFORMS = MatrixEffect.NUM_UNIFORMS;
-  private static final int INDEX = EffectType.MATRIX.ordinal();
-
-  private static float[] mMVPMatrix = new float[16];
-  private static float[] mViewMatrix= new float[16];
-
-  private static int[] mObjDH      = new int[Distorted.MAIN_VARIANTS];
-  private static int[] mMVPMatrixH = new int[Distorted.MAIN_VARIANTS];
-  private static int[] mMVMatrixH  = new int[Distorted.MAIN_VARIANTS];
-
-  private static float[] mTmpMatrix = new float[16];
-  private static float[] mTmpResult = new float[4];
-  private static float[] mTmpPoint  = new float[4];
-  private static float mMinX,mMaxX,mMinY,mMaxY;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-  EffectQueueMatrix(long id)
-    { 
-    super(id,NUM_UNIFORMS,INDEX );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void getUniforms(int mProgramH, int variant)
-    {
-    mObjDH[variant]     = GLES31.glGetUniformLocation(mProgramH, "u_objD");
-    mMVPMatrixH[variant]= GLES31.glGetUniformLocation(mProgramH, "u_MVPMatrix");
-    mMVMatrixH[variant] = GLES31.glGetUniformLocation(mProgramH, "u_MVMatrix");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void magnifyDir()
-    {
-    Matrix.multiplyMV(mTmpResult,0,mTmpMatrix,0,mTmpPoint,0);
-    float nx = mTmpResult[0]/mTmpResult[3];
-    float ny = mTmpResult[1]/mTmpResult[3];
-
-    if( nx<mMinX ) mMinX = nx;
-    if( nx>mMaxX ) mMaxX = nx;
-    if( ny<mMinY ) mMinY = ny;
-    if( ny>mMaxY ) mMaxY = ny;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// return a float which describes how much larger an object must be so that it appears to be (about)
-// 'marginInPixels' pixels larger in each direction. Used in Postprocessing.
-
-  float magnify(DistortedOutputSurface projection, float halfX, float halfY, float halfZ, float marginInPixels)
-    {
-    mMinX = Integer.MAX_VALUE;
-    mMaxX = Integer.MIN_VALUE;
-    mMinY = Integer.MAX_VALUE;
-    mMaxY = Integer.MIN_VALUE;
-
-    mTmpPoint[3] = 1.0f;
-
-    Matrix.multiplyMM(mTmpMatrix, 0, projection.mProjectionMatrix, 0, mViewMatrix, 0);
-
-    mTmpPoint[0] = +halfX; mTmpPoint[1] = +halfY; mTmpPoint[2] = +halfZ; magnifyDir();
-    mTmpPoint[0] = +halfX; mTmpPoint[1] = +halfY; mTmpPoint[2] = -halfZ; magnifyDir();
-    mTmpPoint[0] = +halfX; mTmpPoint[1] = -halfY; mTmpPoint[2] = +halfZ; magnifyDir();
-    mTmpPoint[0] = +halfX; mTmpPoint[1] = -halfY; mTmpPoint[2] = -halfZ; magnifyDir();
-    mTmpPoint[0] = -halfX; mTmpPoint[1] = +halfY; mTmpPoint[2] = +halfZ; magnifyDir();
-    mTmpPoint[0] = -halfX; mTmpPoint[1] = +halfY; mTmpPoint[2] = -halfZ; magnifyDir();
-    mTmpPoint[0] = -halfX; mTmpPoint[1] = -halfY; mTmpPoint[2] = +halfZ; magnifyDir();
-    mTmpPoint[0] = -halfX; mTmpPoint[1] = -halfY; mTmpPoint[2] = -halfZ; magnifyDir();
-
-    float xLenInPixels = projection.mWidth *(mMaxX-mMinX)/2;
-    float yLenInPixels = projection.mHeight*(mMaxY-mMinY)/2;
-
-    // already margin / avg(xLen,yLen) is the size of the halo.
-    // Here we need a bit more because we are marking not only the halo, but a little bit around
-    // it as well so that the (blur for example) will be smooth on the edges. Thus the 2.0f.
-    // ( 4.0 because there is an extra 2.0 from the avg(xLen,yLen) )
-    //
-    // mMipmap ( 1.0, 0.5, 0.25, 0.125 ) - we need to make the size of the halo independent
-    // of postprocessing effect quality.
-
-    return projection.mMipmap*4.0f*marginInPixels/( xLenInPixels+yLenInPixels );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void compute(long currTime)
-    {
-    if( currTime==mTime ) return;
-    if( mTime==0 ) mTime = currTime;
-    long step = (currTime-mTime);
-
-    for(int i=0; i<mNumEffects; i++)
-      {
-      mCurrentDuration[i] += step;
-
-      if( mEffects[i].compute(mUniforms, NUM_UNIFORMS*i, mCurrentDuration[i], step) )
-        {
-        for(int j=0; j<mNumListeners; j++)
-          EffectMessageSender.newMessage( mListeners.get(j), EffectMessage.EFFECT_FINISHED, mEffects[i].getID(), mDistortedEffectsID);
-        }
-      }
-     
-    mTime = currTime;  
-    }  
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float[] getMVP()
-    {
-    return mMVPMatrix;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void send(DistortedOutputSurface projection, float halfX, float halfY, float halfZ, int variant)
-    {
-    Matrix.setIdentityM(mViewMatrix, 0);
-    Matrix.translateM(mViewMatrix, 0, -projection.mWidth/2, -projection.mHeight/2, -projection.mDistance);
-    float mipmap = projection.mMipmap;
-    if( mipmap!=1 ) Matrix.scaleM(mViewMatrix, 0, mipmap, mipmap, mipmap);
-
-    for(int i=0; i<mNumEffects; i++) ((MatrixEffect)mEffects[i]).apply(mViewMatrix,mUniforms,i);
-
-    Matrix.translateM(mViewMatrix, 0, halfX,halfY,halfZ);
-    Matrix.multiplyMM(mMVPMatrix, 0, projection.mProjectionMatrix, 0, mViewMatrix, 0);
-
-    GLES31.glUniform3f( mObjDH[variant] , halfX, halfY, halfZ);
-    GLES31.glUniformMatrix4fv(mMVMatrixH[variant] , 1, false, mViewMatrix, 0);
-    GLES31.glUniformMatrix4fv(mMVPMatrixH[variant], 1, false, mMVPMatrix , 0);
-    }
-  }
diff --git a/src/main/java/org/distorted/library/main/EffectQueuePostprocess.java b/src/main/java/org/distorted/library/main/EffectQueuePostprocess.java
deleted file mode 100644
index 9a9578a..0000000
--- a/src/main/java/org/distorted/library/main/EffectQueuePostprocess.java
+++ /dev/null
@@ -1,221 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted is free software: you can redistribute it and/or modify                             //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted is distributed in the hope that it will be useful,                                  //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library.main;
-
-import android.content.res.Resources;
-import android.opengl.GLES31;
-import android.util.Log;
-
-import org.distorted.library.R;
-import org.distorted.library.effect.EffectType;
-import org.distorted.library.effect.PostprocessEffect;
-import org.distorted.library.effect.VertexEffect;
-import org.distorted.library.mesh.MeshBase;
-import org.distorted.library.message.EffectMessage;
-import org.distorted.library.message.EffectMessageSender;
-import org.distorted.library.program.DistortedProgram;
-
-import java.io.InputStream;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-class EffectQueuePostprocess extends EffectQueue
-  {
-  private static final int NUM_UNIFORMS = PostprocessEffect.NUM_UNIFORMS;
-  private static final int INDEX = EffectType.POSTPROCESS.ordinal();
-
-  private int mHalo;
-  private float mR, mG, mB, mA;
-
-  private static DistortedProgram mPreProgram;
-  private static int mPreColorH;
-  private static int mPreTextureH;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  EffectQueuePostprocess(long id)
-    { 
-    super(id,NUM_UNIFORMS,INDEX );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void compute(long currTime)
-    {
-    if( currTime==mTime ) return;
-    if( mTime==0 ) mTime = currTime;
-    long step = (currTime-mTime);
-
-    mR = mG = mB = mA = 0.0f;
-    mHalo = 0;
-    int halo;
-
-    for(int i=0; i<mNumEffects; i++)
-      {
-      mCurrentDuration[i] += step;
-
-      // first zero out the 'alpha' because BLUR effect will not overwrite this (it is a 1D effect)
-      // and if previously there was a GLOW effect here then mA would be non-zero and we don't want
-      // that (see preprocess())
-      mUniforms[NUM_UNIFORMS*i+4]=0.0f;
-
-      if( mEffects[i].compute(mUniforms, NUM_UNIFORMS*i, mCurrentDuration[i], step) )
-        {
-        for(int j=0; j<mNumListeners; j++)
-          EffectMessageSender.newMessage( mListeners.get(j), EffectMessage.EFFECT_FINISHED, mEffects[i].getID(), mDistortedEffectsID);
-        }
-
-      halo = (int)mUniforms[NUM_UNIFORMS*i];
-      if( halo>mHalo ) mHalo = halo;
-      }
-
-    // TODO  (now only really works in case of 1 effect!)
-    if( mNumEffects>0 )
-      {
-      mR = mUniforms[1];
-      mG = mUniforms[2];
-      mB = mUniforms[3];
-      mA = mUniforms[4];
-      }
-
-    mTime = currTime;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void createPrograms(Resources resources)
-    {
-    final InputStream mainVertStream = resources.openRawResource(R.raw.main_vertex_shader);
-    final InputStream mainFragStream = resources.openRawResource(R.raw.preprocess_fragment_shader);
-
-    int numV = VertexEffect.getNumEnabled();
-
-    String mainVertHeader= Distorted.GLSL_VERSION + ("#define NUM_VERTEX "   + ( numV>0 ? Distorted.getMax(EffectType.VERTEX  ) : 0 ) + "\n");
-    String mainFragHeader= Distorted.GLSL_VERSION + "\n";
-
-    String enabledEffectV= VertexEffect.getGLSL();
-
-    try
-      {
-      mPreProgram = new DistortedProgram(mainVertStream, mainFragStream, mainVertHeader, mainFragHeader,
-                                         enabledEffectV, null, Distorted.GLSL, null);
-      }
-    catch(Exception e)
-      {
-      Log.e("POSTPROCESS", e.getClass().getSimpleName()+" trying to compile PRE program: "+e.getMessage());
-      throw new RuntimeException(e.getMessage());
-      }
-
-    int preProgramH = mPreProgram.getProgramHandle();
-    EffectQueueVertex.getUniforms( preProgramH,2 );
-    EffectQueueMatrix.getUniforms( preProgramH,2 );
-    mPreColorH  = GLES31.glGetUniformLocation( preProgramH, "u_Color"  );
-    mPreTextureH= GLES31.glGetUniformLocation( preProgramH, "u_Texture");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// TODO  (now only really works in case of 1 effect!)
-
-  int getQuality()
-    {
-    return mNumEffects>0 ? ((PostprocessEffect)mEffects[0]).getQuality() : 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// TODO  (now only really works in case of 1 effect!)
-
-  boolean getRender()
-    {
-    return mNumEffects>0 ? ((PostprocessEffect)mEffects[0]).getRender() : false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int preprocess(DistortedOutputSurface buffer, DistortedNode node)
-    {
-    buffer.setAsOutput();
-    DistortedSurface input = node.getInternalSurface();
-
-    if( input.setAsInput() )
-      {
-      MeshBase mesh = node.getMesh();
-
-      float halfW = input.getWidth() / 2.0f;
-      float halfH = input.getHeight()/ 2.0f;
-      float halfZ = halfW*mesh.getZFactor();
-
-      DistortedRenderState.setUpStencilMark(mA!=0.0f);
-      DistortedRenderState.disableBlending();
-
-      GLES31.glViewport(0, 0, buffer.mWidth, buffer.mHeight );
-
-      mPreProgram.useProgram();
-
-      mesh.bindVertexAttribs(mPreProgram);
-
-      EffectQueue[] queues = node.getEffects().getQueues();
-      EffectQueueMatrix matrix = (EffectQueueMatrix)queues[0];
-      EffectQueueVertex vertex = (EffectQueueVertex)queues[1];
-
-      float inflate=0.0f;
-
-      matrix.send(buffer,halfW,halfH,halfZ,2);
-
-      if( mHalo!=0.0f )
-        {
-        inflate = matrix.magnify(buffer,halfW,halfH,halfZ,mHalo);
-        }
-
-      vertex.send(inflate,2);
-
-      if( mA!=0.0f )
-        {
-        GLES31.glUniform4f(mPreColorH, mR, mG, mB, mA);
-        GLES31.glUniform1i(mPreTextureH, 0);
-        }
-
-      GLES31.glDrawArrays(GLES31.GL_TRIANGLE_STRIP, 0, mesh.getNumVertices() );
-
-      DistortedRenderState.restoreBlending();
-      DistortedRenderState.unsetUpStencilMark();
-
-      return 1;
-      }
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int postprocess(DistortedFramebuffer buffer)
-    {
-    int numRenders = 0;
-
-    GLES31.glDisable(GLES31.GL_BLEND);
-
-    for(int i=0; i<mNumEffects; i++)
-      {
-      numRenders += ((PostprocessEffect)mEffects[i]).apply(mUniforms,NUM_UNIFORMS*i, buffer);
-      }
-
-    GLES31.glEnable(GLES31.GL_BLEND);
-
-    return numRenders;
-    }
-  }
diff --git a/src/main/java/org/distorted/library/main/EffectQueueVertex.java b/src/main/java/org/distorted/library/main/EffectQueueVertex.java
deleted file mode 100644
index 61058ef..0000000
--- a/src/main/java/org/distorted/library/main/EffectQueueVertex.java
+++ /dev/null
@@ -1,97 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted is free software: you can redistribute it and/or modify                             //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted is distributed in the hope that it will be useful,                                  //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library.main;
-
-import android.opengl.GLES31;
-
-import org.distorted.library.effect.EffectType;
-import org.distorted.library.effect.VertexEffect;
-import org.distorted.library.message.EffectMessage;
-import org.distorted.library.message.EffectMessageSender;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-class EffectQueueVertex extends EffectQueue
-  { 
-  private static final int NUM_UNIFORMS = VertexEffect.NUM_UNIFORMS;
-  private static final int INDEX = EffectType.VERTEX.ordinal();
-
-  private static int[] mNumEffectsH = new int[Distorted.MAIN_VARIANTS];
-  private static int[] mNameH       = new int[Distorted.MAIN_VARIANTS];
-  private static int[] mUniformsH   = new int[Distorted.MAIN_VARIANTS];
-  private static int[] mInflateH    = new int[Distorted.MAIN_VARIANTS];
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-  EffectQueueVertex(long id)
-    { 
-    super(id,NUM_UNIFORMS,INDEX);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void getUniforms(int mProgramH, int variant)
-    {
-    mNumEffectsH[variant]= GLES31.glGetUniformLocation( mProgramH, "vNumEffects");
-    mNameH[variant]      = GLES31.glGetUniformLocation( mProgramH, "vName");
-    mUniformsH[variant]  = GLES31.glGetUniformLocation( mProgramH, "vUniforms");
-    mInflateH[variant]   = GLES31.glGetUniformLocation( mProgramH, "u_Inflate");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-  
-  void compute(long currTime,float halfX, float halfY, float halfZ)
-    {
-    if( currTime==mTime ) return;
-    if( mTime==0 ) mTime = currTime;
-    long step = (currTime-mTime);
-
-    for(int i=0; i<mNumEffects; i++)
-      {
-      mCurrentDuration[i] += step;
-
-      if( mEffects[i].compute(mUniforms, NUM_UNIFORMS*i, mCurrentDuration[i], step) )
-        {
-        for(int j=0; j<mNumListeners; j++)
-          EffectMessageSender.newMessage( mListeners.get(j), EffectMessage.EFFECT_FINISHED, mEffects[i].getID(), mDistortedEffectsID);
-        }
-
-      mUniforms[NUM_UNIFORMS*i+5] -= halfX;
-      mUniforms[NUM_UNIFORMS*i+6] -= halfY;
-      mUniforms[NUM_UNIFORMS*i+7] -= halfZ;
-      }
-
-    mTime = currTime;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void send(float inflate, int variant)
-    {
-    GLES31.glUniform1i( mNumEffectsH[variant], mNumEffects);
-    GLES31.glUniform1f( mInflateH[variant]   , inflate    );
-
-    if( mNumEffects>0 )
-      {
-      GLES31.glUniform1iv( mNameH[variant]    ,                 mNumEffects, mName    ,0);
-      GLES31.glUniform4fv( mUniformsH[variant],(NUM_UNIFORMS/4)*mNumEffects, mUniforms,0);
-      }
-    }
-  }
