Geometer»Blog
Andrew Reece
Apologies for the long delay since my previous post - I quit my job, and I've been focusing more on things that will pay the bills...

I'm starting to think about UI a bit more, for which colour is often very important. Something that's fairly easy to do, but often overlooked, is to make sure that colour choices are suitable for colourblind users as well as people with normal colour vision (if that's even done at all). Since about 8% of people are colourblind, it's a significant chunk of your user base to alienate if you get it badly wrong.

I've put together some utilities for evaluating colours and simulating colourblindness for each missing cone. This is as a C header library and some glsl matrices.

Check it out on Github.
Andrew Reece
Say goodbye to jaggies, it's smooth sailing (and rasterization) from here on out!

I finally got round to getting my rendering to work on OpenGL.
This lets me do things like animation smears really easily.

I'm not sure what the compatibility is like for graphics cards, but if it doesn't work you can hit F6 to toggle software rendering.

Download from the Github repo

Andrew Reece
I don't think I'm doing anything revolutionary with my undo system, but hopefully making the reasoning behind it very explicit will elucidate any missing steps in the reader's or my thinking.

A large part of this is trying to define 'units of interaction' for the user and then matching that to what can be robustly determined and stored in the software.

I describe this here as a roughly linear sequence for the sake of clarity. In reality the process was more iterative, and broadly follows Casey Muratori's Semantic Compression approach.

Throughout the article, I use 'units of interaction', 'units of action', 'task units' etc interchangeably.


Contents
  • Contents
  • Intro
  • Naive undo states
    • Why this is not good enough
  • Actions
  • User Actions
    • Theory
      • Action Cycle and Principles
      • Error types
      • Action hierarchy
    • Example action set
    • User-centred goals
      • Functionality goal - cost saving
      • Consistency
      • Mapping
    • Error prevention
  • Technical considerations
    • Objectives
    • Triggers and choice of representation level
  • Representation
    • Undo
    • Redo
    • Collection data structure
    • Resultant structs
    • Mismatch between user actions and software actions
    • Function Layout
      • Action Functions
      • Undo/Redo functions
    • Required restructuring
  • Future possibilities for actions
  • Conclusion
  • Further reading

Intro

If you've used any editing software, for text, images or something else, you've almost certainly had to use the undo function. Its purpose is fairly clear. Simply put, people commit errors all the time, and so it's necessary to have a way to fix them.

On top of this, having a robust undo system allows users to have enough confidence in your software to rapidly experiment, safe in the knowledge that they can just undo if it turns out wrong.


Naive undo states

My first approach was to have a fixed array containing snapshots of the entire state at successive points in time. Once the array was full, the new states would wrap to the beginning. Each state had a separate memory arena assigned. Undos and redos would just change the index of state looked at, and base all of the interactions, rendering etc on that.

The state would be saved at various points in the interaction processing that seemed appropriate. Because the entire state was saved this could be a bit sloppy, because all the information required was in that one state.


Why this is not good enough

The naive approach wasn't going to work long-term, in large part because I wanted to have the option of indefinitely long undo history. It was doomed for replacement because it:
  • grows exponentially - this is the main reason. Every change also stores all the previous changes.
  • is awkward to access in code (State->Draw[State->iCurrentUndoLevel].Points[State->iActivePoint]),
    which would be further complicated by the addition of layers.
  • is more difficult to implement than the improvements described later:
    • partly due to lessons learnt by doing this first.
    • it has general fiddly bits in terms of:
      • keeping track of the start and end of the circular buffer.
      • special-casing before the array was full.
      • memory management - making sure that the next state was large enough to contain any changes from the current state.

Actions

The change in thinking was that rather than storing absolute states, I should store the difference between them. The things that cause the change in state are the actions by the user. Broadly speaking, each time the user does an action, I should record what it is, then be able to undo/redo it at will.

But what counts as a user action?


User Actions

Theory

It's worth taking a slight detour into UX/ergonomics, as we're stepping into their wheelhouse. Not all of this is applied directly in this article, but I think that it provides background/context and informs intuitions in a useful way.

That said, it's not necessary to understand the rest of the article, so feel free to skip it if you want a shorter read.


Action Cycle and Principles

Don Norman's 7 step model of action/action cycle provides a breakdown of interaction from the user's perspective, which allows us to better target corrections.

Goal formation stage
  1. Goal formation.
Execution stage
  1. Translation of goals into a set of unordered tasks required to achieve goals.
  2. Sequencing the tasks to create the action sequence.
  3. Executing the action sequence.
Evaluation stage
  1. Perceiving the results after having executed the action sequence.
  2. Interpreting the actual outcomes based on the expected outcomes.
  3. Comparing what happened with what the user wished to happen.
These lead to 4 principles of good (UI) design: 1. Visible system state 2. Consistent conceptual model 3. 'Intuitive' mappings between action and consequence 4. Continuous feedback of past and potential actions

-- Don Norman (Action Cycle - Wikipedia)


Error types

Undoing is typically for the purpose of correcting (potential) errors. I won't go too far into this, but it's useful to have some vocabulary and context for thinking about errors. Different error types may require different prevention and recovery methods
  • Mistakes - conscious decision to do something that turns out to be wrong i.e. forming the wrong goal.
  • Slips - unconscious automatic behaviour resulting in unintended consequences.
    • Capture error - a common script overriding the intended action (e.g. driving to work instead of to the shops).
    • Description error - do the right thing with the wrong object.
    • Data-driven error - information from the world intruding into an action.
    • Associative activation error - Freudian slips (roughly).
    • Loss-of-activation error - forgetting.
    • Mode error - do the otherwise right thing in a mode that interprets it differently.
Errors can also be categorised as:
  • Commission - did something incorrectly.
  • Omission - didn't do something that should have been done.
[Don Norman (Design of Everyday Things)]


Action hierarchy

A common start point for human factors/ergonomics analysis is a Hierarchical Task Analysis. This involves recursively breaking down the component tasks required to achieve a larger task/goal based on observations of people performing the task. The end result is similar to a hierarchically-organized graph of functions that call a number of other functions, which each call a number of other functions... The main takeaway is that there are multiple possible levels of detail that could be appropriate to consider 'an action', as you'll see in the upcoming example.


Example action set

It's often easiest to design a user interface if you can reference a concrete example of something a user might do. This is the UI/UX equivalent of

Casey Muratori

Always write the usage code first (broadly talking about API design).

I'll be running with this example for the rest of the article. It's not comprehensive, but other actions follow a similar line of reasoning. As already mentioned, tasks can be broken down in granularity into a hierarchy. Here's a slice of an example goal/task hierarchy for a drawing in Geometer:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
0.  Draw a cathedral in profile view                 /* Entire drawing */
    ...
    4.  Draw the east wing                           /* Section of drawing */
        ...
        4.6.  Draw an arch                           /* Architectural component */
            4.6.1.  Draw the left-hand arc           /* Discrete shape */
                4.6.1.1.  Add a focus point          /* Shape component */
                    4.6.1.1.1.  Move the cursor to the bottom-right point  /* Direct input */
                        4.6.1.1.1.1.  Grip mouse     /* Joint movement */
                        4.6.1.1.1.2.  Retract shoulder girdle
                        4.6.1.1.1.3.  Extend, laterally rotate and adduct shoulder
                        4.6.1.1.1.4.  Allow passive elbow flexion
                        4.6.1.1.1.5.  Ulnar deviate wrist

                    4.6.1.1.2.  Check that the intended point is snapped to
                    4.6.1.1.3.  Click and release the left mouse button at that point

                4.6.1.2.  Start the arc
                    4.6.1.2.1.  Move the cursor to the bottom-left point
                    4.6.1.2.2.  Check that the intended point is snapped to
                    4.6.1.2.3.  Click and hold the left mouse button at that point

                4.6.1.3.  Finish the arc
                    4.6.1.3.1.  Move the cursor to the top-centre point
                    4.6.1.3.2.  Check that the intended point is snapped to
                    4.6.1.3.3.  Release the left mouse button at that point

            4.6.2.  Draw the right-hand arc
                ...

Note that:
  • If asked "what are you doing?", you could accurately answer at any level;
    there are multiple viable levels of analysis - you can be as detailed or abstract as you like.
  • The action cycle occurs at each level from direct input up
  • Errors could occur at any level
In magical UI land, the unit of interaction should be at whatever level in the hierarchy the user thinks it is at the time. I'm not sure to what extent you can implement variable hierarchical levels of undo, let alone communicate it in a way that makes sense to the user. As such, we'll start with a single level-of-detail action unit that is good enough for most purposes. This may be extensible to get closer to magical UI land, as hinted at in Future possibilities for actions.

It's also worth noting that other interface features and external programs can fill the role of dealing with high-level units of interaction, particularly saves, backups and version control.


User-centred goals

As already mentioned, the primary purpose of undo is to correct mistakes. This is not the only purpose, however - it's also useful for: - reminding users of the sequence of actions they've just taken as a prompt for what is needed next. - repeatedly returning to a common point after temporarily exploring multiple elaborations from it.

There are a few axes on which to evaluate an undo/redo system.
  • Functionality goal - cost-saving
  • Consistency goals:
    • Self-consistency
    • Consistency with other programs (less important, but better if possible)
  • Mapping

Functionality goal - cost saving

An action should represent some notable cost to the user: in precision or time or effort.

If it's just as easy to actually redo the action as to use the redo input, there's not that much point in having the control. Too small action sizes are similarly irritating for undo as well: consider a text editor that removes one letter per undo. With this in mind, we'd like to define task units higher up the hierarchy to save as much effort as possible per undo/redo. We don't, however, want them to be too high up, otherwise we'll unavoidably skip back past too many valid actions, which will then require manual redoing.

One way to think about this is, "if the user realises they made an error here, how many times will they have to undo, and how much will they have to manually redo in order to return to the state immediately before the error".

The aim here is to minimize estimated total effort in error recovery

An empirical question that would be useful to know the answer to is "At what level do most errors occur?". Without an answer, it seems better to lean too fine-grained than too coarse-grained - it's much harder to increase resolution than decrease it.


Consistency

Self-consistency:
  • Undo and redo should have the same unit of action.
  • Units of action should be the same size.
External consistency: - I'll be using Ctrl+Z for undo, Ctrl+Y/Ctrl+Sh+Z for redo, as with other programs. - Units of interaction will represent a similar time investment.
  • The user should know in advance what will disappear when they undo.
    • This minimizes time spent in the evaluation stage of the action cycle.
    • Actions should be demarcated in some way, e.g. with distinct start and end points.

Mapping

Quality of mapping can be evaluated of in terms of how well the application matches what the user expects from the first time they undo. Consistency plays a large part of expectations in future interactions How well does it fit their initial mental model?

In order to make a clear mapping, actions should correspond fairly directly to a pattern of inputs rather than some downstream changes.


Error prevention

I won't go into this too much here as it's not really in the intended scope of this article, but helping to minimise stupid errors is important. Errors from bad design:
  • waste your user's time
  • make the user frustrated with themself and your product
  • undeservedly diminish the user's sense of competence/mastery
  • reduce the chance of continued use of your product
Note the qualifier 'from bad design'. Some increased likelihood of error occurs as a side-effect of flexibility and other positive qualities.

Mode errors are among the most insidious. Vim is a classic example for this: users try to type but the editor seems to go haywire because it is not in 'input' mode. It is not immediately apparent which mode vanilla Vim is in, which worsens the problem.

I try to avoid this in Geometer by making changes in mode obvious, discrete and brief. I still have more work to do to improve this.


Technical considerations

I have not yet talked about computer actions or representation. Without a good idea of what we're aiming for, it's easy to prematurely limit the technical representation to the first/easiest thing we think of. We should be aware of technical limitations, but we want a user-focused goal to shoot for, even if we fall short.


Objectives

Nothing novel here, but some things to keep in mind:
  • Quick to read/write
  • Not memory intensive
  • Deterministic in terms of:
    • Interpreting a pattern of input as 'an action'
    • Recreating that action
  • Able to traverse forwards and backwards
  • Changes are self- and externally-consistent, e.g.
    • Multiple undos followed by an equal number of redos returns to the same place
    • A redo makes the same state changes that doing the action manually would (excluding any changes to the action array)

Triggers and choice of representation level

Working up from the lowest level (which incidentally tends to be how humans search for the cause of errors), let's consider how well the task levels fit the objective of determinism:
  • Joint movement - no way to directly interpret
  • Direct input - difficult to tell how much movement to classify as an action; state doesn't necessarily change
  • Shape component - needed to reconstruct shapes, a single function is executed for each
  • Discrete shape - this is demarcated by leaving the normal state, going through the drawing process, then returning to the normal state again
    (see the explanation of state-based input-handling in my 1 year-in article).
  • Architectural component - this is drawing-specific, and without additional user tagging would need some fancy heuristics.
    This (and higher levels in the hierarchy) are thus ruled out as non-deterministic.
So we're left with shape components and discrete shapes as viable levels for units of interaction. The shape components need to be stored, but the input demarcation of discrete shapes seems more appropriate for a user's mental model. Additionally, returning to part-way through a shape would leave the program in a non-normal state, encouraging mode errors.

For Geometer, shape components have the useful property of being the same as other discrete-shape-level actions: adding points. That is to say that shape components are a subset of the discrete shapes. 'Hierarchy levels' is a leaky abstraction! This allows me to somewhat cheat here: I can store both.

This should allow for something that broadly matches user-based objectives.

Now we have an idea of what level we want to capture, how best to represent this?


Representation

The action representation must provide sufficient information for traversal in either direction.


Undo

Some changes are directly invertible, i.e. the way to reverse can be found from how they work normally. A change of basis, for instance, is primarily a matrix multiplication. This can be reversed by multiplying by the inverse matrix. Other changes need additional information: points only need a position to be added, but its index in the Points array is needed to reverse this. The inverse is true for deleted points.


Redo

This mirrors adding/removing shapes, and is perhaps even easier. This should just do the same thing that the user did, so the initial handling and the redoing can be collapsed into one function.

Not all actions have the same 'remove' equivalent, so Remove___ actions are treated as separate entities rather than inversions of the original action (in the enum).


Collection data structure

I like arrays:
  • They're easy to write bug-minimal code for (particularly with a foreach construct).
  • They are fast to traverse:
    • They're cache-friendly (all the data is in contiguous memory).
    • You often get an efficiency/speed bonus from cache prefetching.
  • Elements at arbitrary indices can be accessed in O(1) time.
  • Bounds checking can be added and toggled pretty easily.
  • They're already serialized for saving to disk.
Unless there's a good reason otherwise (see Future possibilities for actions) I will stick with them.

The data access pattern here is normally going to be linearly progressing either forwards or backwards with the occasional random access, which is a good fit for arrays.

They'll need to be resized as more and more actions are added, so it'll have to be a dynamic array (allocated and reallocated on the heap) rather than simple fixed-size static arrays. See Sean Barrett's stretchy buffer code and Per Vognsen explanation of dynamic arrays as part of Bitwise.


Resultant structs

The data needed for different actions are of comparable size, but not all exactly the same in contents. This is a textbook case for using discriminated unions. That is, indicating the kind of action with an enum/integer ID, then interpreting the following data based on that kind.

My current representation of shapes is as a union, but it needn't necessarily continue like that. As a result I have to discriminate between shape types at the action level. It would be redundant to also have the shape Kind stored in both the shape and the action. I have to have the action Kind, so I only include the union for shapes.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
typedef enum action_types {
    ACTION_Reset   = 0,
    ACTION_Line    = SHAPE_Line,    /* 1 */
    ACTION_Ray     = SHAPE_Ray,     /* 2 */
    ACTION_Segment = SHAPE_Segment, /* 3 */
    ACTION_Circle  = SHAPE_Circle,  /* 4 */
    ACTION_Arc     = SHAPE_Arc,     /* 5 */
    ACTION_Point,
    ACTION_RemovePt,
    ACTION_RemoveShape, /* This will probably turn out to be insufficient */
    ACTION_Basis,
    ACTION_Move,

    ACTION_Count,
    ACTION_SHAPE_START = ACTION_Line,
    ACTION_SHAPE_END   = ACTION_Arc
} action_types;

typedef struct action_v2 {
    i32 Kind;   /* 4 bytes  */
    union {     /* 20 bytes */
        struct reset_action {
            u32 iAction;    /* where I'm resetting back to */
            u32 cPoints;    /* maybe needed for future stuff */
            u32 cShapes;    /* maybe needed for future stuff */
            f32 Empty_Space_To_Fill_1;
            f32 Empty_Space_To_Fill_2;
        } Reset;

        struct shape_action {
            u32 iShape;
            u32 iLayer;
            shape_union;    /* Contains point indices for line/circle/arc */
        } Shape;

        /* struct move_action Move; - temporarily retired while I figure some stuff out */

        struct pt_action {
            u32 iPoint;
            u32 iLayer;
            v2 po;
            u32 Empty_Space_To_Fill;
        } Point;

        basis_v2 Basis;
    };
} action_v2;

Mismatch between user actions and software actions

Given what I've said so far, an example of a user action could be adding an arc. The process of drawing the arc also adds up to 3 new points (if they weren't there already): the focus, the start of the arc and the end of the arc. These need to be added as actions so that the software knows what to undo/redo, but as I've already said, the user action is adding an arc - I don't want the user to have to undo 4 times (3 points + 1 shape) for 1 conceptual action.

