XNA Back Buffers and the Status Bar
November 3, 2010
New York, N.Y.
Sometimes in the course of writing a book, my standards and practices evolve. I begin using a programming technique in later chapters that I didn't use in earlier chapters, and it's way too late in these later chapters to discuss the technique in detail. I know I should really go back and insert a discussion into an earlier chapter. But in some cases, the best chapter for the discussion has already gone through the editing pipeline. While that doesn't necessarily preclude further revisions, they certainly become more difficult, particularly if additional code or screenshots are involved.
Sometimes I just have to "let it go" and move on, and hope that someday I'll be able to discuss the issue in more detail in a blog entry exactly like this one.
Let's look at an XNA project for Windows Phone 7 called — for reasons you'll discover shortly — BackBufferDemo. You can download the entire BackBufferDemo project but it doesn't do anything spectacular and I'll be showing you all the relevant code.
The program defines some fields and uses those fields in the Draw override:
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
const string TEXT = "Hello?";
SpriteFont segoe14;
Vector2 textPosition;
Texture2D texture;
Vector2 texturePosition;
...
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Navy);
spriteBatch.Begin();
spriteBatch.Draw(texture, texturePosition, Color.White);
spriteBatch.DrawString(segoe14, TEXT, textPosition, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
All it's doing is drawing a text string and a Texture2D (a bitmap) at particular locations on the screen. The font is loaded, the Texture2D is created and initialized, and the textPosition and texturePosition fields are calculated in the standard LoadContent override:
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
Viewport viewport = this.GraphicsDevice.Viewport;
segoe14 = this.Content.Load<SpriteFont>("Segoe14");
Vector2 textSize = segoe14.MeasureString(TEXT);
textPosition =
new Vector2((int)((viewport.Width - textSize.X) / 2),
(int)((viewport.Height - textSize.Y) / 2));
int radius = (int)(textSize.X / 2 + 1);
texture = new Texture2D(this.GraphicsDevice, 2 * radius + 1,
2 * radius + 1);
Color[] pixels = new Color[texture.Width * texture.Height];
texture.GetData<Color>(pixels);
DrawCenteredCircle(texture, pixels, radius, Color.White);
texture.SetData<Color>(pixels);
texturePosition =
new Vector2((int)((viewport.Width - texture.Width) / 2),
(int)((viewport.Height - texture.Height) / 2));
}
Notice that I'm very careful about casting the textPosition and texturePosition constructor arguments to integers. I want the text and the bitmap aligned at pixel boundaries so XNA doesn't need to perform an excessive amount of anti-aliasing to display these objects on the screen.
The third statement from the bottom calls a method named DrawCenteredCircle that sets pixel bits to display a circle on the bitmap. This code also aligns the circle on pixel boundaries:
void DrawCenteredCircle(Texture2D texture, Color[] pixels,
int radius, Color clr)
{
Point center = new Point(texture.Width / 2, texture.Height / 2);
int halfPoint = (int)Math.Round(0.707 * radius);
for (int y = -halfPoint; y <= halfPoint; y++)
{
int x1 = (int)Math.Round(Math.Sqrt(radius * radius - Math.Pow(y, 2)));
int x2 = -x1;
SetPixel(texture, pixels, x1 + center.X, y + center.Y, clr);
SetPixel(texture, pixels, x2 + center.X, y + center.Y, clr);
// Since symmetric, just swap coordinates for other piece
SetPixel(texture, pixels, y + center.X, x1 + center.Y, clr);
SetPixel(texture, pixels, y + center.X, x2 + center.Y, clr);
}
}
void SetPixel(Texture2D texture, Color[] pixels, int x, int y, Color clr)
{
pixels[y * texture.Width + x] = clr;
}
All this pixel alignment suggests that when you run this program, you don't expect to start asking "Does this look a little fuzzy to you?" You might have a "Why the Face?" reaction when you take a screenshot and blow it up to show the actual pixels:
Even with all that careful pixel alignment, it's obvious that neither the text nor the texture (and hence, the circle) isn't pixel aligned and is displayed with some anti-aliasing. What went wrong?
By default, an XNA program draws on a back buffer that is 800 pixels wide and 480 pixels high, which is precisely the size of the Windows Phone 7 screen in landscape mode. (Shawn Hargreaves discussed this implementation in one and two blog entries on a busy day in July.)
However, this back buffer does not go straight out to the screen. By default, it's scaled to 91% of its size to make room for the Windows Phone 7 status bar, which is that area at the top of the portrait screen that displays the time, battery, and signal strength. This scaling causes objects that are pixel-aligned on the back buffer to lose that pixel alignment on the screen.
Is there a solution? Of course, there's a solution. The simplest solution is to use the Game1 constructor to set the IsFullScreen property of the graphics field to true:
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
TargetElapsedTime = TimeSpan.FromTicks(333333);
graphics.IsFullScreen = true;
}
This causes the status bar to disappear while your program is running, and for your program's back buffer to occupy the entire screen with no scaling. Take a new screenshot and close-up view to confirm that this is truly the case:
You'll notice that the image is a little larger as well as being sharper. This is the solution I use in the PhreeCell program in Chapter 23 of Programming Windows Phone 7. I designed the playing cards to be 88 pixels wide and 112 pixels tall, and I didn't want those cards to be scaled down at all.
The problem with this approach is that users often want that status bar around to keep them informed at least of the current time. You can alternatively retain the display of the status bar but reduce the size of the back buffer to make room. Again, you'll do this in the game's constructor:
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
TargetElapsedTime = TimeSpan.FromTicks(333333);
graphics.PreferredBackBufferWidth = 728;
graphics.PreferredBackBufferHeight = 480;
}
The status bar is 72 pixels wide in landscape mode, so the width of the back buffer is reduced by 72 pixels. The back buffer is than transferred to the screen with no scaling.
In portrait mode, the status bar is only 32 pixels high, so if you're designing your XNA program for portrait mode, you can accomodate the status bar by reducing the height of the back buffer by 32 pixels:
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
TargetElapsedTime = TimeSpan.FromTicks(333333);
graphics.PreferredBackBufferWidth = 480;
graphics.PreferredBackBufferHeight = 768;
}
This is the solution I use in the portrait-mode PhingerPaint and SpinPaint programs also in Chapter 23.
Programming Windows Phone 7
Free thousand-page ebook now available!