Buffering Solutions on BlackBerry, Part 2


In Part 1 of Buffering Solutions on BlackBerry, I wrote about what buffering means, why we need to use it (especially on BlackBerry) and gave the first example of a simple buffering solution using a Full Screen Image. Here, I will expand upon the basic buffering solution presented previously and prepare you for the next part of this series of articles: Segmented Buffers.

If you haven’t read the first article yet, I highly recommend you do so first as this article assumes you know the background information from the first one. As with the first article, I have included the slides from my presentation (link below). There’s animation in the slides that really helps clarify some of the concepts I discuss here so I highly recommend you run them alongside reading this article.

BlackBerry Game Development Challenges – Section 2

And now, let’s resume our buffering discussion…One of the limitations not specifically mentioned in the previous discussion on using a full screen image is that allocating memory for a full image can be troublesome on medium and low-end devices. Think of it this way, a screen resolution of 240 x 240 equates to 57,600 pixels. If the image is stored using 32 bits per pixel (ARGB format) then a full screen image is at minimum just over 230Kb or run-time memory (plus various other meta data created in the data structure around the image data). For today’s devices, 240×240 is a small screen resolution — though at one time it was on the high-end of phone screen resolutions. For a device like the BlackBerry BOLD (480×360 screen), you’re allocating almost 700Kb of memory for just the raw image data in your buffer.

Fortunately, BlackBerries have always have more memory available to them relative to their processing power than other smartphones. This allows you to leverage the memory available to make up for the graphics rendering performance issues. On devices where the processor is disproportionately fast relative to the memory available on the phone, you may not need to buffer as it may not gain you as much.

A basic full screen image is only the most basic of buffering techniques. It’s the “Hello World” of buffering. I’ve discussed how to use the full screen image as a buffer and some of the advantages and disadvantages of it, but there are ways to mitigate the issues you can run into will using a full screen buffer.

Full Screen Image: Working and Live buffers

One buffer makes great improvements in performance, but what about using two? On most smartphones, there is enough runtime memory to allocate more than one full screen image (alongside all your other game images and data). Using two full screen images can help to mitigate some of the issues we encountered with a single full screen image.

We’ll call the two buffers the ‘working’ buffer and the ‘live’ buffer. The working buffer is never painted directly to screen. Instead, the live buffer is always painted to the screen and updated by doing a direct copy of the working buffer onto the live buffer. All rendering of static content is done on the working buffer before it is copied to the live buffer. Create the buffers as before with the single full screen image but use them like the code snippet below.

private void repaintBuffer() {
  synchronized (workingBuffer) {
    // ... draw all the static content onto the workingBuffer ...
    synchronized (liveBuffer) {
      Graphics g = liveBuffer.getGraphics();
      g.drawBitmap(... workingBuffer ...);
    }
    bufferDirty = false;
  }
}

public void paint(Graphics g) {
  synchronized (liveBuffer) {
    g.drawBitmap(... liveBuffer ...);
  }
  // ... draw all dynamic content ...
}

You can see the code is very similar to the single full screen image buffer but with a couple of critical differences. repaintBuffer DOES NOT synchronize on the same lock as the paint method while rendering the static content onto the workingBuffer. If you recall from the previous discussion, with a single buffer, there was a synchronization block that encompassed all the time it took to re-render the static content onto the buffer — which could be relatively long as you gain more benefits in buffering for doing more work while rendering your static content.

Instead, all the static content is rendered on the workingBuffer and then the workingBuffer is quickly copied to the liveBuffer. In your setBufferDirty method, make sure you are synchronizing on the same lock as the lock you’re using when modifying the bufferDirty variable in this method.

In the paint method, the liveBuffer is always directly copied to the screen with no checks for dirty/clean state. There is still synchronization in the paint method but the maximum time for this synchronization block is “time to acquire lock” + “time to copy workingBuffer to liveBuffer” + “time to copy liveBuffer to screen”. A full-screen image copy is a relatively fast operation compared to drawing many images to a full screen image. The overhead in multiple draw calls is relatively high, so minimizing the number of calls, regardless of the size of the drawable content, is beneficial.

Note: a full image copy (working -> live or live -> screen) is a relatively fast operation even though the images are large because the overhead in multiple graphics method calls when rendering each portion of the static content is very high.

