Undo and Redo: Yes you have to implement it

A professional developer on the gamedev.net forums once said “if you’ve implemented Undo and Redo in your app, then you’re in the top 1% of applicants for a tools developer position”.  That’s funny to me, because I have no idea how you could possibly have a useful tool without such a fundamental element of GUI application development.  I mean…people screw up.  It’s nice for users to know that their mistake can go away with a single press of “ctrl+z”.

When I first started working on the map editor for my game JumpSwitch, I put Undo/Redo functionality in there right away.  Like I said, it’s fundamental.  The problem was, I implemented it probably the most naive way possible.  See I handled saving and loading by having my Level class dump a whole bunch of data into a LevelData class, and then serializing that to XML.  So I thought “hey, I’ll just hang onto an array of these LevelData instances and then load them up when the user wants to Undo or Redo!”  Yeah I know, dumb.  Feel free to laugh.  Worked okay when my level had under a dozen entities hanging around, but anything after that meant you were waiting 3 or more seconds.  Not exactly what I’d call a fast and responsive UI.

So….what had to be done?  I had to stop being lazy and do Undo and Redo for real.  This we got a little more sophisticated:

-Make an abstract “EditAction” class, that represents a single action taken by the user to edit the level.  Give it virtual methods for “Undo” and “Redo”
-Make a few classes that inherit from EditAction and implement Undo and Redo for a specific action (moving an object, rotating an object, adding/removing an object, changing a Property, etc.)
-MapEditorLevel class has two stacks of EditAction’s: an Undo stack and a Redo stack.  When the user performs an action, it’s pushed onto the Undo stack and the Redo stack is cleared.  When the user does an Undo, the Undo stack is popped, Undo is called on that EditAction, and the action is pushed onto the Redo stack. When the user does a Redo, the Redo stack is popped, Redo is called, and the action is pushed onto the Undo stack.

/// <summary>
    /// Represents a single action taken by the user to edit the level,
    /// and that can be Undone/Redone.
    /// </summary>
    public abstract class EditAction
    {
        protected MapEditorLevel level;

        public EditAction(MapEditorLevel level)
        {
            this.level = level;
        }

        /// <summary>
        /// Undoes the action
        /// </summary>
        public abstract void Undo();

        /// <summary>
        /// Redoes the action
        /// </summary>
        public abstract void Redo();
    }

    /// <summary>
    /// Handles undo/redo for a rotation of a GameObject
    /// </summary>
    public class RotateAction : EditAction
    {
        GameObject[] targetObjects;
        Matrix rotation;
        Matrix inverseRotation;

        public RotateAction(MapEditorLevel level, GameObject[] targetObjects, Matrix rotation)
            : base(level)
        {
            this.targetObjects = targetObjects;
            this.rotation = rotation;
            inverseRotation = Matrix.Invert(rotation);
        }

        public override void Undo()
        {
            foreach (GameObject targetObject in targetObjects)
                targetObject.PropogateRotation(ref inverseRotation);
        }

        public override void Redo()
        {
            foreach (GameObject targetObject in targetObjects)
                targetObject.PropogateRotation(ref rotation);
        }
    }

/// <summary>
    /// Handles UndoRedo for properties
    /// </summary>
    public class PropertyEditAction : EditAction
    {
        PropertyInfo property;
        object[] oldValues;
        object newValue;
        object[] propertyOwners;

        public PropertyEditAction(MapEditorLevel level, object[] propertyOwners, object[] oldValues, string propertyName)
            : base(level)
        {
            this.oldValues = oldValues;
            this.propertyOwners = propertyOwners;

            Type ownerType = propertyOwners[0].GetType();

            // Find the property
            property = ownerType.GetProperty(propertyName);

            // Get the new value
            newValue = property.GetValue(propertyOwners[0], null);
        }

        public override void  Undo()
        {
            for (int i = 0; i < propertyOwners.Length; i++)
                property.SetValue(propertyOwners[i], oldValues[i], null);
        }

        public override void Redo()
        {
            for (int i = 0; i < propertyOwners.Length; i++)
                property.SetValue(propertyOwners[i], newValue, null);
        }
    }

    // You get the idea...

