Fix missing/incorrect checksums for Sega CD, Saturn, and Jaguar CD (squashed PR #4024)

* Fix non-PSX disc hashing somewhat

- Don't hash generated (empty) lead-in/lead-out tracks
- Limit to track length, not absolute LBA of next track

* Use `InformationTrackCount` instead of `Tracks.Count`

* Add Jaguar CD hashing

Reuse RetroAchievements implementation, move into `DiscHasher`
This commit is contained in:
kalimag 2024-09-14 14:29:01 +02:00 committed by GitHub
parent 1b3e7d45f2
commit e2a6942df0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 135 additions and 113 deletions

View File

@ -234,9 +234,7 @@ namespace BizHawk.Client.Common
// TODO - use more sophisticated IDer
var discType = new DiscIdentifier(disc).DetectDiscType();
var discHasher = new DiscHasher(disc);
var discHash = discType == DiscType.SonyPSX
? discHasher.Calculate_PSX_BizIDHash()
: discHasher.OldHash();
var discHash = discHasher.CalculateBizHash(discType);
var game = Database.CheckDatabase(discHash);
if (game is not null) return game;

View File

@ -168,115 +168,12 @@ namespace BizHawk.Client.EmuHawk
buffer.AddRange(new ArraySegment<byte>(buf2048, 0, 512));
break;
case ConsoleID.JaguarCD:
// we want to hash the second session of the disc
if (disc.Sessions.Count > 2)
var discHasher = new DiscHasher(disc);
return discHasher.CalculateRAJaguarHash() switch
{
static string HashJaguar(DiscTrack bootTrack, DiscSectorReader dsr, bool commonHomebrewHash)
{
const string _jaguarHeader = "ATARI APPROVED DATA HEADER ATRI";
const string _jaguarBSHeader = "TARA IPARPVODED TA AEHDAREA RT";
var buffer = new List<byte>();
var buf2352 = new byte[2352];
// find the boot track header
// see https://github.com/TASEmulators/BizHawk/blob/f29113287e88c6a644dbff30f92a9833307aad20/waterbox/virtualjaguar/src/cdhle.cpp#L109-L145
var startLba = bootTrack.LBA;
var numLbas = bootTrack.NextTrack.LBA - bootTrack.LBA;
int bootLen = 0, bootLba = 0, bootOff = 0;
bool byteswapped = false, foundHeader = false;
var bootLenOffset = (commonHomebrewHash ? 0x40 : 0) + 32 + 4;
for (var i = 0; i < numLbas; i++)
{
dsr.ReadLBA_2352(startLba + i, buf2352, 0);
for (var j = 0; j < 2352 - bootLenOffset - 4; j++)
{
if (buf2352[j] == _jaguarHeader[0])
{
if (_jaguarHeader == Encoding.ASCII.GetString(buf2352, j, 32 - 1))
{
bootLen = (buf2352[j + bootLenOffset + 0] << 24) | (buf2352[j + bootLenOffset + 1] << 16) |
(buf2352[j + bootLenOffset + 2] << 8) | buf2352[j + bootLenOffset + 3];
bootLba = startLba + i;
bootOff = j + bootLenOffset + 4;
// byteswapped = false;
foundHeader = true;
break;
}
}
else if (buf2352[j] == _jaguarBSHeader[0])
{
if (_jaguarBSHeader == Encoding.ASCII.GetString(buf2352, j, 32 - 2))
{
bootLen = (buf2352[j + bootLenOffset + 1] << 24) | (buf2352[j + bootLenOffset + 0] << 16) |
(buf2352[j + bootLenOffset + 3] << 8) | buf2352[j + bootLenOffset + 2];
bootLba = startLba + i;
bootOff = j + bootLenOffset + 4;
byteswapped = true;
foundHeader = true;
break;
}
}
}
if (foundHeader)
{
break;
}
}
if (!foundHeader)
{
return null;
}
dsr.ReadLBA_2352(bootLba++, buf2352, 0);
if (byteswapped)
{
EndiannessUtils.MutatingByteSwap16(buf2352.AsSpan());
}
buffer.AddRange(new ArraySegment<byte>(buf2352, bootOff, Math.Min(2352 - bootOff, bootLen)));
bootLen -= 2352 - bootOff;
while (bootLen > 0)
{
dsr.ReadLBA_2352(bootLba++, buf2352, 0);
if (byteswapped)
{
EndiannessUtils.MutatingByteSwap16(buf2352.AsSpan());
}
buffer.AddRange(new ArraySegment<byte>(buf2352, 0, Math.Min(2352, bootLen)));
bootLen -= 2352;
}
return MD5Checksum.ComputeDigestHex(buffer.ToArray());
}
var jaguarHash = HashJaguar(disc.Sessions[2].Tracks[1], dsr, false);
switch (jaguarHash)
{
case null:
return 0;
case "254487B59AB21BC005338E85CBF9FD2F": // see https://github.com/RetroAchievements/rcheevos/pull/234
{
jaguarHash = HashJaguar(disc.Sessions[1].Tracks[2], dsr, true);
if (jaguarHash is null)
{
return 0;
}
break;
}
}
return IdentifyHash(jaguarHash);
}
return 0;
string jaguarHash => IdentifyHash(jaguarHash),
null => 0,
};
}
var hash = MD5Checksum.ComputeDigestHex(buffer.ToArray());

View File

