commit 862fcd795e17079d63b260d53ed5aa8609d0a0c5
Author: Leszek Koltunski <leszek@distorted.org>
Date:   Wed Jun 8 00:03:48 2016 +0100

    New app: PlainMonaLisa (Mona Lisa rendered on plain SurfaceView)

diff --git a/build.gradle b/build.gradle
index 9eaca95..1d681fc 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ android {
 
     defaultConfig {
         applicationId "org.distorted.examples"
-        minSdkVersion 15
+        minSdkVersion 18
         targetSdkVersion 23
     }
 
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index c6252b6..a2113dd 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -34,5 +34,6 @@
         <activity android:name=".cubes.CubesActivity" />       
         <activity android:name=".quaternion.QuaternionActivity" />          
         <activity android:name=".effects3d.Effects3DActivity" />  
+        <activity android:name=".plainmonalisa.PlainMonaLisaActivity" />  
     </application>
 </manifest>
diff --git a/src/main/java/org/distorted/examples/TableOfContents.java b/src/main/java/org/distorted/examples/TableOfContents.java
index 320527e..69378cc 100644
--- a/src/main/java/org/distorted/examples/TableOfContents.java
+++ b/src/main/java/org/distorted/examples/TableOfContents.java
@@ -36,6 +36,7 @@ import org.distorted.examples.starwars.StarWarsActivity;
 import org.distorted.examples.cubes.CubesActivity;
 import org.distorted.examples.quaternion.QuaternionActivity;
 import org.distorted.examples.effects3d.Effects3DActivity;
+import org.distorted.examples.plainmonalisa.PlainMonaLisaActivity;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -238,6 +239,15 @@ public class TableOfContents extends ListActivity
       data.add(item);
       activityMapping.put(i++, Effects3DActivity.class);
    }
+
+   {
+      final Map<String, Object> item = new HashMap<String, Object>();
+      item.put(ITEM_IMAGE, R.drawable.icon_example_monalisa);
+      item.put(ITEM_TITLE, (i+1)+". "+getText(R.string.example_plainmonalisa));
+      item.put(ITEM_SUBTITLE, getText(R.string.example_plainmonalisa_subtitle));
+      data.add(item);
+      activityMapping.put(i++, PlainMonaLisaActivity.class);
+   }
      
    final SimpleAdapter dataAdapter = new SimpleAdapter(this, data, R.layout.toc_item, new String[] {ITEM_IMAGE, ITEM_TITLE, ITEM_SUBTITLE}, new int[] {R.id.Image, R.id.Title, R.id.SubTitle});
    setListAdapter(dataAdapter);  