This solution has many advantages:

  1. the maximum paint time is reduced. This allows for a higher frames-per-second (FPS) since the FPS is a direct calculation of the number of times the screen can be re-painted in one second.
  2. the variance between the minimum and maximum times needed to paint a frame is reduced. All other things being equal, the variance is +/- “time to acquire lock” + “time to copy workingBuffer to liveBuffer”. Low variance in the time to paint frames results in smoother frame rates as the frame rate stays more consistent over time eliminating (or mitigating) the appearance of choppiness in your game. The “time to acquire lock” is the only call with variable timing so it also allows you to profile your paint calls and calculate your expected FPS within a small window (as the lock acquisition time is going to be fast unless the liveBuffer it in the process of actually being updated).

You still may run into issues with a synchronized block on the event thread but using the working/live buffer solution should mitigate the problems compared to a single full screen image as the lock is held for a much shorter duration. This type of buffering does still have a risk of dirty paints as the model could be updating the workingBuffer while painting the liveBuffer and then you’ll have a time delta between seeing the effects of the changes to the workingBuffer. You could explicitly invoke a re-paint at the end of each repaintBuffer call but you can end up with other frame rate issues doing that depending on how often and when the buffer is updated.

Larger-Than-Full Screen Image

The next simple variation on the full screen image is to use a larger-than-full screen image. Basically you allocate a buffer that is the size of your entire background, render your entire background and then show a screen-sized portion of the buffer on each paint.

BlackBerry Game Development Challenges - Slide9

Slide 9

The code is the same basic code as with the full screen image except instead of the size of the image being the screen width and height; it’s the entire size of your background.

When using this method, we need to introduce the concept of a viewport. Think of the viewport as a window onto your buffer. Through this window, you can only see a portion of your buffer at a time. As you move the viewport around relative to the buffer, you can see different areas of the background come in and out of view.

The slide shows an example where this method is particularly useful. The example shows a large area from Call of Duty 3 by HandsOn Mobile (BlackBerry version by Magmic Games). At any time, the player can only see what is in the viewport. As they move their character around the board, the viewport moves as well to attempt to keep the player centered (as much as possible) within the viewport. Even though the background is very complex and is composed of a lot of tiles, painting it to screen is very fast because of the buffer.

To render the correct viewport, you will need to add some code to the full screen image code to keep track of the x/y location of the viewport relative to the buffer, and possibly the width and height of the viewport, if you are not using the entire screen as the viewport (for example, you may cut off portions of the screen for HUD or status messages that permanently obscure the background). Then, when drawing the buffer, offset the image by the viewport location (see the drawBitmap method definition in the BlackBerry API reference) and you’ll be good to go.

The great advantage of this method comes when you do not have to update the buffer frequently — or at all — after it is initially rendered. Since the buffer is very large, re-rendering the entire buffer is not generally feasible during gameplay. It is best left to level loading or specific waiting times in the game to re-render the background. You can re-paint sections of the buffer as needed, just not the whole buffer (in most cases).

Obviously allocating a huge image like this is not technically possible on all platforms. Many will not have the runtime memory available to create the image, and some platforms (like BlackBerry) have limitations on the maximum size of an image that can be created — which is not directly tied to the available memory on the device.

If you are so lucky to be on a device with enough memory to allocate two huge images, you could combine this method with the working/live setup described above. However, that will take a lot of your runtime memory and you may be better off finding ways to design the game such that the background can be updated on the buffer faster or less often if that’s part of the reason you need to use two buffers.

More to come…

Large buffers solve a lot of issues if you are able to allocate them and manage them on the device you’re targeting. But how do we deal with devices that cannot allocate large images? The next article in this series will talk about the final buffering types I will cover (Segmented Buffers) and, later, I’ll tie everything togeather in a case study on Worms: A Space Oddity.

Note: when I originally started writing these articles I did not expect to think of so much useful information that was not covered in the DEVCON presentation. Instead of writing a book, I have left out some details that I expect a competent developer should be able to figure out on their own after having read the articles. Please feel free to contact me with questions and I’ll answer them or clarify anything that’s not clear.

(Thanks to Simon Dale for working on the original presentation with me as well as reviewing my work here to make sure I didn’t miss anything!)

Advertisements
This entry was posted in Game Development and tagged , , , , , , , , , , , . Bookmark the permalink.

One Response to Buffering Solutions on BlackBerry, Part 2

  1. Pingback: Buffering Solutions on BlackBerry, Part 3 | Bacon on the Go

Comments are closed.