Move bitmap drawing to separate thread.\n\nBitmap drawing is now triggered by a Handler on the native thread. The bitmap thread runs a Looper that receives an empty message.\n\nAlso, screen size and scaling logic is moving into BitmapSize class.

This commit is contained in:
James Sanford 2012-09-30 21:17:32 -07:00
parent 2c402d03ff
commit 63b7bf1c6d
5 changed files with 297 additions and 120 deletions

View File

@ -0,0 +1,116 @@
package com.froop.app.kegs;
import android.graphics.Rect;
import android.util.Log;
class BitmapSize {
static class Const {
public static final int A2Width = 640 + 32 + 32; // kegs defcomm.h
public static final int A2Height = 400 + 32 + 30; // kegs defcomm.h
}
private int mWidth = 0;
private int mHeight = 0;
private boolean mScaled = false;
private boolean mCropped = false;
private float mScaleFactorX = 1.0f;
private float mScaleFactorY = 1.0f;
public BitmapSize(int width, int height) {
mWidth = width;
mHeight = height;
calculateScale(width, height);
}
public boolean showActionBar() {
if (mHeight < ((400 + 64) * mScaleFactorY)) {
return false;
} else {
return true;
}
}
public int getViewWidth() {
return (int)(Const.A2Width * mScaleFactorX);
}
public int getViewHeight() {
if (!doCropBorder()) {
return (int)(Const.A2Height * mScaleFactorY);
} else {
return (int)((Const.A2Height - 32) * mScaleFactorY);
}
}
private boolean doCropBorder() {
return mCropped;
}
public boolean isScaled() {
return (mScaleFactorX != 1.0f || mScaleFactorY != 1.0f);
}
public float getScaleX() {
return mScaleFactorX;
}
public float getScaleY() {
return mScaleFactorY;
}
public Rect getRectSrc() {
if (doCropBorder()) {
return new Rect(0, 32, Const.A2Width, Const.A2Height);
} else {
return new Rect(0, 0, Const.A2Width, Const.A2Height);
}
}
public Rect getRectDst() {
if (doCropBorder()) {
return new Rect(0, 0, Const.A2Width, Const.A2Height - 32);
} else {
return new Rect(0, 0, Const.A2Width, Const.A2Height);
}
}
// If we can fit at least 90% of a scaled screen into the display area, do it.
// If we hit less than 100% height, turn off system action bar and title.
// If ((400 + 32) * scale) > height, then crop border.
private void calculateScale(int width, int height) {
float scaleX = 1.0f;
float scaleY = 1.0f;
boolean crop = false;
// Force integer scaling on X axis.
scaleX = (float)Math.round((width * 0.9) / 640);
// TODO: Fix '48' hack being used for system buttons or soft buttons.
scaleY = Math.min(scaleX, (height - 48) / 400.0f);
// If Y would be compressed in a weird way, reduce the scale and use 1:1.
if ((scaleX - scaleY) > 0.5) {
scaleX = Math.max(1, scaleX - 1);
scaleY = scaleX;
}
// TODO: Fix '32' and '64' for software buttons and window decorations.
if (height < ((400 + 32 + 64) * scaleY)) {
crop = true;
}
mCropped = crop;
mScaleFactorX = scaleX;
mScaleFactorY = scaleY;
Log.w("kegs", "using scale " + scaleX + ":" + scaleY + " " + crop + from screen " + width + "x" + height);
}
// call us when you update your screen size/configuration
// helper to calculate view area for KegsView:onMeasure
// helper to create size struct
// helper struct for scale factors (&isScaled), crop info, source/dest rects
// KegsView can get this and pass it into the thread.
}

View File

@ -0,0 +1,96 @@
package com.froop.app.kegs;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Message;
import android.os.Looper;
import android.util.Log;
import android.view.SurfaceHolder;
import java.util.concurrent.locks.ReentrantLock;
class BitmapThread extends Thread {
public Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
updateScreen();
}
};
private SurfaceHolder mSurfaceHolder;
private Bitmap mBitmap;
private ReentrantLock mSurfaceLock;
private Canvas mCanvas;
private boolean mHaveSurface = false;
private boolean mScaled = false;
private float mScaleFactorX = 1.0f;
private float mScaleFactorY = 1.0f;
private Rect mRectSrc = new Rect(0, 0, 0, 0);
private Rect mRectDst = new Rect(0, 0, 0, 0);
private FpsCounter fpsCount = new FpsCounter("kegs", "thread");
public void setBitmap(SurfaceHolder surfaceHolder, Bitmap bitmap, ReentrantLock surfaceLock) {
mSurfaceHolder = surfaceHolder;
mBitmap = bitmap;
mSurfaceLock = surfaceLock;
}
public void run() {
Looper.prepare();
Looper.loop();
}
public Handler getHandler() {
return mHandler;
}
public void updateScreen() {
mSurfaceLock.lock();
try {
if (!mHaveSurface) {
return; // unlock with 'finally' clause
}
mCanvas = mSurfaceHolder.lockCanvas(); // Use Rect ?
if(mCanvas != null) {
if (!mScaled) {
mCanvas.drawBitmap(mBitmap, mRectSrc, mRectDst, null);
} else {
mCanvas.save();
mCanvas.scale(mScaleFactorX, mScaleFactorY);
mCanvas.drawBitmap(mBitmap, mRectSrc, mRectDst, null);
mCanvas.restore();
}
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
mCanvas = null;
}
} finally {
mSurfaceLock.unlock();
fpsCount.fps();
}
}
public void updateScreenSize(BitmapSize bitmapSize) {
// Keep our own copy of the size data, to give atomicity and
// possibly help performance with fewer indirections.
mSurfaceLock.lock();
mScaled = bitmapSize.isScaled();
mScaleFactorX = bitmapSize.getScaleX();
mScaleFactorY = bitmapSize.getScaleY();
mRectSrc = new Rect(bitmapSize.getRectSrc());
mRectDst = new Rect(bitmapSize.getRectDst());
mSurfaceLock.unlock();
updateScreen(); // Note: UI thread.
}
public void setHaveSurface(boolean haveSurface) {
mSurfaceLock.lock();
mHaveSurface = haveSurface;
mSurfaceLock.unlock();
if (haveSurface) {
// Refresh the canvas when we obtain a surface.
updateScreen(); // Note: UI thread.
}
}
}

