$$\newcommand{\n}{\hat{n}}\newcommand{\thetai}{\theta_\mathrm{i}}\newcommand{\thetao}{\theta_\mathrm{o}}\newcommand{\d}[1]{\mathrm{d}#1}\newcommand{\w}{\hat{\omega}}\newcommand{\wi}{\w_\mathrm{i}}\newcommand{\wo}{\w_\mathrm{o}}\newcommand{\wh}{\w_\mathrm{h}}\newcommand{\Li}{L_\mathrm{i}}\newcommand{\Lo}{L_\mathrm{o}}\newcommand{\Le}{L_\mathrm{e}}\newcommand{\Lr}{L_\mathrm{r}}\newcommand{\Lt}{L_\mathrm{t}}\newcommand{\O}{\mathrm{O}}\newcommand{\degrees}{{^{\large\circ}}}\newcommand{\T}{\mathsf{T}}\newcommand{\mathset}[1]{\mathbb{#1}}\newcommand{\Real}{\mathset{R}}\newcommand{\Integer}{\mathset{Z}}\newcommand{\Boolean}{\mathset{B}}\newcommand{\Complex}{\mathset{C}}\newcommand{\un}[1]{\,\mathrm{#1}}$$

Day 19. Improving Audio Synchronization

Day 19. Improving Audio Synchronization
Video Length (including Q&A): 1h45

Welcome to “Handmade Hero Notes”, the book where we follow the footsteps of Handmade Hero in making the complete game from scratch, with no external libraries. If you'd like to follow along, preorder the game on handmadehero.org, and you will receive access to the GitHub repository, containing complete source code (tagged day-by-day) as well as a variety of other useful resources.

When we left off last time, our audio had many issues. They came to light after we finished implementing our timed frame loops, so our objective for the next couple of days is to make them go away.

At the start of the program, we're opening a sound buffer which is outputting 48000 samples every second to the sound card. These are groups of one value for the left and one value for the right audio channel, representing sound amplitude at a point in time. We take 48000 amplitude points, pass them to the sound device which eventually reconstructs the wavelength to the speakers.

This really is just a first step in sound output, our values undergo a lot of transformations on the way out. While it's somewhat a topic for another day, we can say that the samples are but an approximation of what we will hear in the end. Additionally, it's not actually possible to perfectly synchronize sound output with what you see on the screen due to the delays happening at all stages, some of which external to the computer! In any case, our goal is to prevent really bad sync issues, even before it gets to the hardware.

Day 18 Day 20

(Top)
  0.1  Proper Time Tracking
Visualize the Cursors
  1.1  Display the Play Cursor
    1.1.1  Write Usage Code First
    1.1.2  Process the Play Cursors to Draw
    1.1.3  Write the Line-drawing Function
  1.2  Display the Write Cursor
    1.2.1  Compress Time Markers Into a Struct
    1.2.2  Introduce Win32DrawSoundBufferMarker
    1.2.3  Add WriteCursor Display
Address Debug Output Results
  2.1  Inspect Play Cursors
  2.2  Check Documentation on MSDN
  2.3  Inspect Sound Stream
  2.4  Change PlayCursor Snapshot
  2.5  Inspect Cursor Logic
  2.6  Print Out Cursor Logic Values
  2.7  Determine Sound Card Granularity
  2.8  Inspect Write Cursors
Recap
Side Debugging
  4.1  Access Violation in Win32DebugDrawVertical
    4.1.1  Inspect the Pixel Access Logic
    4.1.2  Step Through
    4.1.3  Inspect Computation of X
    4.1.4  Debug Conclusion
Side Considerations
  5.1  Windows Core Audio
Navigation

   

Proper Time Tracking

One thing we need to get out of the way first though is a bug we introduced last time: we aren't timing our frames correctly. If you run the game and take a look at the timing outputs (in the Output window of your debugger), you'll notice that each frame always runs for more than 33.33ms/f we intend it for. This is because we currently have the following structure in our code:

// ...
// Do some work
// ...

LARGE_INTEGER WorkCounter = Win32GetWallClock();
f32 WorkSecondsElapsed = Win32GetSecondsElapsed(LastCounter, WorkCounter);

f32 SecondsElapsedForFrame = WorkSecondsElapsed;
if (SecondsElapsedForFrame < TargetSecondsPerFrame)
{
    // Go to sleep
}

// ... 
// Do more work (blit)
// ... 

LARGE_INTEGER EndCounter = Win32GetWallClock();
f32 MSPerFrame = 1000.0f * Win32GetSecondsElapsed(LastCounter, EndCounter);
LastCounter = EndCounter;

What we really need is to snap our LastCounter directly after we finished sleeping. Even if it won't technically happen at the very end of the frame, when the program loops back the timing will be correct, and the sleep will happen for the correct amount of time:

LARGE_INTEGER WorkCounter = Win32GetWallClock();
f32 WorkSecondsElapsed = Win32GetSecondsElapsed(LastCounter, WorkCounter);

f32 SecondsElapsedForFrame = WorkSecondsElapsed;
if (SecondsElapsedForFrame < TargetSecondsPerFrame)
{
    while (SecondsElapsedForFrame < TargetSecondsPerFrame)
    {
        if (SleepIsGranular)
        {
            DWORD SleepMS = (DWORD)(1000.0f * (TargetSecondsPerFrame -
                                               SecondsElapsedForFrame));
            if (SleepMS > 0)
            {
                Sleep(SleepMS);
            }
        }
        
        SecondsElapsedForFrame = Win32GetSecondsElapsed(LastCounter,
                                                        Win32GetWallClock());
    }
}
else
{
    // TODO(casey): MISSED FRAME RATE!
    // TODO(casey): Logging
}
LARGE_INTEGER EndCounter = Win32GetWallClock(); f32 MSPerFrame = 1000.0f * Win32GetSecondsElapsed(LastCounter, EndCounter); LastCounter = EndCounter;
// ... game_input *Temp = NewInput; NewInput = OldInput; OldInput = Temp;
LARGE_INTEGER EndCounter = Win32GetWallClock(); f32 MSPerFrame = 1000.0f * Win32GetSecondsElapsed(LastCounter, EndCounter); LastCounter = EndCounter;
u64 EndCycleCount = __rdtsc(); s64 CyclesElapsed = EndCycleCount - LastCycleCount; LastCycleCount = EndCycleCount;
 Listing 1: [win32_handmade.cpp > WinMain] Timing frames correctly.

We also want to correct the sleep cycle itself. We already sleep for exact amount of milliseconds, so to prevent additional checks, we can just spin and wait until the last few microseconds elapse:

f32 SecondsElapsedForFrame = WorkSecondsElapsed;
if (SecondsElapsedForFrame < TargetSecondsPerFrame)
{
if (SleepIsGranular) { DWORD SleepMS = (DWORD)(1000.0f * (TargetSecondsPerFrame - SecondsElapsedForFrame)); if (SleepMS > 0) { Sleep(SleepMS); } } while (SecondsElapsedForFrame < TargetSecondsPerFrame) { SecondsElapsedForFrame = Win32GetSecondsElapsed(LastCounter, Win32GetWallClock()); }
}
 Listing 2: [win32_handmade.cpp > WinMain] Updating Sleep loop.

Finally, even this might not provide you with a 100% correct granularity. You will be missing some microseconds here and there, so let's have a quick test and eventually log and do something about it:

if (SleepIsGranular)
{
    DWORD SleepMS = (DWORD)(1000.0f * (TargetSecondsPerFrame -
                                        SecondsElapsedForFrame));
    if (SleepMS > 0)
    {
        Sleep(SleepMS);
    }
}

f32 TestSecondsElapsedForFrame = Win32GetSecondsElapsed(LastCounter, Win32GetWallClock()); if(TestSecondsElapsedForFrame < TargetSecondsPerFrame) { // TODO(casey): LOG MISSED SLEEP HERE }
while (SecondsElapsedForFrame < TargetSecondsPerFrame) { SecondsElapsedForFrame = Win32GetSecondsElapsed(LastCounter, Win32GetWallClock()); }
 Listing 3: [win32_handmade.cpp > WinMain] Finalizing Sleep Routine.
   

Visualize the Cursors

As we output the samples, we try to have some control on when they're going out. And our objective is for our sound to be as close to our target frame as possible. Some platforms have more support for this than the others, and on Windows we have to call GetCurrentPosition. This will return a position that the DirectSound believes the audio is playing at (“Play Cursor”), and another one to which DirectSound is writing sound to (“Write Cursor”).

   

Display the Play Cursor

   

Write Usage Code First

It will be easier for us to understand the issues if we had a way to visualize these in some way. Let's throw in a simple debug overlay which would draw the cursors over time. Just after we request to display our buffer, we'll write a new #if block that would only run if we're in debug mode (or, how we defined it on day 14, HANDMADE_INTERNAL):

win32_window_dimension Dimension = Win32GetWindowDimension(Window);
Win32DisplayBufferInWindow(&GlobalBackbuffer, DeviceContext, Dimension.Width, Dimension.Height);
#if HANDMADE_INTERNAL // NOTE(casey): This is debug code { DWORD DebugPlayCursor; DWORD DebugWriteCursor; GlobalSecondaryBuffer->GetCurrentPosition(&DebugPlayCursor, &DebugWriteCursor); } #endif
 Listing 4: [win32_handmade.cpp > WinMain] Checking cursor position.

Since it's debug code, we don't really care to test whether the operation SUCCEEDED or not, so we'll skip it here. However, we wrapped this code in braces ({}) to make sure that any variables we declare won't escape and potentially interact with the rest of the code.

We want to visualize more than one cursor, to show some progress. Let's say we want to look at all the cursor positions in the last second: simply take whatever our GameUpdateHz is and create an array of debug cursors of that size. We'll need to keep this array outside of the main loop so that the values are preserved.

We also need an index to the array so that we know where to write. The index will go from 0 to the size of our collection of cursors, to 0 again, and we'll use it to store the latest cursor position.

 Figure 1: Rolling buffer advances its index at each iteration.

// Just before the main loop
game_input Input[2] = {};
game_input* OldInput = &Input[0];
game_input* NewInput = &Input[1];
int DebugLastPlayCursorIndex = 0; DWORD DebugLastPlayCursor[GameUpdateHz];
LARGE_INTEGER LastCounter = Win32GetWallClock(); u64 LastCycleCount = __rdtsc(); GlobalRunning = true; while (GlobalRunning) { // ... // Our main loop // ... #if HANDMADE_INTERNAL // NOTE(casey): This is debug code { DWORD DebugPlayCursor; DWORD DebugWriteCursor; GlobalSecondaryBuffer->GetCurrentPosition(&DebugPlayCursor, &DebugWriteCursor);
Assert(DebugLastPlayCursorIndex < ArrayCount(DebugLastPlayCursor)); DebugLastPlayCursor[DebugLastPlayCursorIndex++] = DebugPlayCursor; if (DebugLastPlayCursorIndex == ArrayCount(DebugLastPlayCursor)) { DebugLastPlayCursorIndex = 0; }
} #endif // ... }
 Listing 5: [win32_handmade.cpp > WinMain] Capturing Cursor in a rolling buffer.

You'll notice however that this implementation isn't compilable. This is because C and C++ do not allow array length initialization from a variable (even if we never change that variable at runtime), the array size has to be known at compile time. For the time being, let's change MonitorRefreshHz and GameUpdateHz to #defines.

WindowClass.lpszClassName = "HandmadeHeroWindowClass";
    
// TODO(casey): How do we reliably query on this on Windows?
#define MonitorRefreshHz 60 #define GameUpdateHz (MonitorRefreshHz / 2)
f32 TargetSecondsPerFrame = 1.0f / (f32)GameUpdateHz;
 Listing 6: [win32_handmade.cpp > WinMain] You could also simply initialize DebugLastPlayCursor to be of length 30.

In order to display the results, we can draw straight to our render buffer, before we request page flip. Let's create a utility function to keep things well separated, called Win32DebugSyncDisplay. To keep things tidy, we'll also scope this inside #if HANDMADE_INTERNAL.

For this function, we're going to need:

We'll also throw in our sound buffer and the TargetSecondsPerFrame that we might use later. We can always remove them later.

win32_window_dimension Dimension = Win32GetWindowDimension(Window);
#if HANDMADE_INTERNAL Win32DebugSyncDisplay(&GlobalBackbuffer, ArrayCount(DebugLastPlayCursor), DebugLastPlayCursor, &SoundOutput, TargetSecondsPerFrame); #endif
Win32DisplayBufferInWindow(&GlobalBackbuffer, DeviceContext, Dimension.Width, Dimension.Height);
 Listing 7: [win32_handmade.cpp > WinMain] Introducing Win32DebugSyncDisplay.
   

Process the Play Cursors to Draw

Let's focus on what's happening inside Win32DebugSyncDisplay. First, let's define the function just outside the WinMain:

inline f32
Win32GetSecondsElapsed(LARGE_INTEGER Start, LARGE_INTEGER End)
{
    // ... 
}
internal void Win32DebugSyncDisplay(win32_offscreen_buffer *Backbuffer, int LastPlayCursorCount, DWORD *LastPlayCursor, win32_sound_output *SoundOutput, f32 TargetSecondsPerFrame) { }
int CALLBACK WinMain(...)
 Listing 8: [win32_handmade.cpp] Defining Win32DebugSyncDisplay.

We should now be compilable, with no changes to how our program operates. Take a second to fix all the outstanding errors you might have thus far.

We won't be doing anything fancy inside this function. We don't have any line drawing library just yet, and we certainly won't going implementing one here. What we want is to:

  1. Find a horizontal point in our render buffer to represent our position in the sound buffer.
  2. Paint that pixel and several more below in white.

Deciding the X position requires:

This yields us the coefficient C. We can use it for calculating X positions of each play cursor we get (remember that we need to redraw all 30 cursors each frame!), by simply multiplying the cursor by C (which we cast back to integer) and adding the pad.

As for the Y position, we don't really need to recalculate it. To draw a vertical line, we only need to know the top and bottom points which can be whichever. Let's say we simply add some pads to these as well and call it done.

// Win32DebugSyncDisplay
int PadX = 16; int PadY = 16; int Top = PadY; int Bottom = Backbuffer->Height - PadY; f32 C = (f32)(Backbuffer->Width - 2 * PadX) / (f32)SoundOutput->SecondaryBufferSize; for (int PlayCursorIndex = 0; PlayCursorIndex < LastPlayCursorCount; ++PlayCursorIndex) { int X = PadX + (int)(C * (f32)LastPlayCursor[PlayCursorIndex]); }
 Listing 9: [win32_handmade.cpp > Win32DebugSyncDisplay] Defining line-drawing logic.

But wait, you'll say, we still haven't drawn anything yet! For simplicity sake, we'll separate line prepping and actual drawing to a separate function. We might use it for other purposes, so having a generic debug vertical line drawing function will always be handy. To this function we'll pass our render buffer, X position, as well as top and bottom boundaries we just defined. Maybe we can also specify the color here.

f32 C = (f32)(Backbuffer->Width - 2 * PadX) / (f32)SoundOutput->SecondaryBufferSize;
for (int PlayCursorIndex = 0;
        PlayCursorIndex < LastPlayCursorCount;
        ++PlayCursorIndex)
{
    int X = PadX + (int)(C * (f32)LastPlayCursor[PlayCursorIndex]);
Win32DebugDrawVertical(Backbuffer, X, Top, Bottom, 0xFFFFFFFF);
}
 Listing 10: [win32_handmade.cpp > Win32DebugSyncDisplay] Introducing a call to draw vertical line.
   

Write the Line-drawing Function

Speaking of drawing, since it's debug code we're going to do the simplest thing imaginable (probably the slowest as well): We'll calculate the starting byte of our top pixel and then loop over the buffer until we reach the bottom pixel, “painting” each pixel the color requested.

A small refresher might be useful here. If you recall what we discussed on day 5, here's what we can say:

Using this information we can find any pixel we want by using only X and Y coordinates (or, in our case, Top):

Once we have completed our operation with the pixel (applied the color), we can simply add Pitch and move on to the next row (while preserving our X location). We continue this way until our Y coordinate reaches Bottom.

internal void Win32DebugDrawVertical(win32_offscreen_buffer *Backbuffer, int X, int Top, int Bottom, u32 Color) { u8 *Pixel = (u8 *)Backbuffer->Memory + Top * Backbuffer->Pitch + X * Backbuffer->BytesPerPixel; for (int Y = Top; Y < Bottom; ++Y) { *(u32 *)Pixel = Color; Pixel += Backbuffer->Pitch; } }
internal void Win32DebugSyncDisplay(...)
 Listing 11: [win32_handmade.cpp] Drawing vertical line.

This won't compile since we decided to remove BytesPerPixel from win32_offscreen_buffer. Let's add it back and initialize it properly in Win32ResizeDIBSection:

struct win32_offscreen_buffer
{
    BITMAPINFO Info;
    void *Memory;
    int Width;
    int Height;
    int Pitch;
int BytesPerPixel;
};
 Listing 12: [win32_handmade.h]
WORD BytesPerPixel = 4;
Buffer->BytesPerPixel = BytesPerPixel;
 Listing 13: [win32_handmade.cpp > Win32ResizeDIBSection] Reintroducing BytesPerPixel to win32_offscreen_buffer.

This should be compilable. Build, run and... crash. What happened? If you'd like to follow along on our debugging journey, head over to subsection 4.1. Or, you can try and find the bug yourself!

In the end, we'll have our play cursors up and running.

 Figure 2: Play Cursor Display.

Depending on your computer's sound latency, you might see old cursors overlapping with the new. In that case, you might want to reduce the array size from GameUpdateHz to GameUpdateHz / 2:

int DebugLastPlayCursorIndex = 0;
DWORD DebugLastPlayCursor[GameUpdateHz / 2] = {};
 Listing 14: [win32_handmade.cpp > WinMain] (Optional) Reducing amount of cursor positions stored / displayed.
   

Display the Write Cursor

Another thing that we can do is to expand our debug drawing function to also draw the write cursor.

   

Compress Time Markers Into a Struct

First, we'll introduce a new structure that called win32_time_marker.

struct win32_sound_output
{
    int SamplesPerSecond;
    int BytesPerSample;
    DWORD SecondaryBufferSize;
    u32 RunningSampleIndex;
    int LatencySampleCount;
};
struct win32_debug_time_marker { DWORD PlayCursor; DWORD WriteCursor; };
#define WIN32_HANDMADE_H #endif
 Listing 15: [win32_handmade.h] Introducing win32_debug_time_marker structure.

As you can probably tell, this will be the structure we'll use as our array. We had one value that we were displaying, and now that we want to have more than one value displayed at a time we can simply throw them in a single structure and record them this way.

Our Win32DebugSyncDisplay becomes thus the following:

internal void
Win32DebugSyncDisplay(win32_offscreen_buffer *Backbuffer,
int MarkerCount, win32_debug_time_marker *Markers,
win32_sound_output *SoundOutput, f32 TargetSecondsPerFrame) { int PadX = 16; int PadY = 16; int Top = PadY; int Bottom = Backbuffer->Height - PadY; f32 C = (f32)(Backbuffer->Width - 2 * PadX) / (f32)SoundOutput->SecondaryBufferSize;
for (int MarkerIndex = 0; MarkerIndex < MarkerCount; ++MarkerIndex)
{
win32_debug_time_marker *ThisMarker = &Markers[MarkerIndex]; Assert (ThisMarker->PlayCursor < SoundOutput->SecondaryBufferSize); f32 XReal = C * (f32)ThisMarker->PlayCursor;
int X = PadX + (int)XReal; Win32DebugDrawVertical(Backbuffer, X, Top, Bottom, 0xFFFFFFFF); } }
 Listing 16: [win32_handmade.cpp] Using win32_debug_time_marker structure. Simple renaming and dereferencing for now, no change of functionality.

Finally, inside WinMain you will see something like this:

game_input Input[2] = {};
game_input* OldInput = &Input[0];
game_input* NewInput = &Input[1];
int DebugTimeMarkerIndex = 0; win32_debug_time_marker DebugTimeMarkers[GameUpdateHz / 2] = {};
LARGE_INTEGER LastCounter = Win32GetWallClock(); u64 LastCycleCount = __rdtsc(); GlobalRunning = true; // ... // Most of main loop // ... #if HANDMADE_INTERNAL Win32DebugSyncDisplay(&GlobalBackbuffer,
ArrayCount(DebugTimeMarkers), DebugTimeMarkers,
&SoundOutput, TargetSecondsPerFrame); #endif Win32DisplayBufferInWindow(&GlobalBackbuffer, DeviceContext, Dimension.Width, Dimension.Height); #if HANDMADE_INTERNAL // NOTE(casey): This is debug code {
DWORD DebugPlayCursor; DWORD DebugWriteCursor; GlobalSecondaryBuffer->GetCurrentPosition(&DebugPlayCursor, &DebugWriteCursor); DebugLastPlayCursor[DebugLastPlayCursorIndex++] = DebugPlayCursor;
Assert(DebugTimeMarkerIndex < ArrayCount(DebugTimeMarkers));
win32_debug_time_marker *Marker = &DebugTimeMarkers[DebugTimeMarkerIndex++];
if (DebugTimeMarkerIndex == ArrayCount(DebugTimeMarker)) { DebugTimeMarkerIndex = 0; }
GlobalSecondaryBuffer->GetCurrentPosition(&Marker->PlayCursor, &Marker->WriteCursor);
} #endif
 Listing 17: [win32_handmade.cpp > WinMain] Changing DebugPlayCursors array to an array of win32_debug_time_markers and using them.