My solution to this is pretty simple: I negate the Kind value for all but the last action. The last action is thus conceptually the 'user action' and I have to make sure the actions are ordered accordingly.

I end up with shape components as the software actions and discrete shapes as user actions.

There's an additional wrinkle that the components may already exist. Ways to deal with this are:
  • Not add them (my current strategy) - saves memory and processing, but may make some things harder.
  • Add them and notate them in some way (e.g. high bit in enum)
  • Add them without notation, leave ignoring them to my idempotent AddPoint function, which

Function Layout


Action Functions

As alluded to earlier, more rigour is required for action/delta-based rather than state-based undos because you need to be careful that all the necessary changes are captured. I do this by attaching the action-adding code to the code that makes the relevant change.

Each software action has 2 related functions:
  1. Making whatever changes are necessary without pushing a new action.
    This is used in the undo/redo code, to avoid corruptions of the action array.
  2. A wrapper function that calls (1) then adds an action if necessary.
    This is used in the normal input/interaction handling code.
Below is the basic structure of this for shapes:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// returns true if shape is new (i.e. action to be added)
b32 AddShapeNoAction(state *State, shape Shape, uint *iShapeOut) {
    Assert(ShapeIsValid(Shape));
    b32 ExistingShape = 0;
    uint iShape;

    // check if shape exists already
    for(iShape = 1; iShape <= State->iLastShape; ++iShape) {
        if(ShapeEq(Shape, State->Shapes[iShape])) {
            ExistingShape = 1;
            break;
        }
    }

    // add new shape if needed
    if(! ExistingShape) {
        Push(&State->maShapes, Shape);
        iShape = ++State->iLastShape;
        AddAllShapeIntersects(State, iShape);
    }

    *iShapeOut = iShape;
    return ! ExistingShape;
}

