XNA 3.1 to 4.0 requires constant redraw or will display a purple screen
Asked Answered
F

3

6

For menus in my game, I draw them once to the screen, then only redraw if they've been deemed dirty. This is handled through a boolean set to true whenever the user performs an action that should cause a redraw, and then the draw loop will check that value before drawing the menu. This logic worked perfectly in 3.1, but in 4.0 the menu will flicker (drawn for 1 frame) then show a purple screen until drawn again.

I've created a very simple test game in 4.0 to demonstrate the issue shown below. You will notice that the screen just looks purple. If you remove the line setting _isDirty to false, you will see the cornflower blue background.

public class Game1 : Microsoft.Xna.Framework.Game
{
    GraphicsDeviceManager graphics;
    bool _isDirty = true;

    public Game1()
    {
        graphics = new GraphicsDeviceManager(this);
    }
    protected override void Draw(GameTime gameTime)
    {
        if (_isDirty)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            _isDirty = false;
        }
        base.Draw(gameTime);
    }
}

How would I go about getting the behavior from XNA 3.1? I've seen several people mention PreserveContents, but that doesn't seem to have any effect in 4.0 unless I'm applying it incorrectly.

Fortnight answered 20/11, 2010 at 22:54 Comment(0)
D
13

Here is a rough overview of what gets called in an XNA game:

Update
BeginDraw
Draw
EndDraw

The default implementation of EndDraw ultimately calls GraphicsDevice.Present. With double-buffering turned on, this swaps the back and front buffers.

So what is happening is:

  • First time around: you're drawing your scene to the back buffer and then letting XNA swap it to the front.

  • Second time around: you're drawing nothing, leaving the surface filled with the purple colour that DirectX initialises these surfaces to, and swapping that to the front!

  • Subsequent times: you're drawing nothing, so you'll see the display flicker between these two surfaces.

There are several ways to suppress drawing in XNA. I go over them in this answer to a similar question. As in that answer, I recommend you override BeginDraw, like so:

protected override bool BeginDraw()
{
    if(_isDirty)
        return base.BeginDraw();
    else
        return false;
}

When BeginDraw returns false, Draw and EndDraw (and so Present) will not be called that frame. Nothing will draw and the front/back buffers won't swap.

Dasie answered 21/11, 2010 at 4:24 Comment(6)
Perfect! Thanks for taking the time to detail the inner workings of Draw, it's exactly what I needed.Fortnight
Note: As I discovered over here, XNA will actually actively mark a buffer as uninitialised (clears to dark purple) during Present, so the bit in my answer about flickering is wrong - although the solution is still the same. (Pentti's answer here has details on the mechanism, although I'd avoid turning it off with the sledgehammer that is reflection. Far more sensible to use the API properly, as per my answer.)Dasie
I am getting "The name '_isDirty' does not exist in the current context" error. Using XNA 4.0 and putting this method in the Game1 class. Any ideas?Jovial
Is there another way, potentially to copy the current on screen contents (from the last used buffer) to the new buffer about to be used each time?Jovial
_isDirty comes from the original question. It's a flag that indicates that whether the game's scene (the one that you manage) has changed and needs to be redrawn.Dasie
To get the same effect as copying the buffer, use a render target. Either use PreserveContents and reuse the target each frame. Or bounce the contents to a second render target on the next frame. (And it's best to ask new questions like that in an actual new question.)Dasie
A
2

I recently stumbled into this too; I do not think it's a problem with flipping buffers. That should be the same in 3.1 and 4.0. I looked at GraphicsDevice with ILSpy and found out this:

What has changed is that in 4.0, the current render target is first cleared in GraphicsDevice.Present() if a private flag is set. By default, this flag will be set initially (render target is clean). Drawing into the render target clears this flag (render target now dirty). After performing it's duty, GraphicsDevice.Present() again sets the flag (render target was "flushed out" to your window and is now again considered clean).

To get results from GraphicsDevice.Present(), you'll need to call Draw() and EndDraw() in strict sequence.

If you call EndDraw() without a prior call to Draw(), you will not get the contents of the render target (which gets cleared), only the purple screen instead.

Unfortunately, the flag is private and there's no clean way to get at it. You can use reflection to clear the flag like this, forcing the render target dirty without drawing anything into it:

graphicsDevice.GetType().GetField("lazyClearFlags", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(graphicsDevice, 0);

(graphicsDevice is your current GraphicsDevice instance)

Place the above line before your call to EndDraw(), and the behavior reverts to what it was in 3.1

Ariannaarianne answered 8/6, 2011 at 18:8 Comment(1)
Is there another way, potentially to copy the current on screen contents (from the last used buffer) to the new buffer about to be used each time?Jovial
S
0

In my particular case, I have a state machine that switches between states that are responsible for drawing. So when I transition between two game states, there is nothing to draw, and the screen flashes purple until the next game state is active.

What I did to solve it was simply, in the Update, if I have no state, call the SuppressDraw() method in the game object. This prevents drawing from taking place until the next Update takes place.

I suppose that there is nothing stopping you from always calling it in Update, except when you set an isDirty flag. But you would have to be prepared to handle other events/cases where the screen might get trashed.

Stagg answered 10/11, 2011 at 4:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.