View File

@ -0,0 +1,25 @@
package com.froop.app.kegs;
import android.util.Log;
class FpsCounter {
private String mName;
private String mIdent;
private long fpsLast = System.currentTimeMillis() + 1000;
private int fpsCount = 0;
FpsCounter(String logName, String ident) {
mName = logName;
mIdent = "fps " + ident + " ";
}
public void fps() {
fpsCount += 1;
long fpsNow = System.currentTimeMillis();
if (fpsNow > fpsLast) {
Log.w(mName, mIdent + fpsCount);
fpsLast = fpsNow + 1000;
fpsCount = 0;
}
}
}

View File

@ -186,6 +186,7 @@ public class KegsMain extends Activity implements KegsKeyboard.StickyReset {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage(getResources().getText(R.string.rom_error));
// TODO do getActivity().finish() on button clicks
// TODO setCanceledOnTouchOutside(false) ? otherwise can accidentally dismiss the error.
return builder.create();
}
}
@ -193,46 +194,23 @@ public class KegsMain extends Activity implements KegsKeyboard.StickyReset {
private void setScreenSize() {
final int width = getResources().getDisplayMetrics().widthPixels;
final int height = getResources().getDisplayMetrics().heightPixels;
final BitmapSize bitmapSize = new BitmapSize(width, height);
// If we can fit at least 90% of a scaled screen into the display area, do it.
// If we hit less than 100% height, turn off system action bar and title.
// If ((400 + 32) * scale) > height, then crop border.
mKegsView.updateScreenSize(bitmapSize);
float scaleX = 1.0f;
float scaleY = 1.0f;
boolean crop = false;
// Force integer scaling on X axis.
scaleX = (float)Math.round((width * 0.9) / 640);
// TODO: Fix '48' hack being used for system buttons or soft buttons.
scaleY = Math.min(scaleX, (height - 48) / 400.0f);
// If Y would be compressed in a weird way, reduce the scale and use 1:1.
if ((scaleX - scaleY) > 0.5) {
scaleX = Math.max(1, scaleX - 1);
scaleY = scaleX;
}
if (height < ((400 + 64) * scaleY)) {
ActionBar actionBar = getActionBar();
if (actionBar != null && actionBar.isShowing()) {
actionBar.hide();
}
} else {
ActionBar actionBar = getActionBar();
final ActionBar actionBar = getActionBar();
if (bitmapSize.showActionBar()) {
if (actionBar != null && !actionBar.isShowing()) {
actionBar.show();
}
} else {
if (actionBar != null && actionBar.isShowing()) {
actionBar.hide();
}
}
// TODO: Fix '32' and '64' for software buttons and window decorations.
if (height < ((400 + 32 + 64) * scaleY)) {
crop = true;
}
Log.w("kegs", "using scale " + scaleX + ":" + scaleY + " " + crop);
mKegsView.setScale(scaleX, scaleY, crop);
// Force another redraw of the bitmap into the canvas. Bug workaround.
mKegsView.getThread().updateScreen();
}
@Override
@ -259,7 +237,8 @@ public class KegsMain extends Activity implements KegsKeyboard.StickyReset {
setContentView(R.layout.main);
mKegsView = (KegsView)findViewById(R.id.kegsview);
setScreenSize(); // This causes an unnecessary requestLayout of KegsView.
setScreenSize(); // TODO This causes an unnecessary requestLayout of KegsView.
mKegsTouch = new KegsTouch(mKegsView.getEventQueue());
final GestureDetector inputDetect = new GestureDetector(this, mKegsTouch);

View File

@ -5,7 +5,8 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
@ -14,65 +15,64 @@ import android.view.View;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.ConcurrentLinkedQueue;
// This is the primary interface into the native KEGS thread, and also
// where the native KEGS thread calls back into Java.
class KegsView extends SurfaceView implements SurfaceHolder.Callback {
private static final int mA2Width = 640 + 32 + 32; // kegs defcomm.h
private static final int mA2Height = 400 + 32 + 30; // kegs defcomm.h
// Reported area of this view, see updateScreenSize()
private int mWidth = 0;
private int mHeight = 0;
// Look also at mPauseLock.
private boolean mHaveSurface = false;
private boolean mPaused = false;
private boolean mReady = false;
// Bitmap draw options.
private boolean mScaled = false;
private float mScaleFactorX = 1.0f;
private float mScaleFactorY = 1.0f;
private boolean mReady = false; // 'true' will begin the native thread.
protected ConcurrentLinkedQueue<Event.KegsEvent> mEventQueue = new ConcurrentLinkedQueue<Event.KegsEvent>();
private BitmapThread mBitmapThread;
class KegsThread extends Thread {
private Handler mHandler;
private Bitmap mBitmap;
private Canvas mCanvas;
private SurfaceHolder mSurfaceHolder;
private Context mContext;
private final ReentrantLock mSurfaceLock = new ReentrantLock();
private final ReentrantLock mPauseLock = new ReentrantLock();
private Rect mRectSrc = new Rect(0, 0, mA2Width, mA2Height);
private Rect mRectDst = new Rect(0, 0, mA2Width, mA2Height);
public KegsThread(SurfaceHolder surfaceHolder, Context context) {
mSurfaceHolder = surfaceHolder;
mContext = context;
mBitmap = Bitmap.createBitmap(mA2Width, mA2Height,
mBitmap = Bitmap.createBitmap(BitmapSize.Const.A2Width,
BitmapSize.Const.A2Height,
Bitmap.Config.ARGB_8888);
mBitmap.setHasAlpha(false);
mBitmapThread = new BitmapThread();
mHandler = mBitmapThread.getHandler();
mBitmapThread.setBitmap(surfaceHolder, mBitmap, mSurfaceLock);
}
// Typically called by the native thread, but this can also be
// called on the UI thread via setHaveSurface.
private FpsCounter fpsCount = new FpsCounter("kegs", "native");
// Typically updateScreen is called by the native thread,
// but it may also be run on the UI thread.
//
// We use a Handler to tell the bitmap thread to actually draw
// on the canvas. No locking is involved, so it is possible for
// the canvas to get a bitmap that is in the process of being updated
// by the native thread. This should be relatively uncommon.
//
// If you wish to draw to the canvas in the native thread, it should
// be safe to bypass the Handler and call mBitmapThread.updateScreen()
// here instead.
protected void updateScreen() {
mSurfaceLock.lock();
try {
if (!mHaveSurface) {
return; // unlock with finally
}
mCanvas = mSurfaceHolder.lockCanvas(); // Use Rect ?
if(mCanvas != null) {
mCanvas.drawARGB(255, 0, 0, 0); // TODO: Figure out why necessary.
if (!mScaled) {
mCanvas.drawBitmap(mBitmap, mRectSrc, mRectDst, null);
} else {
mCanvas.save();
mCanvas.scale(mScaleFactorX, mScaleFactorY);
mCanvas.drawBitmap(mBitmap, mRectSrc, mRectDst, null);
mCanvas.restore();
}
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
mCanvas = null;
}
} finally {
mSurfaceLock.unlock();
}
// Empty the queue first in case bitmap thread is lagging behind.
mHandler.removeMessages(0);
mHandler.sendEmptyMessage(0);
fpsCount.fps();
}
private void checkForPause() {
@ -83,6 +83,7 @@ class KegsView extends SurfaceView implements SurfaceHolder.Callback {
}
}
// See jni/android_driver.c:mainLoop()
private native void mainLoop(Bitmap b, ConcurrentLinkedQueue q);
@Override
@ -111,12 +112,13 @@ class KegsView extends SurfaceView implements SurfaceHolder.Callback {
if (!mReady) {
return; // bail out, we haven't started doing anything yet
}
thread.updateScreen();
updateScreen();
if (mPaused) {
mPaused = false;
mPauseLock.unlock();
} else if (!thread.isAlive()) {
thread.start();
mBitmapThread.start();
}
}
@ -124,40 +126,8 @@ class KegsView extends SurfaceView implements SurfaceHolder.Callback {
public boolean nowPaused() {
return mPauseLock.hasQueuedThreads();
}
public void setScale(float scaleFactorX, float scaleFactorY, boolean cropBorder) {
mSurfaceLock.lock();
if (scaleFactorX == 1.0f && scaleFactorY == 1.0f) {
mScaled = false;
} else {
mScaled = true;
}
mScaleFactorX = scaleFactorX;
mScaleFactorY = scaleFactorY;
if (cropBorder) {
mRectSrc = new Rect(0, 32, mA2Width, mA2Height);
mRectDst = new Rect(0, 0, mA2Width, mA2Height - 32);
} else {
mRectSrc = new Rect(0, 0, mA2Width, mA2Height);
mRectDst = new Rect(0, 0, mA2Width, mA2Height);
}
mSurfaceLock.unlock();
updateScreen();
}
public void setHaveSurface(boolean haveSurface) {
mSurfaceLock.lock();
mHaveSurface = haveSurface;
mSurfaceLock.unlock();
if (haveSurface) {
// Refresh the canvas when we obtain a surface.
updateScreen();
}
}
}
private Context mContext;
private KegsThread thread;
public KegsView(Context context, AttributeSet attrs) {
@ -188,20 +158,9 @@ class KegsView extends SurfaceView implements SurfaceHolder.Callback {
return mEventQueue;
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
// TODO: check to see if this is necessary, for example during alarms or phone calls.
// if (!hasWindowFocus) {
// thread.onPause();
// } else {
// thread.onResume();
// }
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension((int)(mA2Width * mScaleFactorX),
(int)(mA2Height * mScaleFactorY));
setMeasuredDimension(mWidth, mHeight);
}
public native String stringFromJNI();
@ -216,9 +175,11 @@ class KegsView extends SurfaceView implements SurfaceHolder.Callback {
}
}
public void setScale(float scaleFactorX, float scaleFactorY, boolean cropBorder) {
thread.setScale(scaleFactorX, scaleFactorY, cropBorder);
public void updateScreenSize(BitmapSize bitmapSize) {
mWidth = bitmapSize.getViewWidth();
mHeight = bitmapSize.getViewHeight();
requestLayout();
mBitmapThread.updateScreenSize(bitmapSize);
}
public void surfaceChanged(SurfaceHolder holder,
@ -228,10 +189,10 @@ class KegsView extends SurfaceView implements SurfaceHolder.Callback {
// The surface callbacks are occasionally called in between pause and resume.
public void surfaceCreated(SurfaceHolder holder) {
thread.setHaveSurface(true);
mBitmapThread.setHaveSurface(true);
}
public void surfaceDestroyed(SurfaceHolder holder) {
thread.setHaveSurface(false);
mBitmapThread.setHaveSurface(false);
}
}