@ -1,3 +1,7 @@
#nullable enable
using System.Collections.Generic;
using System.Text;
using BizHawk.Common;
using BizHawk.Common.BufferExtensions;
@ -12,6 +16,16 @@ namespace BizHawk.Emulation.DiscSystem
private readonly Disc disc;
public string CalculateBizHash(DiscType discType)
{
return discType switch
{
DiscType.SonyPSX => Calculate_PSX_BizIDHash(),
DiscType.JaguarCD => CalculateRAJaguarHash() ?? "",
_ => OldHash(),
};
}
/// <summary>
/// calculates the hash for quick PSX Disc identification
/// </summary>
@ -87,12 +101,15 @@ namespace BizHawk.Emulation.DiscSystem
{
var buffer = new byte[512 * 2352];
var dsr = new DiscSectorReader(disc);
foreach (var track in disc.Session1.Tracks)
// don't hash generated lead-in and lead-out tracks
for (int i = 1; i <= disc.Session1.InformationTrackCount; i++)
{
var track = disc.Session1.Tracks[i];
if (track.IsAudio)
continue;
var lba_len = Math.Min(track.NextTrack.LBA, 512);
var lba_len = Math.Min(track.NextTrack.LBA - track.LBA, 512);
for (var s = 0; s < 512 && s < lba_len; s++)
dsr.ReadLBA_2352(track.LBA + s, buffer, s * 2352);
@ -100,5 +117,115 @@ namespace BizHawk.Emulation.DiscSystem
}
return "no data track found";
}
/// <summary>
/// Calculate Jaguar CD hash according to RetroAchievements logic
/// </summary>
public string? CalculateRAJaguarHash()
{
if (disc.Sessions.Count <= 2)
{
return null;
}
var dsr = new DiscSectorReader(disc)
{
Policy = { DeterministicClearBuffer = false } // let's make this a little faster
};
static string? HashJaguar(DiscTrack bootTrack, DiscSectorReader dsr, bool commonHomebrewHash)
{
const string _jaguarHeader = "ATARI APPROVED DATA HEADER ATRI";
const string _jaguarBSHeader = "TARA IPARPVODED TA AEHDAREA RT";
var buffer = new List<byte>();
var buf2352 = new byte[2352];
// find the boot track header
// see https://github.com/TASEmulators/BizHawk/blob/f29113287e88c6a644dbff30f92a9833307aad20/waterbox/virtualjaguar/src/cdhle.cpp#L109-L145
var startLba = bootTrack.LBA;
var numLbas = bootTrack.NextTrack.LBA - bootTrack.LBA;
int bootLen = 0, bootLba = 0, bootOff = 0;
bool byteswapped = false, foundHeader = false;
var bootLenOffset = (commonHomebrewHash ? 0x40 : 0) + 32 + 4;
for (var i = 0; i < numLbas; i++)
{
dsr.ReadLBA_2352(startLba + i, buf2352, 0);
for (var j = 0; j < 2352 - bootLenOffset - 4; j++)
{
if (buf2352[j] == _jaguarHeader[0])
{
if (_jaguarHeader == Encoding.ASCII.GetString(buf2352, j, 32 - 1))
{
bootLen = (buf2352[j + bootLenOffset + 0] << 24) | (buf2352[j + bootLenOffset + 1] << 16) |
(buf2352[j + bootLenOffset + 2] << 8) | buf2352[j + bootLenOffset + 3];
bootLba = startLba + i;
bootOff = j + bootLenOffset + 4;
// byteswapped = false;
foundHeader = true;
break;
}
}
else if (buf2352[j] == _jaguarBSHeader[0])
{
if (_jaguarBSHeader == Encoding.ASCII.GetString(buf2352, j, 32 - 2))
{
bootLen = (buf2352[j + bootLenOffset + 1] << 24) | (buf2352[j + bootLenOffset + 0] << 16) |
(buf2352[j + bootLenOffset + 3] << 8) | buf2352[j + bootLenOffset + 2];
bootLba = startLba + i;
bootOff = j + bootLenOffset + 4;
byteswapped = true;
foundHeader = true;
break;
}
}
}
if (foundHeader)
{
break;
}
}
if (!foundHeader)
{
return null;
}
dsr.ReadLBA_2352(bootLba++, buf2352, 0);
if (byteswapped)
{
EndiannessUtils.MutatingByteSwap16(buf2352.AsSpan());
}
buffer.AddRange(new ArraySegment<byte>(buf2352, bootOff, Math.Min(2352 - bootOff, bootLen)));
bootLen -= 2352 - bootOff;
while (bootLen > 0)
{
dsr.ReadLBA_2352(bootLba++, buf2352, 0);
if (byteswapped)
{
EndiannessUtils.MutatingByteSwap16(buf2352.AsSpan());
}
buffer.AddRange(new ArraySegment<byte>(buf2352, 0, Math.Min(2352, bootLen)));
bootLen -= 2352;
}
return MD5Checksum.ComputeDigestHex(buffer.ToArray());
}
var jaguarHash = HashJaguar(disc.Sessions[2].Tracks[1], dsr, false);
if (jaguarHash is "254487B59AB21BC005338E85CBF9FD2F") // see https://github.com/RetroAchievements/rcheevos/pull/234
{
jaguarHash = HashJaguar(disc.Sessions[1].Tracks[2], dsr, true);
}
return jaguarHash;
}
}
}