We managed to reduce the amount of lines in this code, while increasing functionality! This is what you can call “Compression-Oriented Programming” style, in which you first write what you need, and the code marches towards better over time. Each time it just gets a little bit better, a little bit cleaner, a little bit more flexible as you go, and everything (hopefully) works out for the best.

   

Introduce Win32DrawSoundBufferMarker

Now we can quickly compile (to ensure that our old functionality is preserved) and move on. To make use of the new functionality, we can pull out the X-calculating to a separate function and call it twice from Win32DebugSyncDisplay:

inline void Win32DrawSoundBufferMarker(win32_offscreen_buffer *Backbuffer, win32_sound_output *SoundOutput, f32 C, int PadX, int Top, int Bottom) { Assert (ThisMarker->PlayCursor < SoundOutput->SecondaryBufferSize); f32 XReal = C * (f32)ThisMarker->PlayCursor; int X = PadX + (int)XReal; Win32DebugDrawVertical(Backbuffer, X, Top, Bottom, 0xFFFFFFFF); }
internal void Win32DebugSyncDisplay(win32_offscreen_buffer *Backbuffer, int MarkerCount, win32_debug_time_marker *Markers, win32_sound_output *SoundOutput, f32 TargetSecondsPerFrame) { int PadX = 16; int PadY = 16; int Top = PadY; int Bottom = Backbuffer->Height - PadY; f32 C = (f32)(Backbuffer->Width - 2 * PadX) / (f32)SoundOutput->SecondaryBufferSize; for (int MarkerIndex = 0; MarkerIndex < MarkerCount; ++MarkerIndex) { win32_debug_time_marker *ThisMarker = &Markers[MarkerIndex];
Assert (ThisMarker->PlayCursor < SoundOutput->SecondaryBufferSize); f32 XReal = C * (f32)ThisMarker->PlayCursor; int X = PadX + (int)XReal; Win32DebugDrawVertical(Backbuffer, X, Top, Bottom, 0xFFFFFFFF);
Win32DrawSoundBufferMarker(Backbuffer, SoundOutput, C, PadX, Top, Bottom); Win32DrawSoundBufferMarker(Backbuffer, SoundOutput, C, PadX, Top, Bottom);
} }
 Listing 18: [win32_handmade.cpp] Introducing Win32DrawSoundBufferMarker.

