Day 19: Improving Audio Synchronization - Casting float to DWORD on day019 ̶r̶o̶u̶n̶d̶s̶ ̶t̶o̶ ̶n̶e̶a̶r̶e̶s̶t̶,̶ ̶r̶a̶t̶h̶e̶r̶ ̶t̶h̶a̶n̶ ̶t̶r̶u̶n̶c̶a̶t̶e̶s̶,̶ ̶t̶h̶e̶ ̶r̶e̶s̶u̶l̶t̶ ̶w̶i̶t̶h̶ ̶G̶C̶C̶

[EDIT] tl;dr its just a bug in my code. Casting floats to ints truncates.

Mārtiņš hit pretty close to the issue in this post when describing a specific intrinsic float to int cast that rounds to the nearest integer, rather than truncate. However, whilst using ordinary casting in GCC, I duplicated Casey's timing check:

1
2
1006                            DWORD SleepMS = (DWORD)(1000.0f * (TargetSecondsPerFrame -
1007                                                               SecondsElapsedForFrame));


which eventually, quickly, and inevitably triggered the Assert (duped from HMH line 1016). It might run any number of passes through the game loop, but always managed to crash within the first three wraps of the audio ring buffer. After one of the crashes, I fetched the values of the elapsed seconds, the target seconds, then did the math by hand to find 200.56 became 201ms for the time to sleep. That was just enough to push it ever so slightly over my target seconds.

As it turns out, every other major compiler apart from Microsoft's does use the round to nearest intrinsic for their casts from float. Microsoft knows this and there is an open bug for it on their website.

̶F̶Y̶I̶,̶ ̶i̶f̶ ̶t̶h̶i̶s̶ ̶b̶u̶g̶ ̶w̶a̶s̶ ̶t̶o̶ ̶b̶e̶ ̶f̶i̶x̶e̶d̶,̶ ̶a̶n̶d̶ ̶y̶o̶u̶r̶ ̶c̶o̶d̶e̶ ̶d̶e̶p̶e̶n̶d̶s̶ ̶o̶n̶ ̶t̶r̶u̶n̶c̶a̶t̶i̶n̶g̶ ̶c̶a̶s̶t̶s̶ ̶f̶r̶o̶m̶ ̶f̶l̶o̶a̶t̶,̶ ̶y̶o̶u̶ ̶c̶o̶u̶l̶d̶ ̶h̶a̶v̶e̶ ̶s̶t̶r̶a̶n̶g̶e̶ ̶b̶u̶g̶s̶ ̶a̶p̶p̶e̶a̶r̶i̶n̶g̶ ̶i̶n̶ ̶y̶o̶u̶r̶ ̶p̶r̶o̶g̶r̶a̶m̶ ̶i̶f̶ ̶y̶o̶u̶ ̶u̶p̶d̶a̶t̶e̶d̶ ̶o̶r̶ ̶i̶n̶s̶t̶a̶l̶l̶e̶d̶ ̶a̶ ̶n̶e̶w̶ ̶v̶e̶r̶s̶i̶o̶n̶ ̶o̶f̶ ̶V̶i̶s̶u̶a̶l̶ ̶S̶t̶u̶d̶i̶o̶.̶

Though probably not a very good solution, subtracting 1ms from every calculation of time to sleep stopped the bug and allowed my Pure C version of this timing test to run. I use a gaming laptop with decent audio hardware, so I don't think I even *needed* to code Casey's logic around this and go directly to day 20's much more robust audio timing code, but in the pursuit I discovered some very useful diagnostics around compiler behavior, for which I am looking :)

Edited by k2t0f12d on
This is completely not true. GCC same as MSVC truncates. It would be a huge bug if GCC rounded instead of truncation, because C/C++ standard requires to truncate when casting. It would break so many different audio, math and graphics applications because they rely on this behavior.

Here's the program:
1
2
3
4
5
6
7
#include <stdio.h>

int main()
{
    volatile float f = 1.6f;
    printf("%i\n", (int)f);
}

Output from gcc:
1
2
$ gcc -O2 t.c && ./a.out
1

That issue on MS connect website is about performance and how use better optimizations to generate less instructions. Its not about trunctation vs rounding when casting to int.

Edited by Mārtiņš Možeiko on
Okay. I tested that out on all my builds using the same flags etc etc as I do to build my platform code, and can confirm, cast from float to int truncates on all of them.

What was actually happening in my original printf was "%ul", sleepms, hence it was printing out 20 + an l, not 201.

(If I had been thinking about the size of that return it might have twigged that 201ms is a lot for one run through the game loop, but 20 isn't.)

Nevertheless, looking around for the bug turned up that bug report to Microsoft reporting that Everyone Else is Doing It, Why Not You, and it was a perfect storm that made be believe 100% that my compiler was rounding to the nearest int.

So back to square one. There's a bug in my audio timing code that's setting a sleep time that's a tiny bit too long, once in awhile.
I rechecked all my code leading up to the calculation of sleepms, and it looks fine. At least as good as what was in HMH at the end of day 019 (bearing in mind, Casey left a comment on it saying it was probably buggy). I tested without subtracting 1ms from the sleep, several times. Although the crash happens randomly, the over sleep is very near two ten-thousandths of a second.

That made me think it could be my diagnostics and printf's eating up just enough clock time to oversleep. I removed them all and the Assert never fires.

It just seems to be *that* close there's no wiggle room in there ;)

[EDIT] Oh no wonder, I never add any frames of latency at all. My equiv of SoundOutput.LatencySampleCount is merely SamplesPerSecond / (GameUpdateHz / 2).

Edited by k2t0f12d on