I recently posted a Twitter thread about this, but thought I'd share it here too

All code samples shown have been rewritten from decompiled code to make it more readable.
As recently discovered it turns out Rayman 1 calculates the times for the Magician bonus rounds incorrectly under certain circumstances. Did you know that a time of 12:1 is actually greater/slower than a time of 12:14?
I'll try and explain why in detail here. First of all we need to look at what the time displayed actually means. Is 12:1 the same as 12:10 or 12:01? By looking at the code we can see that it's the latter. If it's a single-digit value then it pads it with whitespace on the right side, so it becomes "1 ".
The second thing to note is that there is no conversion from frames to milliseconds for the value after the colon. This means that the timer actually displays in frames. The range for each second then becomes XX:00 - XX:59. The time difference between say 1:59 and 2:00 would then actually be around 16 ms, rather than 410 as you might first have expected. Counterintuitive, but not an actual bug (yet!).
With that out of the way let's look at the actual bugs. The problem is that if the time is XX:0 or XX:1 then it seems to be showing a lower time than it should be. To figure out why we need to look at the math involved here.
Let's break it up and look at each value separately. First we have the value for the amount of seconds. It's calculated like this:
Code: Select all
allowed_time - (left_time / 60 + 1)
allowed_time is the time limit in seconds and left_time is the remaining time in frames. Also all values are integers since the PS1 doesn't support floating points.
Dividing by 60 seems reasonable since we want to get seconds from frames, and the game runs in 60 fps, but why the +1? Well because we want to round up if it's a fraction of a second. Why round up and not down? It's because the left_time value is how much is left, not your actual time, so it's reversed from what you might first expect.
However there's a bug here. What if the time is not a fraction of a second, but rather XX:0? Then the +1 actually offsets it by 1 second! Since we're subtracting the left_time, this means we end up with 1 second less than it should be.
So we've now established that if the time is showing for example 12:00 then it's displaying 1 second less than it should, and the time is actually 13:00. But like I said before there is also an issue if the time is XX:1. How come?
Let's look at the second value, the amount of frames within the displaying second. This is using the map_time variable, the amount of frames you've been in the map, and doing mod 60 on it. This is all well and good, but how does map_time relate to left_time?
In the main game loop we have two functions, horloges and calc_left_time, that run each frame. Exactly how they work isn't important, but horloges increments map_time by 1 each frame and then calc_left_time sets the left_time value based on map_time, thus keeping them in sync.

- do_moteur.png (4.32 KiB) Viewed 562 times
But here comes the issue. When you finish a bonus level and get taken to the "PERFECT!" screen, then it exits the main game loop and enters another game loop called do_perfect_bonus - the same function that creates the timer text. Notice anything wrong here?
As you might have seen the do_perfect_bonus game loop calls horloges each frame, but does NOT call calc_left_time. This means map_time is going to be off by 1 frame from left_time! And this is exactly what causes a time of both XX:0 and XX:1 to be off by 1 second.
I wrote a small program to test this out, replicating the logic and adding 1 to map_time, and yep, same results!

- sample-results.png (1.56 KiB) Viewed 562 times
So here it is - thanks to 2 bugs in the code it turns out that the time displayed is off by 1 second if it ends with a single digit of 0 or 1! This was quite confusing and had us stumped for a while until we figured it out! This is thanks to the speedrunners originally noticing the times sometimes not matching that we decided to dig into this. I should also mention all of this is for the PS1 version, but I had a quick look at the PC code and it seems the same, so I assume this bug exists in all versions (besides Jaguar probably since that is coded entirely differently).