Monday, April 21, 2014

Time Keeping

I've been chatting to Ade recently about the timing bugs present in OutRun. We initially discussed a timing bug that another blog reader, James Pearce submitted a fix for previously. Ade writes:

I believe the lap timing code in Out Run to be incorrect and would like your opinion on it. The timers are incremented every vertical blank so that should mean that seconds should increase by 1 every 60 vertical blanks.  I've looked at the original 68000 code and also your Cannonball source and it would appear that there's a bug in the code that handles the overflow from hundredths of seconds into full seconds.

I don't need to tell you that the 68000 code that increments the timers is at 0x007f4c in main cpu address space.  Specifically the bit of code that handles the overflow of hundredths to seconds is :-

007f58: cmpi.b   #$40, ($2,A1)       ; compare hundreds with 64 (should this be 60??)
007f5e: blt   $7f90                  ; branch if less than else reset hundredths and increment full seconds

Surely if the increment timers routine is called every vertical blank then it should be checking if hundredths are less than 60 and NOT less than 64?

I confirmed that this is indeed a bug in the Sega code. Ade looked into this further: 

I've been looking more and more into the timing issues and have come to the conclusion that Out Run timing is in quite a bad way and littered with bugs!

After doing the $40 to $3c alteration at $7f5b the timer still wasn't right, if I started a game with the dips set to normal (i.e. start time of 75 seconds on stage 1) the lap timer showed 1:18:50 when time ran out.  This was in comparison with 1:13:62 without the $40/$3c fix.

So I looked deeper into it and found another bug, this time in decrement_timers (0xb736).  The 68k here is :

subq.w #1, $60864.l            ; subtract 1 from the frame_counter
bge       $b760                ; branch if greater than or equal to zero

The Cannonball source is :-

if (--ostats.frame_counter >= 0)
    return false;

This is basically saying that the routine should still return false even if the frame_counter has reached zero, this is incorrect.  This means that the next time this routine is called the frame_counter (which is already at zero) will have another 1 subtracted from it, leaving a value of $ffff at $60864.w and it will have taken 31 iterations of this routine to reduce the time_counter by 1 whereby it should be 30 iterations.  The bge instruction should be a bgt instruction.  The Cannonball source should be '> 0' NOT '>= 0'.  By fixing this ($6c to $6e @ $b73c) I was getting closer to perfection!  Now when I started a game with 75 seconds on the timer the lap timer showed exactly 1:16:00 when time ran out.  The timing was now exactly 1 second out from where it should be.

The next bug I found was also in decrement_timers.  This time at the very end of the routine.  The Cannonball source is :-

return (ostats.time_counter < 0);

This should be <= 0 as, in its current guise, it will still count a whole second beyond the expiration of the timer before it returns a false value.  The 68k code uses the bcs (branch if carry set) instruction to test if the sbcd instruction has caused a carry by subtracting 1 from a value of 0.  To fix it in 68k requires a little more work but can be achieved by the fact that the sbcd instruction clears the Z flag if the result of the subtract operation is non-zero, otherwise it leaves it unchanged.  If the Z flag is set immediately before the sbcd instruction then it can be tested afterwards and if it's still set then we know that the result of the subtract was zero and can return a true value to indicate this.

I came up with this fix :-

00B74A: moveq #1, D1           ; replace moveq #0, D1 and addq.w #1, D1 with this single instruction
00B74C: move.w $60860.l, D0    ; move 16-bits from $60860 into D0 (make double sure that the sbcd instruction works correctly)
00B752: moveq #0, D5           ; trash D5 to set the Z flag in the CCR (D5 is unused so this is OK)
00B754: sbcd D1, D0            ; subtract D1 from D0 and set Z flag accordingly
00B756: beq $b764              ; branch if Z flag is still set after the sbcd instruction (counter has reached zero)

The instruction at $b74c (move.w $60860.l, D0) probably isn't needed and I could probably have got away with using the original instruction (move.b $60861.l, D0) as the sbcd instruction is byte-sized, not word-sized, but I erred on the side of caution, just to be double sure.

The above works well and I finally have a situation whereby I start a game with 75 seconds on the timer and the lap timer says 1:15:00 when time runs out!

There is a slight drawback though....  When the timer expires the game engine state changes from $c (GS_INGAME) to $f (GS_INIT_GAMEOVER).  This has the side effect of not updating the countdown timer at the top left of the screen (via the HUD display routine which checks the game engine state) and so it stays on 1 instead of being updated to 0.  So I need to alter the exit condition of the decrement_timers routine to set $60860.w to be 0 and then call draw_timer1 ($8216) before the game engine state is changed to $f, to force the game engine to update the HUD timer at the top left of the screen to zero.  I haven't done this yet, that's the next task!

I know that all your resource is currently tied up in Cannonboard but I was wondering whether you'd ever consider including fixes to the timing code in a future update to the enhanced roms.  I'm obviously intending on fixing up my own roms so that the total of all 5 laps exactly equals the overall game time when I cross the finish line, which will also agree to my stopwatch!

But I'm wondering how many other people out there are as bothered about it as I am?  It's a tough one to call as, like you say, the timing is so fundamental to Out Run and it's been wrong ever since day one so why bother to fix it now when people have got used to how it is.  It's a shame there's no more unused dip switches available where you could give people the option to toggle it fixed or original.

Now, we could fix these bugs in CannonBall. However, my original thinking was that having two timing systems would be confusing. I assumed players would want to compare times with the original machine and for a time to be consistent (even if it does not reflect the actual time you took to race the course!)

The only bug I have fixed relating to timing was a display bug with the EXTEND TIME overlay. What do you all think? 

1 comment:

Adrian said...

I'm Ade so I guess I'm qualified to comment on this issue ;-)

I intend to fix all the timing related bugs on my own Out Run board so that the overall race time agrees to my stopwatch at the end of the game. I do tend to agree with you that two parallel timings systems is not desirable and I'd imagine the majority of Out Run aficionados will choose NOT to implement this fix on their own boards. To facilitate this I'll be adding my patches to the enhanced roms but due to the fact that there aren't any unused dip switches left available I intend to repurpose the dip switch which the enhanced roms uses to toggle between mph and kmh for the purposes of toggling between original (bugged) timing and fixed (true) timing.