// returns position in Shapes array
uint AddShape(state *State, shape Shape) {
    uint iShape = 0;
    if(AddShapeNoAction(State, Shape, &iShape)) {
        action Action = ActionShape(iShape, Shape);
        AddAction(State, Action);
    }
    return iShape;
}

Non-user actions come before the user action. Here's how I make sure that happens for adding a line segment:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// returns position in Shapes array
inline uint AddSegment(state *State, uint P1, uint P2) {
    shape Shape;
    Shape.Kind = SHAPE_Segment;
    Shape.Line.P1 = P1;
    Shape.Line.P2 = P2;
    return AddShape(State, Shape);
}

inline uint AddSegmentAtPoints(state *State, v2 P1, v2 P2) {
    uint iP1 = AddPoint(State, P1, -ACTION_Point);   /* these will add new actions if the points are new */
    uint iP2 = AddPoint(State, P2, -ACTION_Point);   /* negative -> non-user action */
    return AddSegment(State, iP1, iP2);
}


Undo/Redo functions

The undo/redo functions are divided into a function for software actions and a loop repeats these until a user action is reached.

The simple form of undo/redo works at the component level by switching on the type, then doing the action/its reverse as needed. Here's an abridged version:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void SimpleUndo(state *State) {
    action Action = State->Actions[State->iCurrentAction];
    switch(AbsInt(Action.Kind)) {    /* whether or not it is user-level is dealt with elsewhere */
        /* ... */

        case ACTION_RemoveShape:
        {
            shape *Shape = ShapeFromAction(State, Action);
            uint iShape  = 0;
            AddShapeNoAction(State, Shape, &iShape);
        } break;

        case ACTION_Segment:
        case ACTION_Circle:
        case ACTION_Arc:
        {
            RemoveShape(State, Action);
        } break;

        /* ... */

        default: { Assert(!"Unknown/invalid action type"); }
    }

    --State->iCurrentAction;
}


