Android Game Development Blog

A blog on Android game development by Douglas Tanner

July 2011 Archives

Android ATITC (ATC) texture encoding and performance



As I discussed before in my blog post about PVRTC texture encoding and performance, every Android OpenGL ES 2.0 phone supports at least two texture formats: ETC1 and one of the three proprietary formats (PVRTC, ATITC and S3TC). I just got myself a cheap second-hand Nexus One (with an Adreno 200 graphics chip), so I wanted to test out the ATI-specific texture format and see what the trade-offs are vs. ETC1 texture performance.


    Get the Adreno SDK
    
Get adreno-sdk-2-2-00.zip from the Qualcomm developer website. Free registration is required (WARNING: They send you your own password in plaintext). Once installed it will be placed in C:\Program Files (x86)\Qualcomm\AdrenoSDK\.


    Encoding ATC textures

Unlike with PVRTC, the SDK does not contain a stand-alone executable, it contains a library that has to be directly linked into your packing program. The documentation explaining in detail how to do that is located in AdrenoSDK\Tools\Texture Converter\docs. After including the appropriate header file (TextureConverter.h) and linking with the library (TextureConverter.lib), your code will look something like the following (this is directly based on AdrenoSDK\Tools\Texture Converter\samples\Convert.c).

The first step is to initialize the source image (in my case a .tga file):



Then we setup the destination image (either ATITC_RGB or ATITC_RGBA):



Finally we do the actual conversion from .tga to ATC:




    Loading ATC textures

Nothing special needs to be done to load ATC textures, just make a regular call to glCompressedTexImage2D with the right constant (substitute GL_ATC_RGBA_EXPLICIT_ALPHA_AMD for ATC alpha textures).




    ATC texture performance

For my test case setup, read blog post on ETC1 texture performance. The only difference is that these results are from a Nexus One which has an Adreno 200 instead of the PowerVR SGX on my Milestone. These are the results I got:



These results are very interesting, they show that the graphics architecture of the Nexus One is vastly different from that of the Milestone. There is absolutely no performance hit to using uncompressed textures, compared to a setup that was giving me a 13-33 fps difference between compressed and uncompressed on my Milestone. It's definitely worth using compressed textures for the file size difference, but unless your game has some crazy-complex textures you'll probably be fine with the more compatible ETC1 format.

However, one thing I will mention is that the ATC texture format is subjectively 'better looking' than the ETC1 format (to my eyes anyway). This is possibly due to better hardware decoding/interpolation support. When I manage to get my hands on a phone that supports S3TC I think I'll do an in-depth visual quality comparison between the four formats.

Next time I will show you how to find free 2D game art.

POSIX threads on Android



    Threading setup for an Android OpenGL Game
    
I'm not going to go into too much detail about how to setup your thread organization other than to say that for performance reasons you need an update thread separate from your render thread, because eglSwapBuffers() is a blocking call on the render thread. For an in-depth discussion of how to structure things check out Rendering With Two Threads.

My basic plan is to have an update thread running at 60 fps (to minimize input lag) and my render thread running at 30 fps. My render thread will grab the latest snapshot from the update thread when it begins and then use this information to build the graphics primitives and upload them to the GPU. I may also have a separate thread to handle audio updates and streaming, but I'll deal with that later when I've actually implemented audio.


    Creating POSIX threads



You'll notice right away that I'm doing something pretty weird; I'm creating/destroying my threads in OnResume/OnPause. If you look at the application lifecycle graph you'd assume the best place to create and destroy a thread would be in onCreate/onDestroy. But, there is no guarantee that either onStop or onDestroy is ever called! There are some sneaky little lines on the left side of the graph that lead directly to "Process is killed". This is really atrocious design, why even have states that can randomly never be called? What code would you possibly want to put in them? I'm sure there's a good reason for the way this is set up, but for now it eludes me... So anyway, I decided to create/kill my update thread in onResume/onPause because I am guaranteed that these states will be called every time my application is run. The initial state of the threads is set up in onCreate.


    Mutual exclusion (critical section)




    Thread communication (semaphores)




    Thread local storage


 

Next time I will explain ATITC (ATC) texture encoding and performance.

Input - Touch Screen, Hardware Keyboard, and Back/Menu/Home/Search



What is a game without interaction? It's time to start accepting input!

    Touch Screen
   
This is the important one! Because the trackball and keyboard are not supported by every phone, your game needs to be playable and navigable using only the touchscreen.


   



    Intercept key-presses (back/menu/search)
   
Every Android phone has 4 physical buttons on the front: back, menu, home, and search (although not necessarily in that order). For a game it's a good idea to override the back button and insert an "Are you sure you want to quit?" dialog box. And obviously the menu button should be there to expose more advanced options to your power users (basic options can be put directly in the front-screen menu). For now I'm just using the physical buttons for debug input, I'll probably cover their actual in-game use in more detail later when I actually have a menu and proper back handling.




    Override home button?

You can't! Unless you're writing a full home replacement (like AWD launcher for example). It's very important for a user to have a 'safety button' that can always return him to his home screen, otherwise it would be possible for a malicious application to completely take over a user's phone.

    Hardware Keyboard

   
The first thing you will want to do for phones with hardware keyboards is to stop your application from restarting when the user opens the hardware keyboard. For regular applications this is important to properly re-set the visual formatting, but for a game that has a fixed landscape orientation you definitely don't want this to occur.



Next time I'll explain how to use POSIX threads on Android.


    Delta Time
    
To profile the framerate of your game, you'll have to query the high resolution timer supported by your hardware. On Android you can use the clock_gettime call to get the current time:



This call returns the number of hardware ticks since the start of your application. By itself this number is useless of course, you need to compare it to the time from the previous frame:



Because a timespec is a 64-bit type, if you just compared the number of nanoseconds from frame to frame, tv_nsec would eventually overflow and you one frame you'd have a large negative time delta.


    Full-screen and forced landscape orientation

   
Use the following code to fully full-screen your game, and also to force landscape orientation.




    Prevent application restart on orientation change

Warning: The following code doesn't seem to play well with OpenGL ES 2.0 and the GLSurfaceView (API version 2.2). On my Nexus One the OpenGL surface does not get re-created and it causes the renderer to blit garbage onto the screen.

Add the following android:configChanges to your AndroidManifest.xml



Override onConfigurationChanged in your Activity to prevent application restart




    A random error message

After having my workflow working fairly well for over a month, I randomly started receiving the following error message: "The Program can't start because AdbWinApi.dll is missing from your computer"

I'm just reproducing the solution here in case anyone ever Googles around for the same problem. My solution was to add the following to the end of my "Path" environment variable:



Next time I will show you how to use the various types of input available to Android devices.