You will notice that Win32DrawSoundBufferMarker is an inline function. This means that instead of having its own space in the program and being referred to each time this function is called, it's “copied and pasted” in its entirety by the compiler directly in the places where the call happens.

Usually compiler may decide to inline a function on its own, but here it's small enough that we can suggest it on our part.

   

Add WriteCursor Display

Having a separate function allows us to modify it to include variable values that we can pass to it:

inline void
Win32DrawSoundBufferMarker(win32_offscreen_buffer *Backbuffer,
                           win32_sound_output *SoundOutput,
                           f32 C, int PadX, int Top, int Bottom,
DWORD Value, u32 Color)
{
Assert (Value < SoundOutput->SecondaryBufferSize); f32 XReal = C * (f32)Value;
int X = PadX + (int)XReal;
Win32DebugDrawVertical(Backbuffer, X, Top, Bottom, Color);
} internal void Win32DebugSyncDisplay(win32_offscreen_buffer *Backbuffer, int MarkerCount, win32_debug_time_marker *Markers, win32_sound_output *SoundOutput, f32 TargetSecondsPerFrame) { int PadX = 16; int PadY = 16; int Top = PadY; int Bottom = Backbuffer->Height - PadY; f32 C = (f32)(Backbuffer->Width - 2 * PadX) / (f32)SoundOutput->SecondaryBufferSize; for (int MarkerIndex = 0; MarkerIndex < MarkerCount; ++MarkerIndex) { win32_debug_time_marker *ThisMarker = &Markers[MarkerIndex];
Win32DrawSoundBufferMarker(Backbuffer, SoundOutput, C, PadX, Top, Bottom, ThisMarker->PlayCursor, 0xFFFFFFFF); Win32DrawSoundBufferMarker(Backbuffer, SoundOutput, C, PadX, Top, Bottom, ThisMarker->WriteCursor, 0xFFFF0000);
} }
 Listing 19: [win32_handmade.cpp] Using Win32DrawSoundBufferMarker to draw different values in different colors.

