Daytona USA 2 High Scores

Technical discussion for those interested in Supermodel development and Model 3 reverse engineering. Prospective contributors welcome. Not for end-user support.
Forum rules
Keep it classy!
  • No ROM requests or links.
  • Do not ask to be a play tester.
  • Do not ask about release dates.
  • No drama!
MO-120FF
Posts: 13
Joined: Thu Mar 27, 2025 8:44 am

Re: Daytona USA 2 High Scores

Post by MO-120FF »

Interestingly when I look at the dump of some game predefined high-score times (e.g., Beginner Time Attack), the outcome of your algorithm matches the display 100% for every rank (though these scores don't have centiseconds).

Code: Select all

Framedivider (int) 	57			
hex	num_f	num_f_sub	total_sec	centi_sec	
1EAA	7850	41		137		71		2:17:71
1EBA	7866	0		138		0		2:18:0
1EBD	7869	3		138		5		2:18:5
1EC0	7872	6		138		10		2:18:10
1EC2	7874	8		138		14		2:18:14
					
3570	13680	0		240		0		4:0:0
3900	14592	0		256		0		4:16:0
3C90	15504	0		272		0		4:32:0
4020	16416	0		288		0		4:48:0
43B0	17328	0		304		0		5:4:0

Attachments
Dump-HS-TA.jpg
Dump-HS-TA.jpg (101.57 KiB) Viewed 64064 times
Bart
Site Admin
Posts: 176
Joined: Tue Nov 07, 2023 5:50 am

Re: Daytona USA 2 High Scores

Post by Bart »

Yeah this is really odd to me. It's *possible* that there is another routine hidden somewhere, or that the calculation happens in some non-straightforward fashion, but seems very improbable. I found the routine by looking for the magic constant 0x0000003C (60) in a disassembly of the entire program. I didn't actually try to identify where the scores are read out of NVRAM.

You may need to do some debugging using the Supermodel debugger.
Bart
Site Admin
Posts: 176
Joined: Tue Nov 07, 2023 5:50 am

Re: Daytona USA 2 High Scores

Post by Bart »

Something weird that I noticed is that it doesn't seem like either 0x4cff8 or 0x4d030 are ever called. I set these breakpoints and they're never hit. I haven't recorded my own lap times, so it's using the default values in NVRAM. Can you attach one of your NVRAM files?
Bart
Site Admin
Posts: 176
Joined: Tue Nov 07, 2023 5:50 am

Re: Daytona USA 2 High Scores

Post by Bart »

Ok, so I found that the NVRAM is read only once at boot-up and presumably the game keeps all data in RAM, writing through to NVRAM when something changes. The table in RAM has an entirely different format. The first entry for the beginner course (default: JUN 5:00:00) is here:

Code: Select all

00106f50h: 01 00 00 00 00 00 42 CC 00 00 00 00 4A 55 4E 00 ; ......BÌ....JUN.
00106f60h: 02 00 00 00 00 00 43 B0 00 00 00 00 4E 41 4F 00 ; ......C°....NAO.
00106f70h: 03 00 00 00 00 00 44 94 00 00 00 00 4B 41 5A 00 ; ......D”....KAZ.
00106f80h: 04 00 00 00 00 00 45 78 00 00 00 00 48 49 44 00 ; ......Ex....HID.
00106f90h: 05 00 00 00 00 00 46 5C 00 00 00 00 4B 59 4F 00 ; ......F\....KYO.
00106fa0h: 01 00 00 00 00 00 21 66 00 00 00 00 4E 49 53 00 ; ......!f....NIS.
00106fb0h: 01 00 00 00 00 00 42 CC 00 00 00 00 52 4E 47 00 ; ......BÌ....RNG.
00106fc0h: 02 00 00 00 00 00 43 B0 00 00 00 00 53 41 4F 00 ; ......C°....SAO.
00106fd0h: 03 00 00 00 00 00 44 94 00 00 00 00 49 53 41 00 ; ......D”....ISA.
00106fe0h: 04 00 00 00 00 00 45 78 00 00 00 00 53 41 54 00 ; ......Ex....SAT.
00106ff0h: 05 00 00 00 00 00 46 5C 00 00 00 00 4A 41 4D 00 ; ......F\....JAM.
00107000h: 01 00 00 00 00 00 21 66 00 00 00 00 4E 49 53 00 ; ......!f....NIS.
Address 0x00106f56 (which contains 0x42cc) is the time.

This is read as a 32-bit word from 0x00106f54 at PC=0x85120:

Code: Select all

0x00085120: 0x81500004	lwz	r10,0x04(r16) 		<-- this will load r10 with the value 0x42cc (5:00:00)
0x00085124: 0xC0300008	lfs	f1,0x08(r16)		<-- a floating point load, but the value is 0
0x00085128: 0x3821FFE0	addi	r1,r1,-0x20
0x0008512C: 0x90610000	stw	r3,0x00(r1)
0x00085130: 0xD8210008	stfd	f1,0x08(r1)
0x00085134: 0x386A0000	addi	r3,r10,0x00
0x00085138: 0xFC200890	fmr	f1,f1
0x0008513C: 0x4BFC8379	bl	0x0004D4B4		<-- need to check what this subroutine does
0x00085140: 0xC8210008	lfd	f1,0x08(r1)
0x00085144: 0x80610000	lwz	r3,0x00(r1)
0x00085148: 0x38210020	addi	r1,r1,0x20
0x0008514C: 0x3821FFE0	addi	r1,r1,-0x20
0x00085150: 0x90610000	stw	r3,0x00(r1)
0x00085154: 0x90810004	stw	r4,0x04(r1)
0x00085158: 0x90A10008	stw	r5,0x08(r1)
0x0008515C: 0x90C1000C	stw	r6,0x0C(r1)
0x00085160: 0x38400064	li	r2,0x00000064		<-- 0x64 = 100, probably the code for centiseconds
0x00085164: 0x7C6013D6	divw	r3,r0,r2
0x00085168: 0x7CC311D6	mullw	r6,r3,r2
0x0008516C: 0x7CC60050	sub	r6,r0,r6
0x00085170: 0x3840003C	li	r2,0x0000003C		<-- 0x3C = 60, (minutes or seconds)
0x00085174: 0x7C0313D6	divw	r0,r3,r2
0x00085178: 0x7CA011D6	mullw	r5,r0,r2
0x0008517C: 0x7CA51850	sub	r5,r3,r5
0x00085180: 0x7C6013D6	divw	r3,r0,r2
0x00085184: 0x7C8311D6	mullw	r4,r3,r2
0x00085188: 0x7C840050	sub	r4,r0,r4
0x0008518C: 0x90610010	stw	r3,0x10(r1)
0x00085190: 0x90810014	stw	r4,0x14(r1)
0x00085194: 0x90A10018	stw	r5,0x18(r1)
0x00085198: 0x90C1001C	stw	r6,0x1C(r1)
0x0008519C: 0x80610000	lwz	r3,0x00(r1)
0x000851A0: 0x80810004	lwz	r4,0x04(r1)
0x000851A4: 0x80A10008	lwz	r5,0x08(r1)
0x000851A8: 0x80C1000C	lwz	r6,0x0C(r1)
0x000851AC: 0x80010010	lwz	r0,0x10(r1)
0x000851B0: 0x80410014	lwz	r2,0x14(r1)
0x000851B4: 0x1C00003C	mulli	r0,r0,0x3C	<-- 60 used again
0x000851B8: 0x7C420214	add	r2,r2,r0
0x000851BC: 0x90410014	stw	r2,0x14(r1)
0x000851C0: 0x81010014	lwz	r8,0x14(r1)
0x000851C4: 0x80E10018	lwz	r7,0x18(r1)
0x000851C8: 0x80C1001C	lwz	r6,0x1C(r1)
0x000851CC: 0x38210020	addi	r1,r1,0x20
0x000851D0: 0x3930000C	addi	r9,r16,0x0C
0x000851D4: 0x3821FFE0	addi	r1,r1,-0x20
0x000851D8: 0x9041001C	stw	r2,0x1C(r1)
0x000851DC: 0x91210000	stw	r9,0x00(r1)
0x000851E0: 0x91010004	stw	r8,0x04(r1)
0x000851E4: 0x90E10008	stw	r7,0x08(r1)
0x000851E8: 0x90C1000C	stw	r6,0x0C(r1)
0x000851EC: 0x4BF83C81	bl	0x00008E6C
... lots more code follows ...
The subroutine at 0x4d4b4 is *very* interesting. I had to step through it with the debugger to see what it produces and I don't fully understand how. But, basically, it converts the integral frame count into a floating point number using a pretty weird set of operations. It then takes that and multiplies it by 100.0/57.0, which converts number of frames -> total centiseconds. It then casts that to an integer and returns it in register r0.

Code: Select all

; r3 = frame count
0x0004D4B4: 0x3821FFE0	addi	r1,r1,-0x20
0x0004D4B8: 0xD8210000	stfd	f1,0x00(r1)
0x0004D4BC: 0xD8410008	stfd	f2,0x08(r1)
0x0004D4C0: 0x3821FFF0	addi	r1,r1,-0x10
0x0004D4C4: 0x3C40000C	li	r2,0x000C0000
0x0004D4C8: 0xC0028440	lfs	f0,-0x7BC0(r2)      <-- some magic value into f2
0x0004D4CC: 0x6C608000	xori	r0,r3,0x80000000  <-- frame count | 0x80000000
0x0004D4D0: 0xD8010000	stfd	f0,0x00(r1)       <-- store 64-bit double to memory
0x0004D4D4: 0x90010004	stw	r0,0x04(r1)         <-- overwrite lower 32-bits with integer frame count (?!)
0x0004D4D8: 0xC8410000	lfd	f2,0x00(r1)         <-- load the 64-bit double back to f2
0x0004D4DC: 0xFC420028	fsub	f2,f2,f0          <-- this voodoo somehow produces the decimal value of frame count in f2!
0x0004D4E0: 0x38210010	addi	r1,r1,0x10
0x0004D4E4: 0xFC401018	frsp	f2,f2             <-- round to single precision
0x0004D4E8: 0xEC21102A	fadds	f1,f1,f2          <-- f1 is 0 before this point (but it may be some other offset?), so now f1 = frame_count
0x0004D4EC: 0x3C40000C	li	r2,0x000C0000
0x0004D4F0: 0xC0428444	lfs	f2,-0x7BBC(r2)      <-- f2 = 1.754386 (seems to be 100.0/57.0)
0x0004D4F4: 0xEC2100B2	fmuls	f1,f1,f2          <-- f1 = frame_count * (100.0/57.0)
0x0004D4F8: 0xFC00081C	fctiw	f0,f1             <-- f0 = int(f1)
0x0004D4FC: 0x3821FFF0	addi	r1,r1,-0x10
0x0004D500: 0x7C000FAE	stfiwx	f0,0,r1         <-- f0 (as integer) -> (r1)
0x0004D504: 0x80010000	lwz	r0,0x00(r1)         <-- r0 = f0 (as integer)
0x0004D508: 0x38210010	addi	r1,r1,0x10
0x0004D50C: 0xC8410008	lfd	f2,0x08(r1)
0x0004D510: 0xC8210000	lfd	f1,0x00(r1)
0x0004D514: 0x38210020	addi	r1,r1,0x20
0x0004D518: 0x4E800020	bclr	0x14,0
Resuming execution after the subroutine call, we can now figure out what is going on (remember that r0 holds total centiseconds elapsed):

Code: Select all

0x00085160: 0x38400064	li	r2,0x00000064		<-- 0x64 = 100
0x00085164: 0x7C6013D6	divw	r3,r0,r2			<-- r3 = total_centiseconds / 100 = whole_seconds 
0x00085168: 0x7CC311D6	mullw	r6,r3,r2		<-- r6 = whole_seconds * 100 = whole_seconds_in_centiseconds 
0x0008516C: 0x7CC60050	sub	r6,r0,r6			<-- r6 = total_centiseconds - whole_seconds_in_centiseconds = centiseconds (to display)
0x00085170: 0x3840003C	li	r2,0x0000003C		<-- 0x3C = 60, (minutes or seconds)
0x00085174: 0x7C0313D6	divw	r0,r3,r2			<-- r0 = whole_seconds / 60 = whole_minutes
0x00085178: 0x7CA011D6	mullw	r5,r0,r2		<-- r5 = whole_minutes* 60 = whole_minutes_in_seconds
0x0008517C: 0x7CA51850	sub	r5,r3,r5			<-- r5 = whole_seconds - whole_minutes_in_seconds = seconds (to display)
0x00085180: 0x7C6013D6	divw	r3,r0,r2			<-- r3 = whole_minutes/ 60 = whole_hours 
0x00085184: 0x7C8311D6	mullw	r4,r3,r2		<-- r4 = whole_hours * 60 = whole_hours_in_minutes
0x00085188: 0x7C840050	sub	r4,r0,r4			<-- r4 = whole_minutes - whole_hours_in_minutes = minutes (to display)
0x0008518C: 0x90610010	stw	r3,0x10(r1)
0x00085190: 0x90810014	stw	r4,0x14(r1)
0x00085194: 0x90A10018	stw	r5,0x18(r1)
0x00085198: 0x90C1001C	stw	r6,0x1C(r1)
0x0008519C: 0x80610000	lwz	r3,0x00(r1)
0x000851A0: 0x80810004	lwz	r4,0x04(r1)
0x000851A4: 0x80A10008	lwz	r5,0x08(r1)
0x000851A8: 0x80C1000C	lwz	r6,0x0C(r1)
0x000851AC: 0x80010010	lwz	r0,0x10(r1)
0x000851B0: 0x80410014	lwz	r2,0x14(r1)
0x000851B4: 0x1C00003C	mulli	r0,r0,0x3C	<-- 60 used again
...
In Python

Code: Select all

from typing import Tuple

nvram_frames = 0x1eba    #0x42cc # 0x2166

def compute_total_centiseconds(num_frames: int) -> int:
    return int(float(num_frames) * 100.0 / 57.0)

def compute_display_time(num_frames: int) -> Tuple[int, int, int]:
    total_centiseconds = compute_total_centiseconds(num_frames)
    whole_seconds = total_centiseconds // 100
    centiseconds = total_centiseconds - (whole_seconds * 100)
    whole_minutes = whole_seconds // 60
    seconds = whole_seconds - (60 * whole_minutes)
    whole_hours = whole_minutes // 60
    minutes = whole_minutes - (60 * whole_hours)
    return (minutes, seconds, centiseconds)

min, sec, csec = compute_display_time(num_frames=nvram_frames)

print(f"{min}:{sec:02d}:{csec:02d}")
I think this yields the same results as the integer version. Specifically, 0x1eba still comes out as 2:18:00 but I am now confident this is correct. To prove this, I overwrote one of the default times with 0x1eba and below is what I got (see the last entry, for NIS)!
Clipboard01.jpg
Clipboard01.jpg (97.64 KiB) Viewed 59232 times
So why does yours differ? Can you tell me which version of Supermodel you are using (the specific git revision)? I suspect there is some sort of rounding issue happening on your machine. There were some fixes to the PowerPC rounding code in the last few months, too.
MO-120FF
Posts: 13
Joined: Thu Mar 27, 2025 8:44 am

Re: Daytona USA 2 High Scores

Post by MO-120FF »

Thanks for your detailed analysis.

I'm using Supermodel Version 0.3a-git-c499584 and I've attached a logfile and the NV dump with the following times:

Beginner Track:
1 - ZER - 2:17:71
2 - ZER - 2:17:71
3 - ZER - 2:17:99
4 - ZER - 2:18:05
5 - ZER - 2:18:10
Attachments
daytona2_20250401.zip
(22.01 KiB) Downloaded 232 times
MO-120FF
Posts: 13
Joined: Thu Mar 27, 2025 8:44 am

Re: Daytona USA 2 High Scores

Post by MO-120FF »

I've updated to the latest Git version (12e3b6a), the displayed times in Daytona2 remain the same.

I also took a look at Daytona2PE (with the old Git version), and the displayed times are as follows:

Beginner Track:
1 - ZER - 2:17:989
2 - ZER - 2:18:012
3 - ZER - 2:18:349
4 - ZER - 2:18:741
5 - ZER - 2:19:196

The times in PE are in ms instead of centiseconds. When I apply your Python integer algorithm the outcome is like this:

Code: Select all

hex	num_f	num_f_sub	total_sec	centi_sec	
1EBA	7866	0		138		0	2:18:0
1EBB	7867	1		138		1	2:18:1
1ECE	7886	20		138		35	2:18:35
1EE5	7909	43		138		75	2:18:75
1EFF	7935	12		139		21	2:19:21
Seems that in PE the ms are stored separately somewhere. This could also be the case in the Battle on the Edge version.
Bart
Site Admin
Posts: 176
Joined: Tue Nov 07, 2023 5:50 am

Re: Daytona USA 2 High Scores

Post by Bart »

What sort of CPU do you have? I'm unfortunately going out of town for a couple weeks so may not have time to dig in any further. I'm quite confident the routine I found is the one that prints the times. I can't get the :99 centisecond value even using the exact same frame count you had in your NVRAM file.

Also, I don't see any of your times in the NVRAM file.

I included code I think for the PE algorithm, it computes milliseconds. It's just using 1000 instead of 100 when doing its calculation.
MO-120FF
Posts: 13
Joined: Thu Mar 27, 2025 8:44 am

Re: Daytona USA 2 High Scores

Post by MO-120FF »

The times in the NV Dump are at these offsets:

Code: Select all

Offset 22DE: 1EAA (Game displayed time: 2:17:71 / your algorithm results in: 2:17:71)
Offset 22FE: 1EAA (Game displayed time: 2:17:71 / your algorithm results in: 2:17:71)
Offset 231E: 1EBA (Game displayed time: 2:17:99 / your algorithm results in: 2:18:0)
Offset 233E: 1EBD (Game displayed time: 2:18:5 / your algorithm results in: 2:18:5)
Offset 235E: 1EC0 (Game displayed time: 2:18:10 / your algorithm results in: 2:18:10)
CPU: Intel Core i7-8700 / GPU: Nvidia 3060 Ti

My actual goal here is to get faster access to the high score times—since in attract mode it takes a few minutes until they appear. In Mame games, I usually just press the "Fast Forward (Insert)" key until the high scores show up. I’ve tried this in Supermodel (Alt + T), but it seems to always throttle to 60 FPS (and I don't see the FPS counter either), even when I apply the following settings (extract from log file) :

[Info] VSync=0
[Info] Throttle=0
[Info] RefreshRate=60
[Info] ShowFrameRate=1

Am I using the wrong settings here?

Thanks for your great help so far. Enjoy your time off! I think we're very close, I'll do some digging in the meantime.
Bart
Site Admin
Posts: 176
Joined: Tue Nov 07, 2023 5:50 am

Re: Daytona USA 2 High Scores

Post by Bart »

I don't think the settings matter. All the routines use a rough value of 57 fps and don't appear to be using measured frame timing. What I mean re: your NVRAM file is that when I run Daytona 2 with it, I don't see the scores. Shouldn't they show up for the beginner track after the attract mode?
MO-120FF
Posts: 13
Joined: Thu Mar 27, 2025 8:44 am

Re: Daytona USA 2 High Scores

Post by MO-120FF »

Hmm, interesting. It just tested the NV file from the zip (daytona2_20250401.nv) with a fresh Supermodel directory and waited until the attract mode showed the times from the beginner track (after about 7:30 min). The times show up like this:
D2_HighScore_20250401_D2.jpg
D2_HighScore_20250401_D2.jpg (236.22 KiB) Viewed 40710 times

In your case, do the default times show up?
Post Reply