Alright, looks like a solid plan.  It worked out very nicely for movement and rotation.  Movement is just translation which is a vector, so if you negate it you get the Undo.  For the rotations, it’s a matrix so you just invert it.  Piece of cake.  Adding and removing…a little more tricky since I had to also keep track of an object’s parent so I’d know where to re-add the object.  But still not too bad.  Changing properties…not so nice.  The problem is that when the user changes the value of a property with the PropertyGrid, it fires a PropertyValueChanged event that lets you know what changed, and what the old value was.  Perfect, right?  Well yes…but only for single objects.  When you have multiple objects selected in the PropertyGrid you get “null” instead of the old value.  Fantastic.  Workaround  time…

-Handle the SelectedGridItemChanged event for the PropertyGrid
-Store the current value of the selected Property for all selected objects in an array
-Handle PropertyValueChanged
-Make an array of current Property values
-Send it all off to the MapEditorLevel so it can create an appropriate EditAction and push it onto the stack

Okay, so this works. Only problem left with that is that when you set the value of a Property through PropertyInfo.SetValue, the PropertyGrid doesn’t reflect the changes.  Still haven’t figured out a workaround for that one…

So in the end it was a little messy, but not that bad.  It only took me a single evening in fact.  Certainly nothing worthy of that frightening 99% statistic, IMO.   Besides…if you don’t implement it, it’s going to be first thing your designers and artists complain to you about anyway.  :P

LogLuv Encoding for HDR

Okay so this marks the third time I’ve posted this blog entry somewhere.  What can  I say…I like it!  I also think it’s something useful for just about anyone trying to do HDR on the 360 through XNA, and I’m hoping some people will stumble upon it.

Designing an effective and performant HDR implementation for my game’s engine was a step that was complicated a bit by a few of the quirks of running XNA on the Xbox 360.  As a quick refresher for those who aren’t experts on the subject, HDR is most commonly implemented by rendering the scene to a floating-point buffer and then performing a tone-mapping pass to bring the colors back into he visible range. Floating-point formats (like A16B16G16R16F, AKA HalfVector4) are used because their added precision and floating-point nature allows them to comfortbly store linear RGB values in ranges beyond the [0,1] typically used for shader output to the backbuffer, which is crucial as HDR requires having data with a wide dynamic range. They’re also convenient, as this it allows values to be stored in the same format they’re manipulated in the shaders. Newer GPU’s also support full texture filtering and alpha-blending with fp surfaces, which prevents the need for special-case handling of things like non-opaque geometry. However as with most things, what’s convient is not always the best option. During planning, I came up with the following list of pro’s and con’s for various types of HDR implementations:

Standard HDR, fp16 buffer
+Very easy to integrate (no special work needed for the shaders)
+Good precision
+Support for blending on SM3.0+ PC GPU’s
+Allows for HDR bloom effects
-Double the bandwidth and storage requirements of R8G8B8A8
-Weak support for multi-sampling on SM3.0 GPU’s (Nvidia NV40 and G70/G71 can’t do it)
-Hardware filtering not available on ATI SM2.0 and SM3.0 GPU’s
-No blending on the Xbox 360
-Requires double space in framebuffer on the 360, which increases the number of tiles needed

HDR with tone-mapping applied directly in the pixel shader (Valve-style)
+Doesn’t require output to an HDR format, no floating-point or encoding required
+Multi-sampling and blending is supported, even on old hardware
-Can’t do HDR bloom, since only an LDR image is available for post-processing
-Luminance can’t be calculated directly, need to use fancy techniques to estimate it
-Increases shader complexity and combinations

HDR using an encoded format
+Allows for a standard tone-mapping chain
+Allows for HDR bloom effects
+Most formats offer a very wide dynamic range
+Same bandwidth and storage as LDR rendering
+Certain formats allow for multi-sampling and/or linear filtering with reasonable quality
-Alpha-blending usually isn’t an option, since the alpha-channel is used by most formats
-Linear filtering and multisampling usually isn’t mathmatically correct, although often the results are “good enough”
-Additional shader math needed for format conversions
-Adds complexity to shaders

My early prototyping used a standard tone-mapping chain and I didn’t want to ditch that, nor did I want to move away from what I was comfortable with.  This pretty much eliminated the second option for me off the bat…although I was unlikely to choose it anyway due its other drawbacks (having nice HDR bloom was something I felt was an important part of the look I wanted for my game, and in my opinion Valve’s method doesn’t do a great job of determining average luminance).  When I tried out the first method I found that it worked as well as it always did on the PC (I’ve used it before), but on the 360 it was another story.  I’m not sure why exactly, but for some reason it simply does not like the HalfVector4 format.  Performance was terrible, I couldn’t blend, I got all kinds of strange rendering artifacts (entire lines of pixels missing), and I’d get bizarre exceptions if I enabled multisampling. Loads of fun, let me tell you.