This should give us what we want, let's compile and verify.

 Figure 3: Debug display of the PlayCursor (white) and WriteCursor (red).

These lines, which represent position of the cursors in the sound buffer as the time goes by, are not looking pretty. Similarly to the PlayCursors, WriteCursors are spread unevenly across time. Moreover, we can see that the amount of write cursors is distinctly lower than the amount of play cursors.

Now, if we want to expand this display, we only need to add more members to the win32_debug_time_marker struct (and of course write data to them). Similarly, if don't need one or both marker types that we already have, we can simply comment out (//) the respective calls to Win32DrawSoundBufferMarker.

   

Address Debug Output Results

   

Inspect Play Cursors

Let's give a closer look at what we're seeing. If we only enable PlayCursors (comment out the line drawing WriteCursor inside Win32DebugSyncDisplay), we can observe several things:

 Figure 4: A Better Look at Play Cursor Display.

The best bet for now would be go and investigate what's going on with those gaps.

   

Check Documentation on MSDN

The first place to start searching would be in the DirectSound configuration. Maybe we missed something that could help us here?

The page detailing contents of the DSBCAPS structure lists all the possible flags you might pass to a buffer. Among others, a suitable candidate is the flag DSBCAPS_GETCURRENTPOSITION2:

The buffer uses the new behavior of the play cursor when IDirectSoundBuffer8::GetCurrentPosition is called. In the first version of DirectSound, the play cursor was significantly ahead of the actual playing sound on emulated sound cards; it was directly behind the write cursor. Now, if the DSBCAPS_GETCURRENTPOSITION2 flag is specified, the application can get a more accurate play cursor. If this flag is not specified, the old behavior is preserved for compatibility. This flag affects only emulated devices; if a DirectSound driver is present, the play cursor is accurate for DirectSound in all versions of DirectX.