void SimpleRedo(state *State) {
    action Action = State->Actions[++State->iCurrentAction];
    int UserActionKind = AbsInt(Action.Kind);
    switch(UserActionKind) {
        /* ... */

        case ACTION_RemoveShape:
        {
            RemoveShape(State, Action);
        } break;

        case ACTION_Segment:
        case ACTION_Circle:
        case ACTION_Arc:
        {
            shape *Shape = ShapeFromAction(State, Action);
            uint iShape  = 0;
            AddShapeNoAction(State, Shape, &iShape);
        } break;

        /* ... */
    }
}

The user-level undo/redo then works as follows:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
b32 Ctrl_Z = Keyboard.Ctrl.EndedDown && Press(Keyboard.Z);
b32 Ctrl_Y = Keyboard.Ctrl.EndedDown && Press(Keyboard.Y);
b32 ShiftHeld = Keyboard.Shift.EndedDown;

if(Ctrl_Z && ! ShiftHeld &&
   State->iCurrentAction > 0)
{ // UNDO
    do { SimpleUndo(State); }
    while( ! IsUserAction(State->Actions[State->iCurrentAction]));
}

if((Ctrl_Y || Ctrl_Z && ShiftHeld) &&
    State->iCurrentAction < State->iLastAction)
{ // REDO
    do { SimpleRedo(State); }
    while( ! IsUserAction(State->Actions[State->iCurrentAction]));
}


Required restructuring

I didn't initially distinguish between user-added points and intersections. Adding intersections into the action array would have added a lot of confusion once deleting shapes was working. There would be a non-deterministic number of unnecessary additions for every shape added.