This left me with option #3.  I wasn’t a fan of this approach initially, as my original design plan called for things to be simple and straightforward whenever possible.  I didn’t really want to have two versions of my material shaders to support encoding, nor did I want to integrate decoding into the other parts of the pipeline that needed it.  But unfortunately, I wasn’t really left with any other options after I found there were no plans to bring the support for the 360’s special fp10 backbuffer format to XNA (which would have conveniently solved my problems on the 360).  So, I started doing my research.  Naturally the first place I looked was to actual released commercial game.  Why?  Because usually when a technique is used in a shipped game, it means it’s gone though the paces and has been determined to actually be feasible and practical in game environment.  Which of course naturally led me to consider NAO32.

NAO32 is a format that gained some fame in the dev community when ex-Ninja Theory programmer Marco Salvi shared some details on the technique over on the beyond3D forums.  Used in the game Heavenly Sword, it allowed for multisampling to be used in conjuction with HDR on a platform (PS3) whose GPU didn’t support multisampling of floating-point surfaces (The RSX is heavily based on Nvidia G70).  In this technique, color is stored in the LogLuv format using a standard R8G8B8A8 surface.  Two components are used to store X and Y at 8-bit precision, and the other two are used to store the log of luminance at 16-bit precision.  Having 16 bits for luminance allows for a wide dynamic range to be stored in this format, and storing the log of the luminance allows for linear filtering in multisampling or texture sampling.  Since he first explained it other games have also used it, such as Naughty Dog’s Uncharted.  It’s likely that it’s been used in many other PS3 games, as well.

My actual shader implementation was helped along quite a bit by Christer Ericson’s blog post, which described how to derive optimized shader code for encoding RGB into the LogLuv format.  Using his code as a starting point, I came up with the following HLSL code for encoding and decoding:

// M matrix, for encoding
const static float3x3 M = float3x3(
    0.2209, 0.3390, 0.4184,
    0.1138, 0.6780, 0.7319,
    0.0102, 0.1130, 0.2969);

// Inverse M matrix, for decoding
const static float3x3 InverseM = float3x3(
    6.0013,    -2.700,    -1.7995,
    -1.332,    3.1029,    -5.7720,
    .3007,    -1.088,    5.6268);    

float4 LogLuvEncode(in float3 vRGB)
{
    float4 vResult;
    float3 Xp_Y_XYZp = mul(vRGB, M);
    Xp_Y_XYZp = max(Xp_Y_XYZp, float3(1e-6, 1e-6, 1e-6));
    vResult.xy = Xp_Y_XYZp.xy / Xp_Y_XYZp.z;
    float Le = 2 * log2(Xp_Y_XYZp.y) + 127;
    vResult.w = frac(Le);
    vResult.z = (Le - (floor(vResult.w*255.0f))/255.0f)/255.0f;
    return vResult;
}

float3 LogLuvDecode(in float4 vLogLuv)
{
    float Le = vLogLuv.z * 255 + vLogLuv.w;
    float3 Xp_Y_XYZp;
    Xp_Y_XYZp.y = exp2((Le - 127) / 2);
    Xp_Y_XYZp.z = Xp_Y_XYZp.y / vLogLuv.y;
    Xp_Y_XYZp.x = vLogLuv.x * Xp_Y_XYZp.z;
    float3 vRGB = mul(Xp_Y_XYZp, InverseM);
    return max(vRGB, 0);
}

Once I had this implemented and worked through a few small glitches;, results were much improved in the 360 version of my game. Performance was much much better, I could multi-sample again, and the results looked great. So while things didn’t exactly work out in an ideal way, I’m pleased enough with the results.

If you’re interested in this, be sure to check out my sample

Jamming to the oldies

Cleared for takeoff

Look out interwebs, I have a new blog to call my own!

*insert trumpet fanfare here*

Here’s what I plan on doing with it:
-integrating some of the stuff I’ve been posting on gamedev.net and xnainfo.com in one place
-put up some samples/tutorials/tools/code I’ve come up with
-yammer on about JumpSwitch, the Xbox 360 game I’m working on
-set up a page to show some sweet pics and vids of JumpSwitch, so everyone can see how awesome it is
-post links to cool and helpful stuff I find
-try to somewhat serious (not likely to happen)

Exciting, right?