This seems like a long shot, but let's try it anyway.

// NOTE(casey): "Create" a secondary buffer
DSBUFFERDESC BufferDescription = {};
BufferDescription.dwSize = sizeof(BufferDescription);
BufferDescription.dwFlags = DSBCAPS_GETCURRENTPOSITION2;
BufferDescription.dwBufferBytes = BufferSize; BufferDescription.lpwfxFormat = &WaveFormat;
 Listing 20: [win32_handmade.cpp > Win32InitDSound] Adding an extra flag to the secondary buffer.

... No changes. Oh well, it was worth a try. No other flag looks particularly useful for our situation.

Another thing we could try is to check whether we pass PlayCursor and WriteCursor in a right order to GetCurrentPosition. Maybe WriteCursor should go first? Let's check documentation:

HRESULT GetCurrentPosition( LPDWORD lpdwCurrentPlayCursor, LPDWORD lpdwCurrentWriteCursor );

Nope, the order is correct. What else it could be?

   

Inspect Sound Stream

We know we have to write from the last position we wrote to for the sound to be continuous. That's what we're doing here:

game_sound_output_buffer SoundBuffer = {};
SoundBuffer.SamplesPerSecond = SoundOutput.SamplesPerSecond;
SoundBuffer.SampleCount = BytesToWrite / SoundOutput.BytesPerSample;
SoundBuffer.Samples = Samples;

// ... 
// Preparing render buffer
// ... 

GameUpdateAndRender(&GameMemory, NewInput, &Buffer, &SoundBuffer);

if(SoundIsValid)
{
    Win32FillSoundBuffer(&SoundOutput, ByteToLock, BytesToWrite, &SoundBuffer);
}

With the sound buffer, we're providing our game with the information of how many samples should we write. The game writes the samples through GameUpdateAndRender to our own Samples memory block. Afterwards, we transfer the written samples to DirectSound buffers and ultimately to the hardware (in Win32FillSoundBuffer). So this covers most of the writing phase. But how early ahead of the Play Cursor should we be writing?

   

Change PlayCursor Snapshot

Now, before we move on and analyze the debug output thoroughly, there's another potential bug hiding in our code. Similarly to the timing issues we encountered earlier, it's possible there's a snapshot timing matter.

Instead of capturing the Play Cursor just before we send it to the game, we probably should calculate it at the frame flip. We're already doing it in our recently added debug code section, so parts of it will be promoted to non-debug code. We'll be still using them for our debug purposes.

Now, because it's no longer debug code, we'll need to test whether the GetCurrentPosition succeeded. We can't allow a failure condition in code exposed to the user, should GetCurrentPosition fail, we'll simply mark our SoundIsValid as false and ignore anything sound-related.

#if HANDMADE_INTERNAL
Win32DebugSyncDisplay(&GlobalBackbuffer,
                        ArrayCount(DebugTimeMarkers), DebugTimeMarkers,
                        &SoundOutput, TargetSecondsPerFrame);
#endif
Win32DisplayBufferInWindow(&GlobalBackbuffer, DeviceContext, Dimension.Width, Dimension.Height);
DWORD PlayCursor = 0; DWORD WriteCursor= 0; if (SUCCEEDED(GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, &WriteCursor))) { LastPlayCursor = PlayCursor; SoundIsValid = true; } else { SoundIsValid = false; }
#if HANDMADE_INTERNAL // NOTE(casey): This is debug code { Assert(DebugTimeMarkerIndex < ArrayCount(DebugTimeMarkers)); win32_debug_time_marker *Marker = &DebugTimeMarkers[DebugTimeMarkerIndex++]; if (DebugTimeMarkerIndex == ArrayCount(DebugTimeMarkers)) { DebugTimeMarkerIndex = 0; }
Marker->PlayCursor = PlayCursor; Marker->WriteCursor = WriteCursor;
} #endif
 Listing 21: [win32_handmade.cpp > WinMain] Capturing the cursors at the end of frame.

Instead of SUCCEEDED(GetCurrentPosition()), you can also write GetCurrentPosition() == DS_OK.

While we're at it, we can handle the case where the sound is valid now, but it wasn't valid before. At that point, we should simply reset our RunningSampleIndex (storing the position in the DirectSound buffer to write to) to whatever our WriteCursor is (divided by bytes per sample since we write in samples).