diff --git a/src/main/java/org/distorted/examples/plainmonalisa/EglCore.java b/src/main/java/org/distorted/examples/plainmonalisa/EglCore.java
new file mode 100644
index 0000000..215aa11
--- /dev/null
+++ b/src/main/java/org/distorted/examples/plainmonalisa/EglCore.java
@@ -0,0 +1,327 @@
+package org.distorted.examples.plainmonalisa;
+
+import android.graphics.SurfaceTexture;
+import android.opengl.EGL14;
+import android.opengl.EGLConfig;
+import android.opengl.EGLContext;
+import android.opengl.EGLDisplay;
+import android.opengl.EGLExt;
+import android.opengl.EGLSurface;
+import android.util.Log;
+import android.view.Surface;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Core EGL state (display, context, config).
+ * <p>
+ * The EGLContext must only be attached to one thread at a time.  This class is not thread-safe.
+ */
+public final class EglCore
+  {
+  private static final String TAG = "EglCore";
+
+  /**
+   * Constructor flag: surface must be recordable.  This discourages EGL from using a
+   * pixel format that cannot be converted efficiently to something usable by the video
+   * encoder.
+   */
+  public static final int FLAG_RECORDABLE = 0x01;
+
+  /**
+   * Constructor flag: ask for GLES3, fall back to GLES2 if not available.  Without this
+   * flag, GLES2 is used.
+   */
+  public static final int FLAG_TRY_GLES3 = 0x02;
+
+  // Android-specific extension.
+  private static final int EGL_RECORDABLE_ANDROID = 0x3142;
+
+  private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;
+  private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;
+  private EGLConfig mEGLConfig = null;
+  private int mGlVersion = -1;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Prepares EGL display and context.
+   * <p>
+   * Equivalent to EglCore(null, 0).
+   */
+  public EglCore() {
+        this(null, 0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Prepares EGL display and context.
+   * <p>
+   * @param sharedContext The context to share, or null if sharing is not desired.
+   * @param flags Configuration bit flags, e.g. FLAG_RECORDABLE.
+   */
+  public EglCore(EGLContext sharedContext, int flags)
+    {
+    if (mEGLDisplay != EGL14.EGL_NO_DISPLAY)
+      {
+      throw new RuntimeException("EGL already set up");
+      }
+
+    if (sharedContext == null)
+      {
+      sharedContext = EGL14.EGL_NO_CONTEXT;
+      }
+
+    mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+
+    if (mEGLDisplay == EGL14.EGL_NO_DISPLAY)
+      {
+      throw new RuntimeException("unable to get EGL14 display");
+      }
+
+    int[] version = new int[2];
+
+    if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1))
+      {
+      mEGLDisplay = null;
+      throw new RuntimeException("unable to initialize EGL14");
+      }
+
+    // Try to get a GLES3 context, if requested.
+    if ((flags & FLAG_TRY_GLES3) != 0)
+      {
+      //Log.d(TAG, "Trying GLES 3");
+      EGLConfig config = getConfig(flags, 3);
+
+      if (config != null)
+        {
+        int[] attrib3_list =  { EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, EGL14.EGL_NONE };
+        EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, attrib3_list, 0);
+
+        if (EGL14.eglGetError() == EGL14.EGL_SUCCESS)
+          {
+          //Log.d(TAG, "Got GLES 3 config");
+          mEGLConfig = config;
+          mEGLContext = context;
+          mGlVersion = 3;
+          }
+        }
+      }
+
+    if (mEGLContext == EGL14.EGL_NO_CONTEXT)
+      {  // GLES 2 only, or GLES 3 attempt failed
+         //Log.d(TAG, "Trying GLES 2");
+      EGLConfig config = getConfig(flags, 2);
+
+      if (config == null)
+        {
+        throw new RuntimeException("Unable to find a suitable EGLConfig");
+        }
+      int[] attrib2_list = { EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE };
+      EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, attrib2_list, 0);
+      checkEglError("eglCreateContext");
+      mEGLConfig = config;
+      mEGLContext = context;
+      mGlVersion = 2;
+      }
+
+    // Confirm with query.
+    int[] values = new int[1];
+    EGL14.eglQueryContext(mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION, values, 0);
+
+    Log.d(TAG, "EGLContext created, client version " + values[0]);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Finds a suitable EGLConfig.
+   *
+   * @param flags Bit flags from constructor.
+   * @param version Must be 2 or 3.
+   */
+  private EGLConfig getConfig(int flags, int version)
+    {
+    int renderableType = EGL14.EGL_OPENGL_ES2_BIT;
+
+    if (version >= 3)
+      {
+      renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR;
+      }
+
+    // The actual surface is generally RGBA or RGBX, so situationally omitting alpha
+    // doesn't really help.  It can also lead to a huge performance hit on glReadPixels()
+    // when reading into a GL_RGBA buffer.
+    int[] attribList =
+                {
+                EGL14.EGL_RED_SIZE, 8,
+                EGL14.EGL_GREEN_SIZE, 8,
+                EGL14.EGL_BLUE_SIZE, 8,
+                EGL14.EGL_ALPHA_SIZE, 8,
+                //EGL14.EGL_DEPTH_SIZE, 16,
+                //EGL14.EGL_STENCIL_SIZE, 8,
+                EGL14.EGL_RENDERABLE_TYPE, renderableType,
+                EGL14.EGL_NONE, 0,      // placeholder for recordable [@-3]
+                EGL14.EGL_NONE
+                };
+
+    if ((flags & FLAG_RECORDABLE) != 0)
+      {
+      attribList[attribList.length - 3] = EGL_RECORDABLE_ANDROID;
+      attribList[attribList.length - 2] = 1;
+      }
+
+    EGLConfig[] configs = new EGLConfig[1];
+    int[] numConfigs = new int[1];
+
+    if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0))
+      {
+      Log.w(TAG, "unable to find RGB8888 / " + version + " EGLConfig");
+      return null;
+      }
+
+    return configs[0];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Discards all resources held by this class, notably the EGL context.  This must be
+   * called from the thread where the context was created.
+   * <p>
+   * On completion, no context will be current.
+   */
+  public void release()
+    {
+    if (mEGLDisplay != EGL14.EGL_NO_DISPLAY)
+      {
+      // Android is unusual in that it uses a reference-counted EGLDisplay.  So for
+      // every eglInitialize() we need an eglTerminate().
+      EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);
+      EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
+      EGL14.eglReleaseThread();
+      EGL14.eglTerminate(mEGLDisplay);
+      }
+
+    mEGLDisplay = EGL14.EGL_NO_DISPLAY;
+    mEGLContext = EGL14.EGL_NO_CONTEXT;
+    mEGLConfig = null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  @Override
+  protected void finalize() throws Throwable
+    {
+    try
+      {
+      if (mEGLDisplay != EGL14.EGL_NO_DISPLAY)
+        {
+        // We're limited here -- finalizers don't run on the thread that holds
+        // the EGL state, so if a surface or context is still current on another
+        // thread we can't fully release it here.  Exceptions thrown from here
+        // are quietly discarded.  Complain in the log file.
+        Log.w(TAG, "WARNING: EglCore was not explicitly released -- state may be leaked");
+        release();
+        }
+      }
+    finally
+      {
+      super.finalize();
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Destroys the specified surface.  Note the EGLSurface won't actually be destroyed if it's
+   * still current in a context.
+   */
+  public void releaseSurface(EGLSurface eglSurface)
+    {
+    EGL14.eglDestroySurface(mEGLDisplay, eglSurface);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Creates an EGL surface associated with a Surface.
+   * <p>
+   * If this is destined for MediaCodec, the EGLConfig should have the "recordable" attribute.
+   */
+  public EGLSurface createWindowSurface(Object surface)
+    {
+    if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture))
+      {
+      throw new RuntimeException("invalid surface: " + surface);
+      }
+
+    // Create a window surface, and attach it to the Surface we received.
+    int[] surfaceAttribs = { EGL14.EGL_NONE };
+    EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface, surfaceAttribs, 0);
+    checkEglError("eglCreateWindowSurface");
+
+    if (eglSurface == null)
+      {
+      throw new RuntimeException("surface was null");
+      }
+    return eglSurface;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Makes our EGL context current, using the supplied surface for both "draw" and "read".
+   */
+  public void makeCurrent(EGLSurface eglSurface)
+    {
+    if (mEGLDisplay == EGL14.EGL_NO_DISPLAY)
+      {
+      // called makeCurrent() before create?
+      Log.d(TAG, "NOTE: makeCurrent w/o display");
+      }
+    if (!EGL14.eglMakeCurrent(mEGLDisplay, eglSurface, eglSurface, mEGLContext))
+      {
+      throw new RuntimeException("eglMakeCurrent failed");
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Makes no context current.
+   */
+  public void makeNothingCurrent()
+    {
+    if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT))
+      {
+      throw new RuntimeException("eglMakeCurrent failed");
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Returns the GLES version this context is configured for (currently 2 or 3).
+   */
+  public int getGlVersion()
+    {
+    return mGlVersion;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Calls eglSwapBuffers.  Use this to "publish" the current frame.
+   *
+   * @return false on failure
+   */
+  public boolean swapBuffers(EGLSurface eglSurface)
+    {
+    return EGL14.eglSwapBuffers(mEGLDisplay, eglSurface);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Checks for EGL errors.  Throws an exception if an error has been raised.
+   */
+  private void checkEglError(String msg)
+    {
+    int error;
+
+    if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS)
+      {
+      throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error));
+      }
+    }
+  }
+///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/examples/plainmonalisa/PlainMonaLisaActivity.java b/src/main/java/org/distorted/examples/plainmonalisa/PlainMonaLisaActivity.java
new file mode 100644
index 0000000..c3b4c60
--- /dev/null
+++ b/src/main/java/org/distorted/examples/plainmonalisa/PlainMonaLisaActivity.java
@@ -0,0 +1,51 @@
+package org.distorted.examples.plainmonalisa;
+
+import org.distorted.library.Distorted;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PlainMonaLisaActivity extends Activity
+{
+    private PlainMonaLisaSurfaceView mView;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onCreate(Bundle icicle) 
+      {
+      super.onCreate(icicle);
+      mView = new PlainMonaLisaSurfaceView(this);
+      setContentView(mView);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onPause() 
+      {
+      mView.onPause();
+      super.onPause();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onResume() 
+      {
+      super.onResume();
+      mView.onResume();
+      }
+    
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onDestroy() 
+      {
+      Distorted.onDestroy();  
+      super.onDestroy();
+      }
+    
+}
diff --git a/src/main/java/org/distorted/examples/plainmonalisa/PlainMonaLisaSurfaceView.java b/src/main/java/org/distorted/examples/plainmonalisa/PlainMonaLisaSurfaceView.java
new file mode 100644
index 0000000..b0df0e7
--- /dev/null
+++ b/src/main/java/org/distorted/examples/plainmonalisa/PlainMonaLisaSurfaceView.java
@@ -0,0 +1,124 @@
+package org.distorted.examples.plainmonalisa;
+
+import android.content.Context;
+import android.view.Choreographer;
+import android.view.SurfaceView;
+import android.view.SurfaceHolder;
+import android.util.Log;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class PlainMonaLisaSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Choreographer.FrameCallback
+  {
+  private static final String TAG = "MonaLisaSurface";
+  private RenderThread mRenderThread;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public PlainMonaLisaSurfaceView(Context context)
+    {
+    super(context);
+    getHolder().addCallback(this);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void surfaceCreated(SurfaceHolder holder)
+    {
+    Log.d(TAG, "surfaceCreated holder=" + holder);
+
+    mRenderThread = new RenderThread(holder, this);
+    mRenderThread.setName("GL render");
+    mRenderThread.start();
+    mRenderThread.waitUntilReady();
+
+    RenderHandler rh = mRenderThread.getHandler();
+
+    if (rh != null)
+      {
+      rh.sendSurfaceCreated();
+      }
+
+    Choreographer.getInstance().postFrameCallback(this);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
+    {
+    Log.d(TAG, "surfaceChanged fmt=" + format + " size=" + width + "x" + height +" holder=" + holder);
+
+    RenderHandler rh = mRenderThread.getHandler();
+
+    if (rh != null)
+      {
+      rh.sendSurfaceChanged(format, width, height);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void surfaceDestroyed(SurfaceHolder holder)
+    {
+    Log.d(TAG, "surfaceDestroyed holder=" + holder);
+
+    // We need to wait for the render thread to shut down before continuing because we
+    // don't want the Surface to disappear out from under it mid-render.  The frame
+    // notifications will have been stopped back in onPause(), but there might have
+    // been one in progress.
+
+    RenderHandler rh = mRenderThread.getHandler();
+
+    if (rh != null)
+      {
+      rh.sendShutdown();
+
+      try
+        {
+        mRenderThread.join();
+        }
+      catch (InterruptedException ie)
+        {
+        throw new RuntimeException("join was interrupted", ie);
+        }
+      }
+    mRenderThread = null;
+
+    Log.d(TAG, "surfaceDestroyed complete");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onPause()
+    {
+    Log.d(TAG, "onPause unhooking choreographer");
+    Choreographer.getInstance().removeFrameCallback(this);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onResume()
+    {
+    if (mRenderThread != null)
+      {
+      Log.d(TAG, "onResume re-hooking choreographer");
+      Choreographer.getInstance().postFrameCallback(this);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Override
+  public void doFrame(long frameTimeNanos)
+    {
+    RenderHandler rh = mRenderThread.getHandler();
+
+    if (rh != null)
+      {
+      Choreographer.getInstance().postFrameCallback(this);
+      rh.sendDoFrame(frameTimeNanos);
+      }
+    }
+  }
+
+
diff --git a/src/main/java/org/distorted/examples/plainmonalisa/RenderHandler.java b/src/main/java/org/distorted/examples/plainmonalisa/RenderHandler.java
new file mode 100644
index 0000000..9de4cae
--- /dev/null
+++ b/src/main/java/org/distorted/examples/plainmonalisa/RenderHandler.java
@@ -0,0 +1,113 @@
+package org.distorted.examples.plainmonalisa;
+
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Handler for RenderThread.  Used for messages sent from the UI thread to the render thread.
+ * <p>
+ * The object is created on the render thread, and the various "send" methods are called
+ * from the UI thread.
+ */
+public class RenderHandler extends Handler
+  {
+  private static final String TAG = "RenderHandler";
+
+  private static final int MSG_SURFACE_CREATED = 0;
+  private static final int MSG_SURFACE_CHANGED = 1;
+  private static final int MSG_DO_FRAME = 2;
+  private static final int MSG_SHUTDOWN = 4;
+
+  // This shouldn't need to be a weak ref, since we'll go away when the Looper quits,
+  // but no real harm in it.
+  private WeakReference<RenderThread> mWeakRenderThread;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Call from render thread.
+   */
+  public RenderHandler(RenderThread rt)
+      {
+      mWeakRenderThread = new WeakReference<>(rt);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Sends the "surface created" message.
+   * <p>
+   * Call from UI thread.
+   */
+  public void sendSurfaceCreated()
+    {
+    sendMessage(obtainMessage(RenderHandler.MSG_SURFACE_CREATED));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Sends the "surface changed" message, forwarding what we got from the SurfaceHolder.
+   * <p>
+   * Call from UI thread.
+   */
+  public void sendSurfaceChanged(@SuppressWarnings("unused") int format, int width, int height)
+    {
+    sendMessage(obtainMessage(RenderHandler.MSG_SURFACE_CHANGED, width, height));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Sends the "do frame" message, forwarding the Choreographer event.
+   * <p>
+   * Call from UI thread.
+   */
+  public void sendDoFrame(long frameTimeNanos)
+    {
+    sendMessage(obtainMessage(RenderHandler.MSG_DO_FRAME, (int) (frameTimeNanos >> 32), (int) frameTimeNanos));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Sends the "shutdown" message, which tells the render thread to halt.
+   * <p>
+   * Call from UI thread.
+   */
+  public void sendShutdown()
+      {
+      sendMessage(obtainMessage(RenderHandler.MSG_SHUTDOWN));
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  @Override  // runs on RenderThread
+  public void handleMessage(Message msg)
+    {
+    int what = msg.what;
+    //Log.d(TAG, "RenderHandler [" + this + "]: what=" + what);
+    RenderThread renderThread = mWeakRenderThread.get();
+
+    if (renderThread == null)
+      {
+      Log.w(TAG, "RenderHandler.handleMessage: weak ref is null");
+      return;
+      }
+
+    switch (what)
+      {
+      case MSG_SURFACE_CREATED: renderThread.surfaceCreated();
+                                break;
+      case MSG_SURFACE_CHANGED: renderThread.surfaceChanged(msg.arg1, msg.arg2);
+                                break;
+      case MSG_DO_FRAME:        long timestamp = (((long) msg.arg1) << 32) | (((long) msg.arg2) & 0xffffffffL);
+                                renderThread.doFrame(timestamp);
+                                break;
+      case MSG_SHUTDOWN:        renderThread.shutdown();
+                                break;
+      default:                  throw new RuntimeException("unknown message " + what);
+      }
+    }
+  }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/examples/plainmonalisa/RenderThread.java b/src/main/java/org/distorted/examples/plainmonalisa/RenderThread.java
new file mode 100644
index 0000000..77f020b
--- /dev/null
+++ b/src/main/java/org/distorted/examples/plainmonalisa/RenderThread.java
@@ -0,0 +1,255 @@
+package org.distorted.examples.plainmonalisa;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.opengl.EGL14;
+import android.opengl.EGLSurface;
+import android.opengl.GLES20;
+import android.os.Looper;
+import android.os.Trace;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import org.distorted.library.Distorted;
+import org.distorted.library.DistortedBitmap;
+import org.distorted.library.Float2D;
+import org.distorted.library.Float3D;
+import org.distorted.library.Float4D;
+import org.distorted.examples.R;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * This class handles all OpenGL rendering.
+ * <p>
+ * Start the render thread after the Surface has been created.
+ */
+public class RenderThread extends Thread
+  {
+  private static final String TAG = "RenderThread";
+
+  // Object must be created on render thread to get correct Looper, but is used from
+  // UI thread, so we need to declare it volatile to ensure the UI thread sees a fully
+  // constructed object.
+  private volatile RenderHandler mHandler;
+
+  // Used to wait for the thread to start.
+  private Object mStartLock = new Object();
+  private boolean mReady = false;
+  private volatile SurfaceHolder mSurfaceHolder;  // may be updated by UI thread
+  private EglCore eglCore;
+  private EGLSurface eglSurface;
+
+  private DistortedBitmap monaLisa;
+  private int bmpHeight, bmpWidth;
+
+  private Float2D pLeft, pRight;
+  private Float4D rLeft, rRight;
+  private Float3D vLeft, vRight;
+
+  SurfaceView mView;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Pass in the SurfaceView's SurfaceHolder.  Note the Surface may not yet exist.
+   */
+  public RenderThread(SurfaceHolder holder, SurfaceView view)
+    {
+    mSurfaceHolder = holder;
+    mView = view;
+
+    pLeft = new Float2D( 90, 258);
+    pRight= new Float2D(176, 255);
+
+    rLeft = new Float4D(-10,-10,25,25);
+    rRight= new Float4D( 10, -5,25,25);
+
+    vLeft = new Float3D(-20,-20,0);
+    vRight= new Float3D( 20,-10,0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Thread entry point.
+   * <p>
+   * The thread should not be started until the Surface associated with the SurfaceHolder
+   * has been created.  That way we don't have to wait for a separate "surface created"
+   * message to arrive.
+   */
+  @Override
+  public void run()
+    {
+    Looper.prepare();
+    mHandler = new RenderHandler(this);
+    eglCore = new EglCore(null, EglCore.FLAG_RECORDABLE | EglCore.FLAG_TRY_GLES3);
+
+    synchronized (mStartLock)
+      {
+      mReady = true;
+      mStartLock.notify();    // signal waitUntilReady()
+      }
+
+    Looper.loop();
+    Log.d(TAG, "looper quit");
+
+    checkGlError("releaseGl start");
+
+    if (eglSurface != null)
+      {
+      eglCore.releaseSurface(eglSurface);
+      eglSurface = EGL14.EGL_NO_SURFACE;
+      }
+
+    checkGlError("releaseGl done");
+
+    eglCore.makeNothingCurrent();
+    eglCore.release();
+
+    synchronized (mStartLock)
+      {
+      mReady = false;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Waits until the render thread is ready to receive messages.
+   * <p>
+   * Call from the UI thread.
+   */
+  public void waitUntilReady()
+    {
+    synchronized (mStartLock)
+      {
+      while (!mReady)
+        {
+        try
+          {
+          mStartLock.wait();
+          }
+        catch (InterruptedException ie) { /* not expected */ }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Shuts everything down.
+   */
+  void shutdown()
+    {
+    Log.d(TAG, "shutdown");
+    Looper.myLooper().quit();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Returns the render thread's Handler.  This may be called from any thread.
+   */
+  public RenderHandler getHandler()
+      {
+      return mHandler;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  /**
+   * Prepares the surface.
+   */
+  void surfaceCreated()
+    {
+    Surface surface = mSurfaceHolder.getSurface();
+
+    eglSurface = eglCore.createWindowSurface(surface);
+    eglCore.makeCurrent(eglSurface);
+
+    InputStream is = mView.getContext().getResources().openRawResource(R.raw.monalisa);
+    Bitmap bmp;
+
+    try
+      {
+      bmp = BitmapFactory.decodeStream(is);
+      }
+    finally
+      {
+      try
+        {
+        is.close();
+        }
+      catch(IOException io) {}
+      }
+
+    monaLisa = new DistortedBitmap(bmp, 10);
+    monaLisa.distort( vLeft, rLeft , pLeft, 1000, 0);
+    monaLisa.distort(vRight, rRight, pRight,1000, 0);
+
+    bmpHeight = bmp.getHeight();
+    bmpWidth  = bmp.getWidth();
+
+    try
+      {
+      Distorted.onSurfaceCreated(mView.getContext());
+      }
+    catch(Exception ex)
+      {
+      Log.e("MonaLisa", ex.getMessage() );
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void surfaceChanged(int width, int height)
+    {
+    Log.d(TAG, "surfaceChanged " + width + "x" + height);
+
+    monaLisa.abortAllEffects(Distorted.TYPE_MATR);
+
+    if( bmpHeight/bmpWidth > height/width )
+      {
+      int w = (height*bmpWidth)/bmpHeight;
+      monaLisa.move((width-w)/2 ,0, 0);
+      monaLisa.scale((float)height/bmpHeight);
+      }
+    else
+      {
+      int h = (width*bmpHeight)/bmpWidth;
+      monaLisa.move(0 ,(height-h)/2, 0);
+      monaLisa.scale((float)width/bmpWidth);
+      }
+
+    Distorted.onSurfaceChanged(width, height);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void doFrame(long frameTimeNs)
+    {
+    Trace.beginSection("doFrame draw");
+    eglCore.makeCurrent(eglSurface);
+
+    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+    GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
+    monaLisa.draw(System.currentTimeMillis());
+
+    eglCore.swapBuffers(eglSurface);
+    Trace.endSection();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void checkGlError(String op)
+    {
+    int error = GLES20.glGetError();
+
+    if (error != GLES20.GL_NO_ERROR)
+      {
+      String msg = op + ": glError 0x" + Integer.toHexString(error);
+      Log.e(TAG, msg);
+      throw new RuntimeException(msg);
+      }
+    }
+  }
+///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 96e4971..63e394e 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -80,7 +80,9 @@
     <string name="example_quaternion_subtitle">Random rotations using quaternions.</string>
     <string name="example_effects3d">Effects3D</string>  
     <string name="example_effects3d_subtitle">Test results of all effects on a 3D object.</string>
-    
+    <string name="example_plainmonalisa">PlainMonaLisa</string>  
+    <string name="example_plainmonalisa_subtitle">MonaLisa rendered on a plain SurfaceView</string>
+
     <string name="example_movingeffects_toast">Click on \'RESET\' and define your path by touching the screen. Then click on one of the effects and see it move along your path.</string>
     <string name="example_cubes_toast">Rotate the cubes by swiping the screen</string>
 </resources>
