libretro/swanstation

Don't use floating-point for time

Opened this issue · 0 comments

The timer API in swanstation correctly stores time as an integer:

using Value = std::uint64_t;

But then provides APIs to convert it to and from floating-point:
static double ConvertValueToSeconds(Value value);
static double ConvertValueToMilliseconds(Value value);
static double ConvertValueToNanoseconds(Value value);
static Value ConvertSecondsToValue(double s);

The floating-point API is used in the code, here's one example:
const Common::Timer::Value current_time = Common::Timer::GetValue();
if (Common::Timer::ConvertValueToNanoseconds(current_time - last_command_time) < SPIN_TIME_NS)

This article has a mostly good writeup on why storing time in floating-point is bad:
https://randomascii.wordpress.com/2012/02/13/dont-store-that-in-a-float/

Keep in mind that most of the examples in the article use the float type in the context of game dev, which is typically single-precision floating-point IEEE754 (32-bit float). Swanstation uses double which is typically double-precision IEEE754 (64-bit float), so things are not as extreme as in the examples there.

The article also suggests using double as a possible fix, however, that isn't good enough for swanstation. For instance, it appears CLOCK_MONOTONIC is implemented as the time since boot at least on NetBSD and OpenBSD. I can't seem to find the relevant code on FreeBSD or Linux, but they should be similar. Considering every integer between -(2^53) and 2^53 can be stored without rounding in a 64-bit IEEE754 float, the time is acquired via CLOCK_MONOTONIC on Unix, and the unit is nanoseconds:

clock_gettime(CLOCK_MONOTONIC, &tv);
return ((Value)tv.tv_nsec + (Value)tv.tv_sec * 1000000000);

It takes 2^53 / 1000000000 seconds or 104 days of uptime for any conversion of Common::Timer to double to lose precision to rounding.

The separate Windows and Unix code can be mostly unified too, only GetValue() needs to be different.