if (SUCCEEDED(GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, &WriteCursor)))
{
    LastPlayCursor = PlayCursor;
if (!SoundIsValid) { SoundOutput.RunningSampleIndex = WriteCursor / SoundOutput.BytesPerSample;
SoundIsValid = true;
}
}
 Listing 22: [win32_handmade.cpp > WinMain] Adding sound initialization.

You might have noticed LastPlayCursor to which we register the PlayCursor's value. This will work similar to how the counters work: LastPlayCursor will live outside the GlobalRunning loop, so that its value can be preserved from one frame to the next. We will also move SoundIsValid up there.

int DebugTimeMarkerIndex = 0;
win32_debug_time_marker DebugTimeMarkers[GameUpdateHz / 2] = {};
DWORD LastPlayCursor = 0; b32 SoundIsValid = false;
LARGE_INTEGER LastCounter = Win32GetWallClock(); u64 LastCycleCount = __rdtsc(); GlobalRunning = true;
 Listing 23: [win32_handmade.cpp > WinMain] Initializing LastPlayCursor.

Coming into the cursor logic section, we can start simplifying things significantly. We no longer need to capture PlayCursor here, and we can just check if the sound is valid to proceed with the computation using the last play cursor:

// NOTE(casey): Compute how much sound to write and where
DWORD ByteToLock = 0; DWORD TargetCursor = 0; DWORD BytesToWrite = 0;
DWORD PlayCursor = 0; DWORD WriteCursor = 0; b32 SoundIsValid = false; // TODO(casey): Tighten up sound logic so that we know where we should be // writing to and can anticipate the time spent in the game updates.
if (SoundIsValid)
{ ByteToLock = ((SoundOutput.RunningSampleIndex * SoundOutput.BytesPerSample) % SoundOutput.SecondaryBufferSize);
TargetCursor = ((LastPlayCursor +
(SoundOutput.LatencySampleCount * SoundOutput.BytesPerSample)) % SoundOutput.SecondaryBufferSize); if(ByteToLock > TargetCursor) { BytesToWrite = SoundOutput.SecondaryBufferSize - ByteToLock; BytesToWrite += TargetCursor; } else { BytesToWrite = TargetCursor - ByteToLock; }
SoundIsValid = true;
}
 Listing 24: [win32_handmade.cpp > WinMain] Updating cursor logic section.

Speaking of latency, we currently defined it as:

SoundOutput.LatencySampleCount = SoundOutput.SamplesPerSecond / (GameUpdateHz / 2);

This looks a bit confusing, as double division usually is. We could rewrite it to make it clearer that it's 2 frames worth of sound. We simply calculate what's the latency of one frame, and then multiply by however many frames we need.

#define FramesOfAudioLatency 2
#define MonitorRefreshHz 60 #define GameUpdateHz (MonitorRefreshHz / 2) // ... // NOTE(casey): Sound test win32_sound_output SoundOutput = {}; SoundOutput.SamplesPerSecond = 48000; SoundOutput.BytesPerSample = sizeof(s16) * 2; SoundOutput.SecondaryBufferSize = 2 * SoundOutput.SamplesPerSecond * SoundOutput.BytesPerSample; SoundOutput.RunningSampleIndex = 0; SoundOutput.RunningSampleIndex = 0;
SoundOutput.LatencySampleCount = FramesOfAudioLatency * (SoundOutput.SamplesPerSecond / GameUpdateHz);
 Listing 25: [win32_handmade.cpp > WinMain] Updating Latency notation.

Overall, this refactoring should be a bit more sane in fixing our issue. Let's compile and test it. Yep, the sound bug is still there.

   

Inspect Cursor Logic

Let's dive deep into our cursor logic block (eventually we could pull it out to a separate function but for now we'll leave it there).

DWORD ByteToLock = 0;
DWORD TargetCursor = 0;
DWORD BytesToWrite = 0;
if (SoundIsValid)
{
    ByteToLock = ((SoundOutput.RunningSampleIndex * SoundOutput.BytesPerSample)
                    % SoundOutput.SecondaryBufferSize);
    
    TargetCursor = ((LastPlayCursor +
                        (SoundOutput.LatencySampleCount * SoundOutput.BytesPerSample))
                    % SoundOutput.SecondaryBufferSize);
    
    if(ByteToLock > TargetCursor)
    {
        BytesToWrite = SoundOutput.SecondaryBufferSize - ByteToLock;
        BytesToWrite += TargetCursor;
    }
    else
    {
        BytesToWrite = TargetCursor - ByteToLock;
    }
}

game_sound_output_buffer SoundBuffer = {};
// ... 

In this block we calculate ByteToLock (start point of our write), TargetCursor (end point of the write) and BytesToWrite (total written this frame). We got LastPlayCursor position during last frame, by asking Windows directly.

LatencyPlayCursor.ByteToLockTargetCursorBytesToWrite.DirectSoundsecondarybuffer

 Figure 5: Our cursor logic if BytesToWrite is behind the TargetCursor. There is also WriteCursor but we don't care about it here.

All of this seems correct.

   

Print Out Cursor Logic Values

To better understand what's going on, we can make another one of those debug printouts, to better compare different values over time. We'll put it next to the sound filling buffer routine. If you prefer, you can also disable the FPS printout.

game_offscreen_buffer Buffer = {};
// ...

GameUpdateAndRender(&GameMemory, NewInput, &Buffer, &SoundBuffer);

if(SoundIsValid)
{
    Win32FillSoundBuffer(&SoundOutput, ByteToLock, BytesToWrite, &SoundBuffer);
    
#if HANDMADE_INTERNAL DWORD PlayCursor; DWORD WriteCursor; GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, &WriteCursor); char TextBuffer[256]; sprintf_s(TextBuffer, sizeof(TextBuffer), "PC:%u BTL:%u TC:%u BTW:%u\n", PlayCursor, ByteToLock, TargetCursor, BytesToWrite); OutputDebugStringA(TextBuffer); #endif
} // ...
#if 0
// debug timing output f32 FPS = 0.0f; // To be fixed later f32 MegaCyclesPerFrame = (f32)CyclesElapsed / (1000.0f * 1000.0f); char FPSBuffer[256]; sprintf_s(FPSBuffer, sizeof(FPSBuffer), "%.02fms/f, %.02ff/s, %.02fMc/f\n", MSPerFrame, FPS, MegaCyclesPerFrame); OutputDebugStringA(FPSBuffer); #endif
 Listing 26: [win32_handmade.cpp > WinMain] Printing cursor values.

What we will get when we compile and run the game inside the debugger is the progressively increasing PlayCursor, ByteToLock, TargetCursor. As for the BytesToWrite, on our machine it oscillates between 5780 and 7680 bytes. This is indicative of less than precise granularity.

   

