SlideShare a Scribd company logo
Tricks to Making a Realtime
SurfaceView Actually Perform in
Realtime
Maarten Edgar
Hello, my name is …
Hello, my name is …
Maarten Edgar
What we’ll cover
SurfaceViews:
• Why
• When
• What
• How
• Hard earned lessons
Why use a SurfaceView?
SurfaceView
GL_SurfaceView
TextureView
SurfaceTexture
View
What is a SurfaceView?
A View which gives you access to a
Surface using .getHolder(), which is
drawn on a seperate thread and is
double/triple buffered behind the
scenes.
It cuts holes and displays underneath
the window it is in.
How to use it:
• Setup
• Threads vs Runnables and other
control mechanisms
• Loops
• UI communication
• Tips
Setup
• Activity/View
• SurfaceView and its Thread
Setup: Activity and View
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// set flags as needed
getWindow().setFormat(PixelFormat.RGBA_8888);
this.setVolumeControlStream(AudioManager.STREAM_MUSIC);
setContentView(R.layout.activity_game);
// get handles to the View from XML, and its Thread
mCSurfaceView = (MySurfaceView) findViewById(R.id.surfaceview);
setSurfaceType(View.LAYER_TYPE_SOFTWARE);
mSurfaceViewThread = mSurfaceView.getThread();
createInputObjectPool();
Your SurfaceView class
public class ChiBlastSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
public ChiBlastSurfaceView(Context context) {
super(context);
mSurfaceCreated = false;
touchBool = true;
// register our interest in hearing about changes to our surface
SurfaceHolder holder = getHolder();
holder.addCallback(this);
myHandler = new MyInnerHandler(this);
// create thread only; it's started in surfaceCreated()
thread = new ChiBlastSurfaceViewThread(holder, context, myHandler);
setFocusable(true); // make sure we get key events
}
Your SurfaceView callbacks 1/3
SurfaceHolder.Callback:
@Override
public void surfaceCreated(SurfaceHolder holder) {
// start the thread here so that we don't busy-wait
in run() waiting for the surface to be created
if (mSurfaceCreated == false)
{
createThread(holder);
mSurfaceCreated = true;
touchBool = true;
}
}
Your SurfaceView callbacks 2/3
SurfaceHolder.Callback:
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mSurfaceCreated = false;
cleanupResource();
terminateThread();
}
Your SurfaceView callbacks 3/3
SurfaceHolder.Callback:
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
thread.setSurfaceSize(width, height);
}
Setup: driving the SurfaceView
Runnables, thread and loops, oh my!
Setup: Thread
public class ChiBlastSurfaceViewThread extends Thread {
public ChiBlastSurfaceViewThread(SurfaceHolder surfaceHolder,
Context context, Handler handler) {
// get handles to some important objects
mSurfaceHolder = surfaceHolder;
mSurfaceHolder.setFormat(PixelFormat.RGBA_8888);
mContext = context;
res = context.getResources();
//any other initialization:
ops = new BitmapFactory.Options();
ops.inPurgeable = true;
ops.inDensity = 0;
ops.inDither = false;
ops.inScaled = false;
ops.inPreferredConfig = Bitmap.Config.ARGB_8888;
ops.inJustDecodeBounds = false;
}
@Override
public void run() {
while (mRun) {
Canvas c = null;
try {
// update game state
processInput();
//if (mMode == STATE_SCROLL_MAP)
if (mMode != STATE_PAUSE)
{
updatePhysics(timeDiff);
}
c = mSurfaceHolder.lockCanvas(null);
synchronized (mSurfaceHolder) {
doDraw(c);
}
} finally {
// do this in a finally so that if an exception is thrown
// during the above, we don't leave the Surface in an
// inconsistent state
if (c != null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
Setup: Thread
The Thread and your Activity
What does this now mean for your
Activity?
or
How do we make this fit into the
Android Lifecycle?
The Thread and your Activity
@Override
protected void onPause() {
super.onPause();
// pause game when Activity pauses
mSurfaceView.getThread().pause();
mSurfaceView.terminateThread();
System.gc();
}
The Thread and your Activity
@Override
protected void onResume()
{
super.onResume();
if (mSurfaceView.mSurfaceCreated)
{
mSurfaceView.createThread(mSurfaceView.getHolder());
setSurfaceType(View.LAYER_TYPE_SOFTWARE);
}
mSurfaceView.SetTouch(true);
}
The Thread and your Activity
@Override
protected void onRestoreInstanceState(Bundle inState) {
// just have the View's thread load its state from our Bundle
if (mSurfaceView.mSurfaceCreated)
{
mSurfaceView.createThread(mSurfaceView.getHolder());
setSurfaceType(View.LAYER_TYPE_SOFTWARE);
}
mSurfaceViewThread.restoreState(inState);
}
The main loop
• AFAFP
• Fixed step
@Override
public void run() {
long beginTime; // the time when the cycle begun
long timeDiff; // the time it took for the cycle to execute
int sleepTime; // ms to sleep (<0 if we're behind)
int framesSkipped; // number of frames being skipped
timeDiff = System.currentTimeMillis()+50;
sleepTime = 0;
while (mRun) {
Canvas c = null;
try {
beginTime = System.currentTimeMillis();
framesSkipped = 0; // resetting the frames skipped
// update game state
processInput();
//if (mMode == STATE_SCROLL_MAP)
if (mMode != STATE_PAUSE)
{
updatePhysics(timeDiff);
}
c = mSurfaceHolder.lockCanvas(null);
synchronized (mSurfaceHolder) {
doDraw(c);
}
The main loop 1/3
The main loop 2/3
// calculate how long did the cycle take
timeDiff = System.currentTimeMillis() - beginTime;
// calculate sleep time
sleepTime = (int)(FRAME_PERIOD - timeDiff);
if (sleepTime > 0) {
// if sleepTime > 0 we're OK
try {
// send the thread to sleep for a short period
// very useful for battery saving
Thread.sleep(sleepTime);
} catch (InterruptedException e) {}
}
while (sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
// we need to catch up
// update without rendering
processInput();
if (mMode != STATE_PAUSE)
{
updatePhysics(timeDiff);
}
// add frame period to check if in next frame
sleepTime += FRAME_PERIOD;
framesSkipped++;
}
The main loop 3/3
} finally {
// do this in a finally so that if an exception is thrown
// during the above, we don't leave the Surface in an
// inconsistent state
if (c != null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
UI Communication
Use a Handler:
static class MyInnerHandler extends Handler {
private final WeakReference<ChiBlastSurfaceView> mView;
MyInnerHandler(ChiBlastSurfaceView aView) {
mView = new WeakReference<ChiBlastSurfaceView>(aView);
}
@Override
public void handleMessage(Message m) {
ChiBlastSurfaceView theView = mView.get();
theView.mStatusText.setText(m.getData().getString("text"));
if (m.getData().getInt("viz") == View.VISIBLE)
{
theView.mStatusText.setVisibility(View.VISIBLE);
//mStatusText.setAnimation(displayTextAnim);
//mStatusText.startAnimation(displayTextAnim);
}
else
{
if (m.getData().getInt("viz") == View.INVISIBLE)
{
theView.mStatusText.setVisibility(View.INVISIBLE);
theView.mStatusText.setAnimation(null);
}
else if (m.getData().getInt("viz") == View.GONE)
{
theView.mStatusText.setVisibility(View.GONE);
}
}
theView.mStatusText.invalidate();
}
}
Setup: Cleanup
public void terminateThread ()
{
boolean retry = true;
thread.setRunning(false);
while (retry) {
try
{
thread.join();
retry = false;
}
catch (InterruptedException e)
{
}
//break; //THIS BREAKS IT ON PUSHING HOME
}
//thread = null; //THIS BREAKS IT ON PUSHING HOME
}
Tips
• Input buffer
• Object creation
• Scaling
• Drawing, bitmaps and other dirty
things
Tips: input buffer in SVActivity
private void createInputObjectPool() {
inputObjectPool = new ArrayBlockingQueue<InputObject>(INPUT_QUEUE_SIZE);
for (int i = 0; i < INPUT_QUEUE_SIZE; i++) {
inputObjectPool.add(new InputObject(inputObjectPool));
}
}
public class InputObject {
public static final byte EVENT_TYPE_KEY = 1;
public static final byte EVENT_TYPE_TOUCH = 2;
public static final int ACTION_KEY_DOWN = 1;
public static final int ACTION_KEY_UP = 2;
public static final int ACTION_TOUCH_DOWN = MotionEvent.ACTION_DOWN;
public static final int ACTION_TOUCH_POINTER_DOWN = MotionEvent.ACTION_POINTER_DOWN;
//public static final int ACTION_TOUCH_POINTER_2_DOWN = MotionEvent.ACTION_POINTER_2_DOWN;
public static final int ACTION_TOUCH_MOVE = MotionEvent.ACTION_MOVE;
public static final int ACTION_TOUCH_UP = MotionEvent.ACTION_UP;
public static final int ACTION_TOUCH_POINTER_UP = MotionEvent.ACTION_POINTER_UP;
//public static final int ACTION_TOUCH_POINTER_2_UP = MotionEvent.ACTION_POINTER_2_UP;
public ArrayBlockingQueue<InputObject> pool;
public byte eventType;
public long time;
public int action;
public int keyCode;
public int x;
public int y;
public int x2;
public int y2;
public int pointerID;
public int pointerIndex;
public int pointerIndex2;
InputObject 1/5
InputObject 2/5
public InputObject(ArrayBlockingQueue<InputObject> pool) {
this.pool = pool;
}
public void useEvent(KeyEvent event) {
eventType = EVENT_TYPE_KEY;
int a = event.getAction();
switch (a) {
case KeyEvent.ACTION_DOWN:
action = ACTION_KEY_DOWN;
break;
case KeyEvent.ACTION_UP:
action = ACTION_KEY_UP;
break;
default:
action = 0;
}
time = event.getEventTime();
keyCode = event.getKeyCode();
}
public void useEvent(MotionEvent event) {
eventType = EVENT_TYPE_TOUCH;
int a = event.getAction();
switch (a) {
case MotionEvent.ACTION_DOWN:
action = ACTION_TOUCH_DOWN;
break;
case MotionEvent.ACTION_POINTER_DOWN:
action = ACTION_TOUCH_POINTER_DOWN;
break;
case MotionEvent.ACTION_POINTER_2_DOWN:
action = ACTION_TOUCH_POINTER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
action = ACTION_TOUCH_MOVE;
break;
case MotionEvent.ACTION_UP:
action = ACTION_TOUCH_UP;
break;
case MotionEvent.ACTION_POINTER_UP:
action = ACTION_TOUCH_POINTER_UP;
break;
case MotionEvent.ACTION_POINTER_2_UP:
action = ACTION_TOUCH_POINTER_UP;
break;
default:
action = -1;
}
InputObject 3/5
InputObject 4/5
time = event.getEventTime();
pointerIndex = (event.getAction() &
MotionEvent.ACTION_POINTER_ID_MASK) >>
MotionEvent.ACTION_POINTER_ID_SHIFT;
pointerID = event.getPointerId(pointerIndex);
x = (int) event.getX(pointerIndex);
y = (int) event.getY(pointerIndex);
if (event.getPointerCount() > 1)
{
pointerIndex2 = pointerIndex== 0 ? 1 : 0;
x2 = (int)event.getX(pointerIndex2);
y2 = (int)event.getY(pointerIndex2);
}
}
InputObject 5/5
public void useEventHistory(MotionEvent event, int historyItem) {
eventType = EVENT_TYPE_TOUCH;
action = ACTION_TOUCH_MOVE;
time = event.getHistoricalEventTime(historyItem);
pointerIndex = (event.getAction() &
MotionEvent.ACTION_POINTER_ID_MASK) >>
MotionEvent.ACTION_POINTER_ID_SHIFT;
pointerID = event.getPointerId(pointerIndex);
x = (int) event.getHistoricalX(pointerIndex, historyItem);
y = (int) event.getHistoricalY(pointerIndex, historyItem);
if (event.getPointerCount() > 1)
{
pointerIndex2 = pointerIndex== 0 ? 1 : 0;
x2 = (int) event.getHistoricalX(pointerIndex2, historyItem);
y2 = (int) event.getHistoricalY(pointerIndex2, historyItem);
}
}
public void returnToPool() {
pool.add(this);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
// history first
int hist = event.getHistorySize();
if (hist > 0)
{
// add from oldest to newest
for (int i = 0; i < hist; i++)
{
//for (int i = hist-1; i > -1; i--) {
InputObject input = inputObjectPool.take();
input.useEventHistory(event, i);
mSurfaceViewThread.feedInput(input);
}
}
// current last
InputObject input = inputObjectPool.take();
input.useEvent(event);
mSurfaceViewThread.feedInput(input);
} catch (InterruptedException e) {
}
// don't allow more than 60 motion events per second
try {
Thread.sleep(16);
} catch (InterruptedException e) {
}
return true;
}
Back to the activity:
public void feedInput(InputObject input) {
synchronized(inputQueueMutex) {
try {
inputQueue.put(input);
} catch (InterruptedException e) {
//Log.e(TAG, e.getMessage(), e);
}
}
}
private void processInput() {
synchronized(inputQueueMutex) {
ArrayBlockingQueue<InputObject> inputQueue = ChiBlastSurfaceView.inputQueue;
while (!inputQueue.isEmpty()) {
try {
InputObject input = inputQueue.take();
if (input.eventType == InputObject.EVENT_TYPE_KEY) {
//processKeyEvent(input);
} else if (input.eventType == InputObject.EVENT_TYPE_TOUCH) {
processMotionEvent(input);
}
input.returnToPool();
} catch (InterruptedException e) {
//Log.e(TAG, e.getMessage(), e);
}
}
}
}
And in the SurfaceView.Thread:
Tips: object creation
Tips: object creation
Just don’t do it.
Tips: object creation
Or do it up front.
No matter how odd that sometimes
may seem.
Tips: scaling
Two types of scaling:
• Realtime whole view SV scaling
only works from Android N
• Fixed scaling (as done in Unreal
Tournament 3)
Tips: scaling
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
//thread.setSurfaceSize(width, height);
if (mCanvasWidth != width)
{
int scaledWidth = (int)(width*0.75f);
int scaledHeight = (int)(height*0.75f);
if (scaledHeight != height)
{
yRatio = (float)(scaledHeight / (float)height);
xRatio = (float)(scaledWidth / (float)width);
}
holder.setFixedSize(scaledWidth, scaledHeight);
thread.setSurfaceSize(scaledWidth, scaledHeight);
}
}
Tips: drawing, bitmaps and other
dirty things
Tips: drawing, bitmaps and other
dirty things
In SurfaceView.Thread doDraw():
canvas.drawBitmap(mBackgroundImage,
null, fullscreenRect, mPicPaint);
Q&A
Thank you!
Maarten Edgar
lifeboatsoft@gmail.com
Resources:
https://source.android.com/devices/graphics/architecture.html
https://github.com/google/grafika
Ad

Recommended

PDF
Knock, knock, who is there? Doze.
Yonatan Levin
 
PDF
Engineering Wunderlist for Android - Ceasr Valiente, 6Wunderkinder
DroidConTLV
 
PDF
Modern Android app library stack
Tomáš Kypta
 
PDF
Deep Dive into Zone.JS
Ilia Idakiev
 
PDF
mDevCamp - The Best from Google IO
ondraz
 
PPTX
Become a Garbage Collection Hero
Tier1app
 
PPTX
Become a GC Hero
Tier1app
 
ODP
Android App Development - 07 Threading
Diego Grancini
 
PPTX
Don't dump thread dumps
Tier1app
 
PDF
Why Task Queues - ComoRichWeb
Bryan Helmig
 
PPTX
Taking advantage of the Amazon Web Services (AWS) Family
Ben Hall
 
PDF
Advanced iOS Build Mechanics, Sebastien Pouliot
Xamarin
 
PPTX
GC Tuning & Troubleshooting Crash Course
Tier1 app
 
PDF
Developing Async Sense
Nemanja Stojanovic
 
PDF
React, Redux and es6/7
Dongho Cho
 
PPTX
Troubleshooting real production problems
Tier1 app
 
PPTX
MongoDB: tips, trick and hacks
Scott Hernandez
 
PPTX
[NDC 2019] Functions 2.0: Enterprise-Grade Serverless
KatyShimizu
 
PDF
Cassandra Summit EU 2014 Lightning talk - Paging (no animation)
Christopher Batey
 
PDF
Third Party Auth in WebObjects
WO Community
 
PPTX
RxJS In-Depth - AngularConnect 2015
Ben Lesh
 
PPTX
Angular 1 + es6
장현 한
 
PDF
Adventures in Multithreaded Core Data
Inferis
 
PDF
LJC Conference 2014 Cassandra for Java Developers
Christopher Batey
 
PPT
Troubleshooting performanceavailabilityproblems (1)
Tier1 app
 
PDF
Snapshot clone-boot-presentation-final
Open Stack
 
PDF
Building Scalable Stateless Applications with RxJava
Rick Warren
 
PDF
Cognitive interaction using Wearables - Eyal herman, IBM
DroidConTLV
 
PDF
3 things every Android developer must know about Microsoft - Ido Volff, Micro...
DroidConTLV
 
PPTX
Creating killer apps powered by watson cognitive services - Ronen Siman-Tov, IBM
DroidConTLV
 

More Related Content

What's hot (19)

PPTX
Don't dump thread dumps
Tier1app
 
PDF
Why Task Queues - ComoRichWeb
Bryan Helmig
 
PPTX
Taking advantage of the Amazon Web Services (AWS) Family
Ben Hall
 
PDF
Advanced iOS Build Mechanics, Sebastien Pouliot
Xamarin
 
PPTX
GC Tuning & Troubleshooting Crash Course
Tier1 app
 
PDF
Developing Async Sense
Nemanja Stojanovic
 
PDF
React, Redux and es6/7
Dongho Cho
 
PPTX
Troubleshooting real production problems
Tier1 app
 
PPTX
MongoDB: tips, trick and hacks
Scott Hernandez
 
PPTX
[NDC 2019] Functions 2.0: Enterprise-Grade Serverless
KatyShimizu
 
PDF
Cassandra Summit EU 2014 Lightning talk - Paging (no animation)
Christopher Batey
 
PDF
Third Party Auth in WebObjects
WO Community
 
PPTX
RxJS In-Depth - AngularConnect 2015
Ben Lesh
 
PPTX
Angular 1 + es6
장현 한
 
PDF
Adventures in Multithreaded Core Data
Inferis
 
PDF
LJC Conference 2014 Cassandra for Java Developers
Christopher Batey
 
PPT
Troubleshooting performanceavailabilityproblems (1)
Tier1 app
 
PDF
Snapshot clone-boot-presentation-final
Open Stack
 
PDF
Building Scalable Stateless Applications with RxJava
Rick Warren
 
Don't dump thread dumps
Tier1app
 
Why Task Queues - ComoRichWeb
Bryan Helmig
 
Taking advantage of the Amazon Web Services (AWS) Family
Ben Hall
 
Advanced iOS Build Mechanics, Sebastien Pouliot
Xamarin
 
GC Tuning & Troubleshooting Crash Course
Tier1 app
 
Developing Async Sense
Nemanja Stojanovic
 
React, Redux and es6/7
Dongho Cho
 
Troubleshooting real production problems
Tier1 app
 
MongoDB: tips, trick and hacks
Scott Hernandez
 
[NDC 2019] Functions 2.0: Enterprise-Grade Serverless
KatyShimizu
 
Cassandra Summit EU 2014 Lightning talk - Paging (no animation)
Christopher Batey
 
Third Party Auth in WebObjects
WO Community
 
RxJS In-Depth - AngularConnect 2015
Ben Lesh
 
Angular 1 + es6
장현 한
 
Adventures in Multithreaded Core Data
Inferis
 
LJC Conference 2014 Cassandra for Java Developers
Christopher Batey
 
Troubleshooting performanceavailabilityproblems (1)
Tier1 app
 
Snapshot clone-boot-presentation-final
Open Stack
 
Building Scalable Stateless Applications with RxJava
Rick Warren
 

Viewers also liked (17)

PDF
Cognitive interaction using Wearables - Eyal herman, IBM
DroidConTLV
 
PDF
3 things every Android developer must know about Microsoft - Ido Volff, Micro...
DroidConTLV
 
PPTX
Creating killer apps powered by watson cognitive services - Ronen Siman-Tov, IBM
DroidConTLV
 
PDF
Android Application Optimization: Overview and Tools - Oref Barad, AVG
DroidConTLV
 
PDF
Android is going to Go! - Android and goland - Almog Baku
DroidConTLV
 
PDF
Write code that writes code! A beginner's guide to Annotation Processing - Ja...
DroidConTLV
 
PPTX
Will it run or will it not run? Background processes in Android 6 - Anna Lifs...
DroidConTLV
 
PPTX
Good Rules for Bad Apps - Shem magnezi
DroidConTLV
 
PDF
Intro to Dependency Injection - Or bar
DroidConTLV
 
PDF
Mobile SDKs: Use with Caution - Ori Lentzitzky
DroidConTLV
 
PDF
Context is Everything - Royi Benyossef
DroidConTLV
 
PPTX
Set it and forget it: Let the machine learn its job - Guy Baron, Vonage
DroidConTLV
 
PDF
Think Async: Understanding the Complexity of Multithreading - Avi Kabizon & A...
DroidConTLV
 
PDF
Knock knock! Who's there? Doze. - Yonatan Levin
DroidConTLV
 
PPTX
Optimize your delivery and quality with the right release methodology and too...
DroidConTLV
 
PDF
Android Continuous Integration and Automation - Enrique Lopez Manas, Sixt
DroidConTLV
 
PDF
Build an App with Blindfold - Britt Barak
DroidConTLV
 
Cognitive interaction using Wearables - Eyal herman, IBM
DroidConTLV
 
3 things every Android developer must know about Microsoft - Ido Volff, Micro...
DroidConTLV
 
Creating killer apps powered by watson cognitive services - Ronen Siman-Tov, IBM
DroidConTLV
 
Android Application Optimization: Overview and Tools - Oref Barad, AVG
DroidConTLV
 
Android is going to Go! - Android and goland - Almog Baku
DroidConTLV
 
Write code that writes code! A beginner's guide to Annotation Processing - Ja...
DroidConTLV
 
Will it run or will it not run? Background processes in Android 6 - Anna Lifs...
DroidConTLV
 
Good Rules for Bad Apps - Shem magnezi
DroidConTLV
 
Intro to Dependency Injection - Or bar
DroidConTLV
 
Mobile SDKs: Use with Caution - Ori Lentzitzky
DroidConTLV
 
Context is Everything - Royi Benyossef
DroidConTLV
 
Set it and forget it: Let the machine learn its job - Guy Baron, Vonage
DroidConTLV
 
Think Async: Understanding the Complexity of Multithreading - Avi Kabizon & A...
DroidConTLV
 
Knock knock! Who's there? Doze. - Yonatan Levin
DroidConTLV
 
Optimize your delivery and quality with the right release methodology and too...
DroidConTLV
 
Android Continuous Integration and Automation - Enrique Lopez Manas, Sixt
DroidConTLV
 
Build an App with Blindfold - Britt Barak
DroidConTLV
 
Ad

Similar to Tricks to Making a Realtime SurfaceView Actually Perform in Realtime - Maarten Edger (20)

PDF
Getting Started with 3D Game Development on Nokia Series 40 Asha Phones
Microsoft Mobile Developer
 
PDF
Developing games for Series 40 full-touch UI
Microsoft Mobile Developer
 
PDF
Project meeting: Android Graphics Architecture Overview
Yu-Hsin Hung
 
PDF
Creating Games for Asha - platform
Jussi Pohjolainen
 
PDF
Breathing the life into the canvas
Tomislav Homan
 
PDF
[1D6]RE-view of Android L developer PRE-view
NAVER D2
 
PPTX
Optimizing Games for Mobiles
St1X
 
PPT
Advanced Game Development with the Mobile 3D Graphics API
Tomi Aarnio
 
DOCX
Android canvas-chapter20
Dr. Ramkumar Lakshminarayanan
 
PDF
Implementing a Simple Game using libGDX
Jussi Pohjolainen
 
PPT
Why your Android Apps Suck
rogeryi
 
PDF
Дмитрий Вовк - Learn iOS Game Optimization. Ultimate Guide
UA Mobile
 
PPTX
Advanced #4 GPU & Animations
Vitali Pekelis
 
PPTX
Beginning android games
Mario Zechner
 
PPTX
Developing Virtual Reality Application using Google Cardboard
apurvmmmec
 
PPTX
Virtual Reality Application Development on Android using Google Cardboard
Apurv Nigam
 
PDF
Tools for developing Android Games
Platty Soft
 
PDF
Make believe - Droidcon UK 2015
Shanee Nishry
 
PPTX
Introduction to open gl in android droidcon - slides
tamillarasan
 
PDF
Abc2011 yagi
Toshihiro Yagi
 
Getting Started with 3D Game Development on Nokia Series 40 Asha Phones
Microsoft Mobile Developer
 
Developing games for Series 40 full-touch UI
Microsoft Mobile Developer
 
Project meeting: Android Graphics Architecture Overview
Yu-Hsin Hung
 
Creating Games for Asha - platform
Jussi Pohjolainen
 
Breathing the life into the canvas
Tomislav Homan
 
[1D6]RE-view of Android L developer PRE-view
NAVER D2
 
Optimizing Games for Mobiles
St1X
 
Advanced Game Development with the Mobile 3D Graphics API
Tomi Aarnio
 
Android canvas-chapter20
Dr. Ramkumar Lakshminarayanan
 
Implementing a Simple Game using libGDX
Jussi Pohjolainen
 
Why your Android Apps Suck
rogeryi
 
Дмитрий Вовк - Learn iOS Game Optimization. Ultimate Guide
UA Mobile
 
Advanced #4 GPU & Animations
Vitali Pekelis
 
Beginning android games
Mario Zechner
 
Developing Virtual Reality Application using Google Cardboard
apurvmmmec
 
Virtual Reality Application Development on Android using Google Cardboard
Apurv Nigam
 
Tools for developing Android Games
Platty Soft
 
Make believe - Droidcon UK 2015
Shanee Nishry
 
Introduction to open gl in android droidcon - slides
tamillarasan
 
Abc2011 yagi
Toshihiro Yagi
 
Ad

More from DroidConTLV (20)

PDF
Mobile Development in the Information Age - Yossi Elkrief, Nike
DroidConTLV
 
PDF
Doing work in the background - Darryn Campbell, Zebra Technologies
DroidConTLV
 
PDF
No more video loss - Alex Rivkin, Motorola Solutions
DroidConTLV
 
PDF
Mobile at Scale: from startup to a big company - Dor Samet, Booking.com
DroidConTLV
 
PDF
LiveData on Steroids - Giora Shevach + Shahar Ben Moshe, Climacell
DroidConTLV
 
PDF
MVVM In real life - Lea Cohen Tannoudji, Lightricks
DroidConTLV
 
PDF
Best Practices for Using Mobile SDKs - Lilach Wagner, SafeDK (AppLovin)
DroidConTLV
 
PDF
Building Apps with Flutter - Hillel Coren, Invoice Ninja
DroidConTLV
 
PDF
New Android Project: The Most Important Decisions - Vasiliy Zukanov
DroidConTLV
 
PDF
Designing a Design System - Shai Mishali, Gett
DroidConTLV
 
PDF
The Mighty Power of the Accessibility Service - Guy Griv, Pepper
DroidConTLV
 
PDF
Kotlin Multiplatform in Action - Alexandr Pogrebnyak - IceRockDev
DroidConTLV
 
PDF
Flutter State Management - Moti Bartov, Tikal
DroidConTLV
 
PDF
Reactive UI in android - Gil Goldzweig Goldbaum, 10bis
DroidConTLV
 
PDF
Fun with flutter animations - Divyanshu Bhargava, GoHighLevel
DroidConTLV
 
PDF
DroidconTLV 2019
DroidConTLV
 
PDF
Ok google, it's time to bot! - Hadar Franco, Albert + Stav Levi, Monday
DroidConTLV
 
PDF
Introduction to React Native - Lev Vidrak, Wix
DroidConTLV
 
PDF
Bang-Bang, you have been hacked - Yonatan Levin, KolGene
DroidConTLV
 
PDF
Educating your app – adding ML edge to your apps - Maoz Tamir
DroidConTLV
 
Mobile Development in the Information Age - Yossi Elkrief, Nike
DroidConTLV
 
Doing work in the background - Darryn Campbell, Zebra Technologies
DroidConTLV
 
No more video loss - Alex Rivkin, Motorola Solutions
DroidConTLV
 
Mobile at Scale: from startup to a big company - Dor Samet, Booking.com
DroidConTLV
 
LiveData on Steroids - Giora Shevach + Shahar Ben Moshe, Climacell
DroidConTLV
 
MVVM In real life - Lea Cohen Tannoudji, Lightricks
DroidConTLV
 
Best Practices for Using Mobile SDKs - Lilach Wagner, SafeDK (AppLovin)
DroidConTLV
 
Building Apps with Flutter - Hillel Coren, Invoice Ninja
DroidConTLV
 
New Android Project: The Most Important Decisions - Vasiliy Zukanov
DroidConTLV
 
Designing a Design System - Shai Mishali, Gett
DroidConTLV
 
The Mighty Power of the Accessibility Service - Guy Griv, Pepper
DroidConTLV
 
Kotlin Multiplatform in Action - Alexandr Pogrebnyak - IceRockDev
DroidConTLV
 
Flutter State Management - Moti Bartov, Tikal
DroidConTLV
 
Reactive UI in android - Gil Goldzweig Goldbaum, 10bis
DroidConTLV
 
Fun with flutter animations - Divyanshu Bhargava, GoHighLevel
DroidConTLV
 
DroidconTLV 2019
DroidConTLV
 
Ok google, it's time to bot! - Hadar Franco, Albert + Stav Levi, Monday
DroidConTLV
 
Introduction to React Native - Lev Vidrak, Wix
DroidConTLV
 
Bang-Bang, you have been hacked - Yonatan Levin, KolGene
DroidConTLV
 
Educating your app – adding ML edge to your apps - Maoz Tamir
DroidConTLV
 

Recently uploaded (20)

PDF
"Database isolation: how we deal with hundreds of direct connections to the d...
Fwdays
 
PDF
Oh, the Possibilities - Balancing Innovation and Risk with Generative AI.pdf
Priyanka Aash
 
PDF
9-1-1 Addressing: End-to-End Automation Using FME
Safe Software
 
PDF
Raman Bhaumik - Passionate Tech Enthusiast
Raman Bhaumik
 
PDF
Quantum AI: Where Impossible Becomes Probable
Saikat Basu
 
PPTX
Wenn alles versagt - IBM Tape schützt, was zählt! Und besonders mit dem neust...
Josef Weingand
 
PPTX
"How to survive Black Friday: preparing e-commerce for a peak season", Yurii ...
Fwdays
 
PDF
AI Agents and FME: A How-to Guide on Generating Synthetic Metadata
Safe Software
 
PDF
Coordinated Disclosure for ML - What's Different and What's the Same.pdf
Priyanka Aash
 
PDF
GenAI Opportunities and Challenges - Where 370 Enterprises Are Focusing Now.pdf
Priyanka Aash
 
PPTX
UserCon Belgium: Honey, VMware increased my bill
stijn40
 
PDF
EIS-Webinar-Engineering-Retail-Infrastructure-06-16-2025.pdf
Earley Information Science
 
PDF
Cyber Defense Matrix Workshop - RSA Conference
Priyanka Aash
 
PDF
Enhance GitHub Copilot using MCP - Enterprise version.pdf
Nilesh Gule
 
PDF
Securing AI - There Is No Try, Only Do!.pdf
Priyanka Aash
 
PDF
The Growing Value and Application of FME & GenAI
Safe Software
 
PDF
PyCon SG 25 - Firecracker Made Easy with Python.pdf
Muhammad Yuga Nugraha
 
PDF
cnc-processing-centers-centateq-p-110-en.pdf
AmirStern2
 
PDF
Using the SQLExecutor for Data Quality Management: aka One man's love for the...
Safe Software
 
PDF
Quantum AI Discoveries: Fractal Patterns Consciousness and Cyclical Universes
Saikat Basu
 
"Database isolation: how we deal with hundreds of direct connections to the d...
Fwdays
 
Oh, the Possibilities - Balancing Innovation and Risk with Generative AI.pdf
Priyanka Aash
 
9-1-1 Addressing: End-to-End Automation Using FME
Safe Software
 
Raman Bhaumik - Passionate Tech Enthusiast
Raman Bhaumik
 
Quantum AI: Where Impossible Becomes Probable
Saikat Basu
 
Wenn alles versagt - IBM Tape schützt, was zählt! Und besonders mit dem neust...
Josef Weingand
 
"How to survive Black Friday: preparing e-commerce for a peak season", Yurii ...
Fwdays
 
AI Agents and FME: A How-to Guide on Generating Synthetic Metadata
Safe Software
 
Coordinated Disclosure for ML - What's Different and What's the Same.pdf
Priyanka Aash
 
GenAI Opportunities and Challenges - Where 370 Enterprises Are Focusing Now.pdf
Priyanka Aash
 
UserCon Belgium: Honey, VMware increased my bill
stijn40
 
EIS-Webinar-Engineering-Retail-Infrastructure-06-16-2025.pdf
Earley Information Science
 
Cyber Defense Matrix Workshop - RSA Conference
Priyanka Aash
 
Enhance GitHub Copilot using MCP - Enterprise version.pdf
Nilesh Gule
 
Securing AI - There Is No Try, Only Do!.pdf
Priyanka Aash
 
The Growing Value and Application of FME & GenAI
Safe Software
 
PyCon SG 25 - Firecracker Made Easy with Python.pdf
Muhammad Yuga Nugraha
 
cnc-processing-centers-centateq-p-110-en.pdf
AmirStern2
 
Using the SQLExecutor for Data Quality Management: aka One man's love for the...
Safe Software
 
Quantum AI Discoveries: Fractal Patterns Consciousness and Cyclical Universes
Saikat Basu
 

Tricks to Making a Realtime SurfaceView Actually Perform in Realtime - Maarten Edger

  • 1. Tricks to Making a Realtime SurfaceView Actually Perform in Realtime Maarten Edgar
  • 2. Hello, my name is …
  • 3. Hello, my name is … Maarten Edgar
  • 4. What we’ll cover SurfaceViews: • Why • When • What • How • Hard earned lessons
  • 5. Why use a SurfaceView? SurfaceView GL_SurfaceView TextureView SurfaceTexture View
  • 6. What is a SurfaceView? A View which gives you access to a Surface using .getHolder(), which is drawn on a seperate thread and is double/triple buffered behind the scenes. It cuts holes and displays underneath the window it is in.
  • 7. How to use it: • Setup • Threads vs Runnables and other control mechanisms • Loops • UI communication • Tips
  • 9. Setup: Activity and View @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // set flags as needed getWindow().setFormat(PixelFormat.RGBA_8888); this.setVolumeControlStream(AudioManager.STREAM_MUSIC); setContentView(R.layout.activity_game); // get handles to the View from XML, and its Thread mCSurfaceView = (MySurfaceView) findViewById(R.id.surfaceview); setSurfaceType(View.LAYER_TYPE_SOFTWARE); mSurfaceViewThread = mSurfaceView.getThread(); createInputObjectPool();
  • 10. Your SurfaceView class public class ChiBlastSurfaceView extends SurfaceView implements SurfaceHolder.Callback { public ChiBlastSurfaceView(Context context) { super(context); mSurfaceCreated = false; touchBool = true; // register our interest in hearing about changes to our surface SurfaceHolder holder = getHolder(); holder.addCallback(this); myHandler = new MyInnerHandler(this); // create thread only; it's started in surfaceCreated() thread = new ChiBlastSurfaceViewThread(holder, context, myHandler); setFocusable(true); // make sure we get key events }
  • 11. Your SurfaceView callbacks 1/3 SurfaceHolder.Callback: @Override public void surfaceCreated(SurfaceHolder holder) { // start the thread here so that we don't busy-wait in run() waiting for the surface to be created if (mSurfaceCreated == false) { createThread(holder); mSurfaceCreated = true; touchBool = true; } }
  • 12. Your SurfaceView callbacks 2/3 SurfaceHolder.Callback: @Override public void surfaceDestroyed(SurfaceHolder holder) { mSurfaceCreated = false; cleanupResource(); terminateThread(); }
  • 13. Your SurfaceView callbacks 3/3 SurfaceHolder.Callback: @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { thread.setSurfaceSize(width, height); }
  • 14. Setup: driving the SurfaceView Runnables, thread and loops, oh my!
  • 15. Setup: Thread public class ChiBlastSurfaceViewThread extends Thread { public ChiBlastSurfaceViewThread(SurfaceHolder surfaceHolder, Context context, Handler handler) { // get handles to some important objects mSurfaceHolder = surfaceHolder; mSurfaceHolder.setFormat(PixelFormat.RGBA_8888); mContext = context; res = context.getResources(); //any other initialization: ops = new BitmapFactory.Options(); ops.inPurgeable = true; ops.inDensity = 0; ops.inDither = false; ops.inScaled = false; ops.inPreferredConfig = Bitmap.Config.ARGB_8888; ops.inJustDecodeBounds = false; }
  • 16. @Override public void run() { while (mRun) { Canvas c = null; try { // update game state processInput(); //if (mMode == STATE_SCROLL_MAP) if (mMode != STATE_PAUSE) { updatePhysics(timeDiff); } c = mSurfaceHolder.lockCanvas(null); synchronized (mSurfaceHolder) { doDraw(c); } } finally { // do this in a finally so that if an exception is thrown // during the above, we don't leave the Surface in an // inconsistent state if (c != null) { mSurfaceHolder.unlockCanvasAndPost(c); } } } } Setup: Thread
  • 17. The Thread and your Activity What does this now mean for your Activity? or How do we make this fit into the Android Lifecycle?
  • 18. The Thread and your Activity @Override protected void onPause() { super.onPause(); // pause game when Activity pauses mSurfaceView.getThread().pause(); mSurfaceView.terminateThread(); System.gc(); }
  • 19. The Thread and your Activity @Override protected void onResume() { super.onResume(); if (mSurfaceView.mSurfaceCreated) { mSurfaceView.createThread(mSurfaceView.getHolder()); setSurfaceType(View.LAYER_TYPE_SOFTWARE); } mSurfaceView.SetTouch(true); }
  • 20. The Thread and your Activity @Override protected void onRestoreInstanceState(Bundle inState) { // just have the View's thread load its state from our Bundle if (mSurfaceView.mSurfaceCreated) { mSurfaceView.createThread(mSurfaceView.getHolder()); setSurfaceType(View.LAYER_TYPE_SOFTWARE); } mSurfaceViewThread.restoreState(inState); }
  • 21. The main loop • AFAFP • Fixed step
  • 22. @Override public void run() { long beginTime; // the time when the cycle begun long timeDiff; // the time it took for the cycle to execute int sleepTime; // ms to sleep (<0 if we're behind) int framesSkipped; // number of frames being skipped timeDiff = System.currentTimeMillis()+50; sleepTime = 0; while (mRun) { Canvas c = null; try { beginTime = System.currentTimeMillis(); framesSkipped = 0; // resetting the frames skipped // update game state processInput(); //if (mMode == STATE_SCROLL_MAP) if (mMode != STATE_PAUSE) { updatePhysics(timeDiff); } c = mSurfaceHolder.lockCanvas(null); synchronized (mSurfaceHolder) { doDraw(c); } The main loop 1/3
  • 23. The main loop 2/3 // calculate how long did the cycle take timeDiff = System.currentTimeMillis() - beginTime; // calculate sleep time sleepTime = (int)(FRAME_PERIOD - timeDiff); if (sleepTime > 0) { // if sleepTime > 0 we're OK try { // send the thread to sleep for a short period // very useful for battery saving Thread.sleep(sleepTime); } catch (InterruptedException e) {} } while (sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) { // we need to catch up // update without rendering processInput(); if (mMode != STATE_PAUSE) { updatePhysics(timeDiff); } // add frame period to check if in next frame sleepTime += FRAME_PERIOD; framesSkipped++; }
  • 24. The main loop 3/3 } finally { // do this in a finally so that if an exception is thrown // during the above, we don't leave the Surface in an // inconsistent state if (c != null) { mSurfaceHolder.unlockCanvasAndPost(c); } } } }
  • 26. static class MyInnerHandler extends Handler { private final WeakReference<ChiBlastSurfaceView> mView; MyInnerHandler(ChiBlastSurfaceView aView) { mView = new WeakReference<ChiBlastSurfaceView>(aView); } @Override public void handleMessage(Message m) { ChiBlastSurfaceView theView = mView.get(); theView.mStatusText.setText(m.getData().getString("text")); if (m.getData().getInt("viz") == View.VISIBLE) { theView.mStatusText.setVisibility(View.VISIBLE); //mStatusText.setAnimation(displayTextAnim); //mStatusText.startAnimation(displayTextAnim); } else { if (m.getData().getInt("viz") == View.INVISIBLE) { theView.mStatusText.setVisibility(View.INVISIBLE); theView.mStatusText.setAnimation(null); } else if (m.getData().getInt("viz") == View.GONE) { theView.mStatusText.setVisibility(View.GONE); } } theView.mStatusText.invalidate(); } }
  • 27. Setup: Cleanup public void terminateThread () { boolean retry = true; thread.setRunning(false); while (retry) { try { thread.join(); retry = false; } catch (InterruptedException e) { } //break; //THIS BREAKS IT ON PUSHING HOME } //thread = null; //THIS BREAKS IT ON PUSHING HOME }
  • 28. Tips • Input buffer • Object creation • Scaling • Drawing, bitmaps and other dirty things
  • 29. Tips: input buffer in SVActivity private void createInputObjectPool() { inputObjectPool = new ArrayBlockingQueue<InputObject>(INPUT_QUEUE_SIZE); for (int i = 0; i < INPUT_QUEUE_SIZE; i++) { inputObjectPool.add(new InputObject(inputObjectPool)); } }
  • 30. public class InputObject { public static final byte EVENT_TYPE_KEY = 1; public static final byte EVENT_TYPE_TOUCH = 2; public static final int ACTION_KEY_DOWN = 1; public static final int ACTION_KEY_UP = 2; public static final int ACTION_TOUCH_DOWN = MotionEvent.ACTION_DOWN; public static final int ACTION_TOUCH_POINTER_DOWN = MotionEvent.ACTION_POINTER_DOWN; //public static final int ACTION_TOUCH_POINTER_2_DOWN = MotionEvent.ACTION_POINTER_2_DOWN; public static final int ACTION_TOUCH_MOVE = MotionEvent.ACTION_MOVE; public static final int ACTION_TOUCH_UP = MotionEvent.ACTION_UP; public static final int ACTION_TOUCH_POINTER_UP = MotionEvent.ACTION_POINTER_UP; //public static final int ACTION_TOUCH_POINTER_2_UP = MotionEvent.ACTION_POINTER_2_UP; public ArrayBlockingQueue<InputObject> pool; public byte eventType; public long time; public int action; public int keyCode; public int x; public int y; public int x2; public int y2; public int pointerID; public int pointerIndex; public int pointerIndex2; InputObject 1/5
  • 31. InputObject 2/5 public InputObject(ArrayBlockingQueue<InputObject> pool) { this.pool = pool; } public void useEvent(KeyEvent event) { eventType = EVENT_TYPE_KEY; int a = event.getAction(); switch (a) { case KeyEvent.ACTION_DOWN: action = ACTION_KEY_DOWN; break; case KeyEvent.ACTION_UP: action = ACTION_KEY_UP; break; default: action = 0; } time = event.getEventTime(); keyCode = event.getKeyCode(); }
  • 32. public void useEvent(MotionEvent event) { eventType = EVENT_TYPE_TOUCH; int a = event.getAction(); switch (a) { case MotionEvent.ACTION_DOWN: action = ACTION_TOUCH_DOWN; break; case MotionEvent.ACTION_POINTER_DOWN: action = ACTION_TOUCH_POINTER_DOWN; break; case MotionEvent.ACTION_POINTER_2_DOWN: action = ACTION_TOUCH_POINTER_DOWN; break; case MotionEvent.ACTION_MOVE: action = ACTION_TOUCH_MOVE; break; case MotionEvent.ACTION_UP: action = ACTION_TOUCH_UP; break; case MotionEvent.ACTION_POINTER_UP: action = ACTION_TOUCH_POINTER_UP; break; case MotionEvent.ACTION_POINTER_2_UP: action = ACTION_TOUCH_POINTER_UP; break; default: action = -1; } InputObject 3/5
  • 33. InputObject 4/5 time = event.getEventTime(); pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT; pointerID = event.getPointerId(pointerIndex); x = (int) event.getX(pointerIndex); y = (int) event.getY(pointerIndex); if (event.getPointerCount() > 1) { pointerIndex2 = pointerIndex== 0 ? 1 : 0; x2 = (int)event.getX(pointerIndex2); y2 = (int)event.getY(pointerIndex2); } }
  • 34. InputObject 5/5 public void useEventHistory(MotionEvent event, int historyItem) { eventType = EVENT_TYPE_TOUCH; action = ACTION_TOUCH_MOVE; time = event.getHistoricalEventTime(historyItem); pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT; pointerID = event.getPointerId(pointerIndex); x = (int) event.getHistoricalX(pointerIndex, historyItem); y = (int) event.getHistoricalY(pointerIndex, historyItem); if (event.getPointerCount() > 1) { pointerIndex2 = pointerIndex== 0 ? 1 : 0; x2 = (int) event.getHistoricalX(pointerIndex2, historyItem); y2 = (int) event.getHistoricalY(pointerIndex2, historyItem); } } public void returnToPool() { pool.add(this); }
  • 35. @Override public boolean onTouchEvent(MotionEvent event) { try { // history first int hist = event.getHistorySize(); if (hist > 0) { // add from oldest to newest for (int i = 0; i < hist; i++) { //for (int i = hist-1; i > -1; i--) { InputObject input = inputObjectPool.take(); input.useEventHistory(event, i); mSurfaceViewThread.feedInput(input); } } // current last InputObject input = inputObjectPool.take(); input.useEvent(event); mSurfaceViewThread.feedInput(input); } catch (InterruptedException e) { } // don't allow more than 60 motion events per second try { Thread.sleep(16); } catch (InterruptedException e) { } return true; } Back to the activity:
  • 36. public void feedInput(InputObject input) { synchronized(inputQueueMutex) { try { inputQueue.put(input); } catch (InterruptedException e) { //Log.e(TAG, e.getMessage(), e); } } } private void processInput() { synchronized(inputQueueMutex) { ArrayBlockingQueue<InputObject> inputQueue = ChiBlastSurfaceView.inputQueue; while (!inputQueue.isEmpty()) { try { InputObject input = inputQueue.take(); if (input.eventType == InputObject.EVENT_TYPE_KEY) { //processKeyEvent(input); } else if (input.eventType == InputObject.EVENT_TYPE_TOUCH) { processMotionEvent(input); } input.returnToPool(); } catch (InterruptedException e) { //Log.e(TAG, e.getMessage(), e); } } } } And in the SurfaceView.Thread:
  • 38. Tips: object creation Just don’t do it.
  • 39. Tips: object creation Or do it up front. No matter how odd that sometimes may seem.
  • 40. Tips: scaling Two types of scaling: • Realtime whole view SV scaling only works from Android N • Fixed scaling (as done in Unreal Tournament 3)
  • 41. Tips: scaling @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { //thread.setSurfaceSize(width, height); if (mCanvasWidth != width) { int scaledWidth = (int)(width*0.75f); int scaledHeight = (int)(height*0.75f); if (scaledHeight != height) { yRatio = (float)(scaledHeight / (float)height); xRatio = (float)(scaledWidth / (float)width); } holder.setFixedSize(scaledWidth, scaledHeight); thread.setSurfaceSize(scaledWidth, scaledHeight); } }
  • 42. Tips: drawing, bitmaps and other dirty things
  • 43. Tips: drawing, bitmaps and other dirty things In SurfaceView.Thread doDraw(): canvas.drawBitmap(mBackgroundImage, null, fullscreenRect, mPicPaint);
  • 44. Q&A