Intersections now only become user points once they have been drawn with. They are calculated based on shape positions and stored in their own dynamic array. After an undo/redo that involves a shape, they are recalculated.


Future possibilities for actions
  • One of the things I like about the action paradigm closely following the user-input model is that it seems like it would lend itself well to macro/function creation.
  • The changes at this level can be fairly easily visualised in a history thumbnail.
  • Although unwise to do automatically, the user might find it helpful to manually group actions together into what would become a hierarchy.
  • Actions could be laid out as a tree, so rather than new actions after undos overwriting all potential redos, they're just written on an new branch.
  • Have variable-sized action data structures. This would save a small amount of memory but would be more error-prone and could make some operations slower.

Conclusion

In this article we:
  • Looked at human factors theory for backing in terms of action cycles and error types.
  • Considered the factors that would make a unit of interaction appropriate for users.
  • Determined some technical objectives for an action representation.
  • Came up with a high-level description of what would fit technical and user objectives.
  • Determined appropriate data structures for actions.
  • Ensured that actions would be captured at the right times.
  • Created a simple and user-level undo/redo.
  • Speculated on potential future developments.
I think this is quite thorough from a theoretical perspective, but would ideally have had some more end-user involvement. Based on use by me and my single test-user (my girlfriend), and given the similarity of my solution to other undo systems, I'm happy with the results so far.

Writing this has helped me clarify some of my own thoughts on the topic and simplify some of the code. Hopefully this has provided you some food for thought in terms of undo systems and interaction design more generally.

Please let me know if any sections were particularly helpful, or if they need any clarification.


Further reading