Determine Sound Card Granularity

If we wanted to calculate the sound card granularity precisely, we can do it by locking ourselves in an infinite loop. Let's have some fun! Just before entering our main GlobalRunning loop, we'll write a new, fake, one:

GlobalRunning = true;
                
while (GlobalRunning) { DWORD PlayCursor; DWORD WriteCursor; GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, &WriteCursor); char TextBuffer[256]; _snprintf_s(TextBuffer, sizeof(TextBuffer), "PC:%u WC:%u\n", PlayCursor, WriteCursor); OutputDebugStringA(TextBuffer); }
while (GlobalRunning) // actual game loop
 Listing 27: [win32_handmade.cpp > WinMain] Printing more cursor values.

After compiling, make sure to run the game from the debugger! You'll only be able to close it by stopping debugging (Ctrl-F5 from the debugger). Let it run for a couple of seconds and stop.

You will see an output similar to this one:

PC:65280 WC:71040
PC:65280 WC:71040
PC:65280 WC:71040
PC:67200 WC:72960
PC:67200 WC:72960
PC:67200 WC:72960
PC:67200 WC:72960
PC:67200 WC:72960
PC:67200 WC:72960

Now you'll notice that each time you call GetCurrentPosition the system reports the same exact position for a while, until it shifts significantly. If we take two subsequent values and find the difference, we'll see it's \(72960 - 71040 = 1920\) bytes. How does this translate to our game?

We know that we have 4 bytes per sample (2 for each channel). So we can make a division \(\frac{1920}{4} = 480\) samples. We also set our sound card frequency to 48000 samples per second, so with a framerate of 30 frames per second, we fill \(\frac{48000}{30} = 1600\) samples per frame. This means that, with granularity of 480, the cursors should advance \(\frac{1600}{480} \approx 3.33\) times each frame.

That doesn't sound too bad. For what we're trying to do, this should be enough to work. Ok, let's #if 0 our mini loop so that we can re-enable it in the future if needed.

#if 0 // NOTE(casey): This tests the PlayCursor/WriteCursor update frequency // On the Handmade Hero machine, it was 480 samples.
while (GlobalRunning) { DWORD PlayCursor; DWORD WriteCursor; GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, &WriteCursor); char TextBuffer[256]; _snprintf_s(TextBuffer, sizeof(TextBuffer), "PC:%u WC:%u\n", PlayCursor, WriteCursor); OutputDebugStringA(TextBuffer); }
#endif
 Listing 28: [win32_handmade.cpp > WinMain] Restoring our game functionality.

Recompiling should give us back our beautiful gradient and terrible sound, just as we expected.

   

Inspect Write Cursors

Let's check our cursors again as they're drawn:

 Figure 6: Debug display of the PlayCursor (white) and WriteCursor (red).

A distinct possibility is that we're still writing too close to the write cursor. This isn't good, as trying to read while writing is never a good idea. So let's expand our text printout to include not only the last play cursor, but also the latest, as well as the write cursor.

if(SoundIsValid)
{
    Win32FillSoundBuffer(&SoundOutput, ByteToLock, BytesToWrite, &SoundBuffer);
    
#if HANDMADE_INTERNAL
    DWORD PlayCursor;
    DWORD WriteCursor;
    GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, &WriteCursor);
    char TextBuffer[256];
sprintf_s(TextBuffer, sizeof(TextBuffer), "LPC:%u BTL:%u TC:%u BTW:%u - PC:%u WC:%u\n", LastPlayCursor, ByteToLock, TargetCursor, BytesToWrite, PlayCursor, WriteCursor);
OutputDebugStringA(TextBuffer); #endif }
 Listing 29: [win32_handmade.cpp > WinMain] Printing more cursor values.

Which will give us the printout similar to this:

LPC:86400 BTL:93440 TC:99200 BTW:5760 - PC:88320 WC:94080
LPC:94080 BTL:99200 TC:106880 BTW:7680 - PC:101760 WC:107520
LPC:105600 BTL:112640 TC:118400 BTW:5760 - PC:107520 WC:113280

We can see that our PlayCursor advances nicely each frame (as LPC and PC values indicate on each line). However, the WriteCursor is significantly far ahead compared to our hypothetic ByteToLock (and even TargetCursor!). This isn't good news because, in Windows' books, any space between PlayCursor and WriteCursor is unsafe territory, where bad things happen.

In fact, if you calculate the gap between the WriteCursor and PlayCursor, you will get an astonishing \(113280 - 107520 = 5760\) bytes! That's 1440 samples, almost a full frame! Which means our dream of having synchronous frame flip for both audio and video at the same time (immediately after this frame) seems quite slim indeed.

Let's just test our hypothesis and increase our latency from 2 to 3 frames.

#define FramesOfAudioLatency 3
#define MonitorRefreshHz 60 #define GameUpdateHz (MonitorRefreshHz / 2)
 Listing 30: [win32_handmade.cpp > WinMain] Testing higher latency.

If we compile and run... the bug would disappear, and you'll hear nice and clean sound. Yay us?

   

Recap

After a long and strenuous investigation day, we can finally conclude that our sound issues were indeed related to low latency. Having such a big delay between the image and the sound is far from perfect, even more so when we transition from 30 to 60 frames per second.

This unfortunately is a sad conclusion, which we will try to overcome next time. We want that ultimately our game wouldn't think of any issues that the platform layer might be facing, and work with pure (and immediate) sound stream. There might be also additional bugs that we may uncover which would help us!

Ultimately, it's not as bad as it sounds. Human brain is quite forgiving for any lag between what you see and what you hear, and tends to synchronize those together. Unless we're going for a beat-matching game, we should be fine. Still, narrowing the latency it makes it subjectively better to feel.

Additionally, we still need to clean up all the loose ends before we are ready to commit to our game layer.

   

Side Debugging

   

Access Violation in Win32DebugDrawVertical

We got an access violation while trying to write the color to Pixel. This probably means that we made a mistake somewhere along the calculation of our pixel location, so let's go thoroughly through everything that's happening.

   

Inspect the Pixel Access Logic

Our pixel access logic seems sound.

u8 *Pixel = (u8 *)Backbuffer->Memory +
            Top * Backbuffer->Pitch +
            X * Backbuffer->BytesPerPixel;

We correctly cast to u8 (which is exactly 1 byte long), so the offsets happen byte after byte. If we inspect Backbuffer in the Watch window, it will give us positive amounts, so we move forward each time:

