Skip to content

Commit

Permalink
Fix missing/incorrect checksums for Sega CD, Saturn, and Jaguar CD (s…
Browse files Browse the repository at this point in the history
…quashed 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`
  • Loading branch information
kalimag authored Sep 14, 2024
1 parent 1b3e7d4 commit e2a6942
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 113 deletions.
4 changes: 1 addition & 3 deletions src/BizHawk.Client.Common/RomLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,7 @@ private GameInfo MakeGameFromDisc(Disc disc, string ext, string name)
// 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,115 +168,12 @@ int GetFileSector(string filename, out int filesize)
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());
Expand Down
131 changes: 129 additions & 2 deletions src/BizHawk.Emulation.DiscSystem/DiscHasher.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#nullable enable

using System.Collections.Generic;
using System.Text;
using BizHawk.Common;
using BizHawk.Common.BufferExtensions;

Expand All @@ -12,6 +16,16 @@ public DiscHasher(Disc disc)

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>
Expand Down Expand Up @@ -87,18 +101,131 @@ public string OldHash()
{
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);

return MD5Checksum.ComputeDigestHex(buffer.AsSpan(start: 0, length: lba_len * 2352));
}
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;
}
}
}

0 comments on commit e2a6942

Please sign in to comment.