N.B. I get a kickback if you buy directly through one of these links (on Amazon UK). If you'd rather not do that for whatever reason, they're easy to find online.
  • 'Design of Everyday Things' - Don Norman (Amazon US, UK). This article was heavily influenced by the ideas in this book. I highly recommend it for anyone making anything that people will interact with (yes, that's intentionally broad!).
  • 'Human Error' - James Reason (Amazon US, UK). A classic work for a deeper dive on error than DOET.
Andrew Reece
At some point you'll probably want to include some data or resources into the executable itself (e.g. fonts, images etc). This is particularly true for my case, as I'm trying to keep to a portable single-executable model. That means that I can't keep resources in a surrounding folder and load at runtime, and I don't want to assume that the user already happens to have the files on their system.

Icons are one example of a useful resource that takes an application from looking amateurish to looking like a Real Product (tm).

This post will show some cross-platform methods of adding resources to your application, and a Windows-only method, with icons as an example.


Contents
  • Intro
  • Compiled resources (platform-independent)
    • xxd
    • GIMP
  • Linking (Windows-specific)
    • Linking icons as resources
      • RC file text
      • RC to RES
      • Linking the RES to your executable
      • Resource access from code
      • Using icons
  • Concluding thoughts

Intro

You can divide methods of including resources in your application into 3 groups based on when they are attached, each with an analogous form for adding libraries:
  • Runtime [.dll/.so files] - loaded with fopen and that family of functions.
    I won't be going into this here as there are tutorials for them all over the internet and it's not relevant to my use case.
  • Linked [.lib/.a files] - processed and added by the linker, these have to be 'found' in the executable at runtime.
  • Compiled [files added with #include] - added to source code, commonly as an array of bytes in a header.
Inherent in linking resources to your executable is that it will get larger. The resources that I'm using in an editor are few and small, and as such, the pros of portability and simplicity win out. If you're making a game with large textures and audio files, the trade-off may not be so appealing.

The ease of modding resources in a folder layout is also something that might be more appreciated for a game.


Compiled resources (platform-independent)

Adding resources via an array in a header file is the simplest in terms of runtime use: they can be accessed like any normal variable/array. As such, this is my preferred method unless there are major reasons to go a different way.


xxd

xxd is a command-line utility for interacting with binary files, primarily converting to and from hex data. It's normally a unix application, but it also comes bundled with the Windows version of Vim, which is handy for me... (You can access it directly through Vim with :!xxd ... or find it in the application's folder.)

The default output of xxd is what you see in a hex viewer, and is actually what I was using in my last blog post to validate my file format:
1
xxd test.geo



Hey, this is just a bunch of hexadecimal numbers; you can format numbers like this in normal C code by slapping "0x" at the front:
1
char Fifteen = 0x0F;

Why not just make an array of these and add them to the code?

Helpfully for our purposes, xxd has an option to format its output for an include file:
1
xxd -i test.geo test_geo.h

My test.geo file has changed since the hex dump image above, but here's a look at the current version.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* test_geo.h */
unsigned char test_geo[] = {
    0x47, 0x65, 0x6f, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x01, 0x00, 0x06, 0x00,
    0x49, 0xd9, 0x86, 0x56, 0x8b, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xce, 0xcc, 0xec, 0xc0,
    0x04, 0x00, 0x00, 0xc0, 0x66, 0x35, 0xec, 0x40, 0xbb, 0x6d, 0x6e, 0xc1,
    0xdc, 0x4c, 0xc0, 0xc1, 0xe2, 0x62, 0x46, 0xc1, 0x01, 0x00, 0x00, 0x00,
    0x03, 0x00, 0x00, 0x00, 0x09, 0x21, 0x21, 0x02, 0x00, 0x00, 0x00, 0x01,
    0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
    0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x09,
    0x00, 0x00, 0x00, 0xfa, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0xcd, 0xcc, 0x80, 0xc1, 0xcd, 0xcc, 0x4c, 0x3e, 0xfa,
    0xff, 0xff, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x22, 0x84, 0xbf, 0xb4, 0x39, 0x4f, 0xc1, 0xfa, 0xff, 0xff, 0xff, 0x03,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xb3, 0x3c, 0x40, 0xdb,
    0x74, 0xc9, 0x40, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
    0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0a,
    0x00, 0x00, 0x00, 0xc0, 0xcc, 0xcc, 0x3e, 0x67, 0x66, 0x06, 0x41, 0x01,
    0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0xcd,
    0xcc, 0x04, 0x41, 0x01, 0x00, 0x28, 0xc1, 0x01, 0x00, 0x00, 0x00, 0x02,
    0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0xcd, 0xcc, 0xbd, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0a,
    0x00, 0x00, 0x00, 0x34, 0x33, 0xaf, 0xc1, 0x66, 0x66, 0xf6, 0xc0, 0x03,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x9a,
    0x99, 0x31, 0xc1, 0x34, 0x33, 0x63, 0xc1, 0x03, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0xa0, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcd, 0xcc, 0xcc, 0x3d
};
unsigned int test_geo_len = 419;

An array and a count of bytes - what more could you ask for!

Note that the size on disc of your h file will be about 6 times larger than the file you're including - you're using 6 bytes to encode a single one: "0x47, ". I tend to search and replace to remove the spaces, making it about 5x the orginal filesize. This shouldn't be a problem unless you're really strapped for space on your development machine. It won't affect the executable size.

I currently do this with a font. In one of my earlier releases I forgot that I was loading the font from disc at runtime, so the first feedback I got was that it was crashing on opening... not great!

You can also do some compression to save space in the executable. You then just need a way to decompress at runtime.

If you change the data contents of your files frequently, they can be converted to C headers as part of your build process (before you compile).

If you don't want to get xxd for whatever reason, there are alternatives out there, or you can make your own, which you can do pretty quickly.


GIMP

(Examples are from GIMP 2.8)

While xxd outputs generic bytes from any old file type, if you want to easily include some images, GIMP has a nice feature where it will let you export C source code/headers in RGB(A). This is the basic format:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static const struct {
    unsigned int  width;
    unsigned int  height;
    unsigned int  bytes_per_pixel; /* 2:RGB16, 3:RGB, 4:RGBA */ 
    unsigned char pixel_data[700 * 400 * 4 + 1];
} gimp_image = {
    700, 400, 4,
    "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
    "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
    /* ... */
};

(Note the more compact string-based encoding.)



There are quite a few options, including basic run-length encoding, which dramatically decreases the size of images with blocks of colour. This also includes a macro for reading the RLE data.

You can trade off a significant reduction in executable size with a slightly easier file to read. Have a play around with the various settings and see what you get.


Linking (Windows-specific)

The other option we're exploring here is for the linker to bundle the file into the executable. Once it's there, we need a way to find it at runtime. On Windows, this is done using the 'Resource' API/system. The simplified process for this is:
  1. Notate the files you want included in a .rc text file
  2. 'Compile' the .rc file to a .res file
  3. Link the .res file in with your program
  4. Access the resource data in your code
  5. Use the data
I'll work through this with icons as an example. It seems as though you have to include icons with this method to get them to show up everywhere you want.


Linking icons as resources

I'll assume here that you have an icon. If not, you can create and export .ico files with GIMP.


RC file text

These have a pretty simple format: the name of the resource (as you'll access it in the code), the type, and the relative file location.
1
2
Icon       ICON "G_32.ico"
IconSmall  ICON "G_16.ico"

There are a number of types, including a generic/custom data format.


RC to RES

This just involves running the rc executable on the RC file. (The nologo flag just stops it from spitting out copyright information).
1
rc -nologo geometer.rc

This outputs geometer.res. If you're doing this from the command line then as with cl, you have to run the vcvarsall.bat in the shell before you run rc:
1
call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x64


Linking the RES to your executable

This is just linked as you would a static library:
1
cl ...flags... win32_geometer.c /link ...libs... geometer.res


Resource access from code

Typical resources are accessed with FindResource and LoadResource. Some of these can be accessed more succinctly, e.g. LoadCursor and LoadIcon.


Using icons

Just having the icon resource attached adds the icon to the executable and associated files in Windows Explorer/My Computer.



Adding the icons to the top-left of the program window and the taskbar is easy if you're loading the window class yourself:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
WNDCLASSEX WindowClass    = {0};
WindowClass.cbSize        = sizeof(WNDCLASSEX);
WindowClass.style         = CS_HREDRAW | CS_VREDRAW;
WindowClass.lpfnWndProc   = Win32MainWindowCallback;
WindowClass.hInstance     = Instance;
WindowClass.hCursor       = LoadCursor(0, IDC_ARROW);

WindowClass.hIcon         = LoadIcon(Instance, "Icon");
WindowClass.hIconSm       = LoadIcon(Instance, "IconSmall");

WindowClass.lpszClassName = "BasicWindowClass";


Concluding thoughts

I've shown how I include data in my executables on Windows and with a platform-independent method. I default to using the platform-independent method unless there's a good reason to use something else (like the special treatment icons get as 'Resources'). Other people I know just put everything in the resource files as they're only/primarily targeting Windows, and the RC files keeps everything in one place. There doesn't seem to be much difference in speed at compile- or run-time.

Please do let me know if you attach files to your executable with these methods or if you have any related tips to share.
Andrew Reece
This is a little out-of-sync, but I've been working on it recently and thought it was worth writing about before I forgot it all.

I didn't want the filetype to be something users would have to think about, so I put a reasonable amount of effort into making the data forward-compatible and the loader backward-compatible.

I have no idea how well the following will relate to text-based file formats.


Contents
  • Header Format
  • Data Array Format
  • Type Enumeration
    • Basics
    • Sizing information
    • Enum Versioning
    • Type Versioning
  • Saving Files
    • Checking the byte output
  • Opening/Loading Files
  • Conclusions

Header Format

Most, if not all, binary filetypes begin with a header to provide some metadata about the information to be loaded. There was a bit of back and forth between how the data would be laid out and the header describing it. After some discussion with the lovely folks on the Handmade Network Discord, I decided on some criteria for a file header:
  • Distinguish .geo filetypes from others (based on the data as well as the extension)
  • Version the entire filetype, in case any changes have to be made
  • Determine how much memory needs to be allocated
  • Basic error checking to see if there's any corruption
I ended up with this:
1
2
3
4
5
6
7
8
typedef struct file_header
{
    char ID[8];        // unique(ish) text id: "Geometer"
    u16 FormatVersion; // file format version num
    u16 cArrays;       // for data section
    u32 CRC32;         // checksum of data
    u64 cBytes;        // bytes in data section (everything after this point)
} file_header;

Note that this has been arranged to fit into 24 bytes with no alignment issues. No padding should be necessary.


Data Array Format

The type of thing that you want to store will probably have an impact on the format you want to save it in. Basically everything that I need saving is stored in memory as dynamic arrays. (e.g. Points, Shapes, Intersections...) Given the length of each array and the size of each element, I can tell how many bytes are needed to store it/read from memory (when loading). I also need a way to interpret the type of the elements in an array, from which I can also determine element sizes.

Given this, I lay out each array as follows:
1
2
3
u32 Type;
u32 cElements;
u8  Contents[];

I don't currently have to, but given a type that needs more metadata, the contents could very easily start with a type-specific header, then cElements would refer to either the number of bytes total or the number of elements after the header.

There's also the occasional single data element that needs adding (e.g. the basis of the canvas). You could keep separate sections of the file (indicated in the header) for these... Or they can just be treated as arrays of length 1, which keeps everything nice and simple. If I were predominantly saving individual elements for some reason, the waste might add up here and I'd consider a different decision.


Type Enumeration

Basics

The type corresponds to an enum tag, which just starts at 0 and is appended to any time a new element type is added. Types are never removed once introduced to a public build as then old numbers would be reused, leading different loaders to interpret the same content in different ways. 4 billion (and change) types should be enough to contain all the mistakes I make with the type system. It does require getting over a little perfectionism - having these numbers around that are no longer relevant initially seemed wrong, but I think it's the way to go.

You may want an invalid type at 0, but it's probably just as useful at U32_MAX.
1
2
3
4
5
6
7
8
typedef enum section_header {
    HEAD_Points = 0,
    HEAD_Shapes,
    HEAD_Actions,
    HEAD_Basis,
    /* ... */
    HEAD_COUNT,
} section_header


Sizing information

Given that enums start at 0 and add 1 each time, they can be used as array indexes. I've found it useful to keep an array of element sizes, indexed by enum. This is easy to create with an X-macro, which relies on deferred macro expansion by the preprocessor:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#define SECTION_HEADERS \
    SECTION_HEADER(HEAD_Points = 0,  v2) \
    SECTION_HEADER(HEAD_Shapes,   shape) \
    SECTION_HEADER(HEAD_Actions, action) \
    SECTION_HEADER(HEAD_Basis,    basis) \
    /* ... */

#define SECTION_HEADER(name, type) name,
typedef enum section_header {
    SECTION_HEADERS
    HEAD_COUNT,
} section_header
#undef SECTION_HEADER

#define SECTION_HEADER(name, type) sizeof(type),
size_t HeaderElSizes[] = { SECTION_HEADERS };
#undef SECTION_HEADER
#undef SECTION_HEADERS


Enum Versioning

As types change, they'll need to be interpreted differently, and so need a different tag. I simply append a version number to the end of each name, and if I need a 'canonical' type, I set an unversioned enum value equal to the current version:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#define SECTION_HEADERS \
    SECTION_HEADER(HEAD_Points_v1 = 0,     v2) \
    SECTION_HEADER(HEAD_Shapes_v1,   shape_v1) \
    SECTION_HEADER(HEAD_Actions_v1, action_v1) \
    SECTION_HEADER(HEAD_Basis_v1,    basis_v1) \
    SECTION_HEADER(HEAD_Actions_v2, action_v2) \
    SECTION_HEADER(HEAD_Basis_v2,    basis_v2) \
    /* ... */

#define SECTION_HEADER(name, type) name,
typedef enum section_header {
    SECTION_HEADERS
    HEAD_COUNT,
    HEAD_Points      = HEAD_Points_v1,
    HEAD_Shapes      = HEAD_Shapes_v1,
    HEAD_Actions     = HEAD_Actions_v2,
    HEAD_Basis       = HEAD_Basis_v2,
} section_header
#undef SECTION_HEADER

It's useful to be able to jump back and forth between thinking of the enums as just name tags and as integers.

A coarser-grained alternative here would be to increment the file version in the header with every change. I'm leaving that one for larger, structural changes.


Type Versioning

As you can see, I've also versioned the types in a way that mirrors the tags. A simple example of this is:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* geometer_filetype.h */
typedef struct basis_v1 {
    v2 XAxis; /* The magnitude corresponds to zoom */
    v2 Offset;
} basis_v1;

typedef struct basis_v2 {
    v2 XAxis; /* YAxis == Perp(XAxis) */
    v2 Offset;
    f32 Zoom;
} basis_v2;

/* geometer.h */
typedef basis_v2 basis;

This canonicalisation keeps any cognitive overhead of thinking of different types local to file loading and saving. You don't want to have to consider all the obsolete formats when writing everyday code. I continue this compartmentalisation by putting all the versioned types into a separate file.

The data attached to each array obviates the need for too much description in the header. It also has the nice feature of not changing size depending on the contents.

The arrays can also come in any order or not be included.


Saving Files

Saving is easier than loading as you only need to consider the current version. This is another place that I use X-macros, which here are in the following format:
1
2
3
#define DATA_PROCESSES                            \
    DATA_PROCESS(tagtype, numElements, arrayBase) \
    /* ... */

They let me automate things that were the source of some bugs before I got my macro game on-point. One came from the cArrays value no longer reflecting the actual number of arrays being saved after I added a new datatype. This then meant that the last array wasn't loaded, which tripped the cBytes check. It was not immediately obvious that cArrays was the culprit, and although it wasn't a particularly bad bug to find, did take longer than necessary. Now I just use #define DATA_PROCESS(a, b, c) +1 and set cArrays equal to DATA_PROCESSES. I don't have to worry about forgetting to update this manually again.

Other than that, all the data is sent through the CRC32 calculator (which can be done with multiple continuations to the function) and added to cBytes. This should include the tag and the count of elements, as these make up part of the save data as well!


Checking the byte output

I used xxd to create a hex view of the data. I then put this in Inkscape(a useful free vector drawing tool) and colour-coded the different sections to make sure it was doing what was expected.



It's useful to keep in mind that this is what it looks like on disk - there's no other tagging, it's just a series of numbers that you have to make meaningful. If you're unfamilar with looking at hexadecimal:
  • 2 characters represent 1 byte.
  • The values for a byte range from 00 to FF (255).
  • The bytes are arranged here in little-endian order (least-significant byte first)
You can see that no unexpected padding has been added. (It may also be worth using a #pragma pack and/or statically asserting sizeof(file_header) == ExpectedSize)


Opening/Loading Files

There's no room for perfectionism here either: if you want to maintain backwards compatibility you have to keep a trace of all of your past mistakes around, taunting you.
  1. Given a file to load, you read the size of the header and make sure that it's the right filetype (is the ID as expected)
  2. My filetype does allow reading in arrays all at once, but it's easier to just allocate cBytes and load everything all at once.
  3. This can then be CRC32'd in one go, getting that out of the way.
  4. I then just allocate and load the arrays as you'd expect, and edit any metadata needed.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    Loop for cArrays:
        With type determined by the tag given:
            Allocate memory as needed (e.g. for arenas)
            Mark that type as included in the save file
            Loop for cElements of that type:
                Update the element individually to the latest version of that type
                Add the element to the relevant array/arena/individual variable
            Update metadata as needed (e.g. arena length, capacity)
    Apply defaults for data not provided by file (e.g. basis)
    Calculate any variables that are based on the loaded data (e.g. shape intersections)
    
There's a count of bytes per array as well as overall. These can be checked together for a bit of redundancy.

I was using at one point X-macros again here, but I found that they're not fine-grained enough for dealing with multiple versions of types.

My main breakthrough here was figuring out a way of avoiding O(n^2) code every time the array element types change. The naive thing to do is directly convert all previous types to the current type. Instead, I'm converting each version to the one that follows it, until it is up-to date, somewhat like a conveyor belt (or Duff's device)

Here's an example of what that might look like if I decided to notate all points with layers when I added the concept, then later increased the precision from f32 to f64. (Neither of these are currently the case). The inner for loop is the most important bit, but I thought I'd add the context.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
u8 *At = FileContents;
u8 *FinalAt = At + FileHeader.cBytes;
for(uint iArray = 0; iArray < FH.cArrays; ++iArray)
{
    u32 ElType    = *(u32 *)At;    At += sizeof(ElType);
    u32 cElements = *(u32 *)At;    At += sizeof(cElements);
    TypesFoundInFile[ElType] = 1;
    size_t cBytesEl = cElements * HeaderElSizes[ElType];
    void *Elements = (void *)At;
    At += cBytesEl;
    switch(ElType)
    {
        case HEAD_Points_v1:
        case HEAD_Points_v2:
        case HEAD_Points_v3:
        {
            /* NOTE: uses canonical/current type */
            State->Points = AllocateDynamicArray(HeaderElSizes[HEAD_Points], cElements);

            for(u32 iEl = 0; iEl < cElements; ++iEl)
            {
                point_v1 Point_v1 = {0};
                point_v2 Point_v2 = {0};
                point_v3 Point_v3 = {0};

                /* This will be the same every iteration of the loop,
                   so branch prediction should be very effective */
                switch(ElType)
                {
                    case HEAD_Points_v1:
                        Point_v1 = (*(point_v1 *)Elements)[iEl];
                        Point_v2.X = Point_v1.X;
                        Point_v2.Y = Point_v1.Y
                        Point_v2.Layer = 1;
                        /* fallthrough */

                    case HEAD_Points_v2:
                        if(ElType == HEAD_Points_v2) /* only if it jumped straight here */
                        { Point_v2 = (*(point_v2 *)Elements)[iEl]; }
                        Point_v3.X = (f64)Point_v2.X;
                        Point_v3.Y = (f64)Point_v2.Y;
                        Point_v3.Layer = Point_v2.Layer;
                        /* fallthrough */

                    case HEAD_Points_v3:
                        if(ElType == HEAD_Points_v3)
                        { Point_v3 = (*(point_v3 *)Elements)[iEl]; }
                }

                State->Points[iEl] = Point_v3;    /* point is typedef'd to point_v3 */
            }
        } break;

        /* ... other element types ... */

        default:
        {
            MessageBox(WindowHandle, "Unexpected data or corrupted file. Try again.",
                       "Filetype Error", MB_ICONERROR);
            File = 0;
        }
    }
}


Conclusions

Hopefully I've shown that simple binary formats are not too tricky to implement. There are some gotchas along the way, but hopefully this post will help you avoid a few of them if you decide to make one yourself. Versioning in a sane way for both coding and loading seems to be the largest hurdle. I'm not sure how well my solutions will work for other people's scenarios, but perhaps they've at least sparked a few ideas.