Name Value
Backbuffer
> Info ...
Memory 0x...
Width 1280
Height 720
Pitch 5120
BytesPerPixel 4
 Table 1: Inspection of Backbuffer values inside the Watch window.

However, if you inspect the difference between the Backbuffer Memory address and Pixel address, you will get some crazy number:

Name Value
Pixel - (unsigned char*)Backbuffer->Memory 45032324
 Table 2: Displaying Pixel Offset.

That's not good at all. Based on the numbers above, the relative address of the pixel should be somewhere between 0 and (Height + 1) * Pitch * BytesPerPixel, i.e. 14 766 080. Our number came in above 45 million, waaay above from it should have been.

   

Step Through

Let's step through our the code. Stop the program, set a breakpoint (F9) before entering in Win32DebugDrawVertical, and inspect our logic as it comes in:

Name Value
Pixel - (unsigned char*)Backbuffer->Memory 81996
Top * Backbuffer->Pitch 81920
X * Backbuffer->BytesPerPixel 76
 Table 3: Pixel Offset and X/Y offsets at our first run. (actual numbers might differ slightly on your machine)

You can repeat the same calculations directly: \(1280 * 4 = 5120\), \(19 * 5120 = 81920\). Looks correct for the start.

We then step through our Y loop and check our Pixel offset. It increases correctly... until we step out of function.

We can now conclude that our function succeeded the first time through, so the issue must be somewhere else.

   

Inspect Computation of X

The only parameter that varies coming into Win32DebugDrawVertical is X. X is calculated on the fly at each iteration. Maybe our coefficient C calculation is incorrect? As long as our X is low, there're no issues inside the drawing routine.

Let's verify this by clearing (F9) all the breakpoints and letting our program run until the crash.

Name Value
X 11237601
 Table 4: Inspecting X value at the moment of crash.

There's your problem! That's nowhere near from where a realistic value should be (between 0 and buffer width, i.e. 1280). Let's take a step back, out from Win32DebugDrawVertical and check what's happening inside Win32DebugSyncDisplay. But before we do that, let's split our computation a bit more: we'll highlight the PlayCursor value as well as the real computation of X (without the pad and as a \({\rm I\!R}\) number)

f32 C = (f32)(Backbuffer->Width - 2 * PadX) / (f32)SoundOutput->SecondaryBufferSize;
for (int PlayCursorIndex = 0;
        PlayCursorIndex < LastPlayCursorCount;
        ++PlayCursorIndex)
{
DWORD ThisPlayCursor = LastPlayCursor[PlayCursorIndex]; f32 XReal = C * (f32)ThisPlayCursor;
int X = PadX + (int) XReal;
Win32DebugDrawVertical(Backbuffer, X, Top, Bottom, 0xFFFFFFFF); }
 Listing 31: [win32_handmade.cpp > Win32DebugSyncDisplay] Splitting computation for simpler debugging.

This preparation is absolutely not necessary, but it's setting us up for debugging success. Remember to stop debugging before recompiling!

Let's set the breakpoint at ThisPlayCursor and step into (`F10) our program.

On the first round (and after they've been initialized), the values seem reasonable:

Name Value
ThisPlayCursor 1136
XReal 3.6920002
X 19
 Table 5: X Calculation at the first round.

However, if you run through the loop a few times (F10 or F5), you'll eventually arrive to something like this:

Name Value
ThisPlayCursor 117702659
XReal 765067.250
X 765083
 Table 6: X Calculation before the crash.

That's... bizarre. It seems like the issue isn't even coming from C calculation, but from the PlayCursor itself!

Similarly to the Pixel, value, PlayCursor is always bound between a specific range: between 0 and SecondaryBufferSize. If you inspect the latter, it gives you 384000, and the PlayCursor here is way outside of that range.

Actually, this is something that we always expect to be true, so let's drop an assertion which would check just that.

DWORD ThisPlayCursor = LastPlayCursor[PlayCursorIndex];
Assert(ThisPlayCursor < SoundOutput->SecondaryBufferSize);
f32 XReal = C * (f32)ThisPlayCursor; int X = PadX + (int) XReal; Win32DebugDrawVertical(Backbuffer, X, Top, Bottom, 0xFFFFFFFF);
 Listing 32: [win32_handmade.cpp > Win32DebugSyncDisplay] Asserting the PlayCursor never goes outside the bounds.

As an aside, this in turn will lead to a compilation warning (signed/unsigned mismatch) which you can fix by changing the type of SecondaryBufferSize from int to DWORD.

struct win32_sound_output
{
    int SamplesPerSecond;
    int BytesPerSample;
DWORD SecondaryBufferSize;
u32 RunningSampleIndex; int LatencySampleCount; };
 Listing 33: [win32_handmade.h] Setting up correct type for SecondaryBufferSize.

Anyway, once we run the program now, the assertion will just go off, notifying us of what we've suspected already: the array of PlayCursors was set up incorrectly, and some values are garbage. In fact, if you check where we (don't) initialize the DebugLastPlayCursor, we currently leave our variable at whatever values that memory segment had:

// Just before the main loop
game_input Input[2] = {};
game_input* OldInput = &Input[0];
game_input* NewInput = &Input[1];

int DebugLastPlayCursorIndex = 0;
DWORD DebugLastPlayCursor[GameUpdateHz]; // <- Here's your problem!

So in order to fix the bug, we simply need to initialize the array to 0:

int DebugLastPlayCursorIndex = 0;
DWORD DebugLastPlayCursor[GameUpdateHz] = {};
 Listing 34: [win32_handmade.cpp > WinMain] Properly initializing DebugLastPlayCursor.
   

Debug Conclusion

We successfully found and fixed the issue of the uninitialized array. We stepped through the memory, compared the numbers to verify that the Y was computed properly, therefore we identified X as the culprit.

(Continue to subsection 2.1)

   

Side Considerations

   

Windows Core Audio

The Windows Core Audio API and its sub-API Windows Audio Session API (WASAPI) are relatively recent audio APIs which were introduced in Windows Vista. DirectSound and other older APIs are all emulated on Core Audio, so eliminating the middle man does have an immediate positive effect on audio latency.

To learn more about the API and how to implement it in Handmade Hero, check out this discussion on Handmade Network.

   

Navigation

Previous: Day 18. Enforcing a Video Frame Rate

Up Next: Day 20. Debugging the Audio Sync

Back to Index

Glossary

MSDN

DSBCAPS

GetCurrentPosition

Core Audio API

WASAPI

formatted by Markdeep 1.10