diff --git a/Source/Core/Core/HW/DVD/DVDInterface.cpp b/Source/Core/Core/HW/DVD/DVDInterface.cpp index 5050cd906e..4a78fcb88b 100644 --- a/Source/Core/Core/HW/DVD/DVDInterface.cpp +++ b/Source/Core/Core/HW/DVD/DVDInterface.cpp @@ -174,6 +174,10 @@ static std::string s_disc_path_to_insert; static std::vector s_auto_disc_change_paths; static size_t s_auto_disc_change_index; +// Hacks +static bool s_disc_has_accurate_size; +static u64 s_guessed_disc_size; + // Events static CoreTiming::EventType* s_finish_executing_command; static CoreTiming::EventType* s_auto_change_disc; @@ -185,6 +189,8 @@ static void EjectDiscCallback(u64 userdata, s64 cyclesLate); static void InsertDiscCallback(u64 userdata, s64 cyclesLate); static void FinishExecutingCommandCallback(u64 userdata, s64 cycles_late); +static u64 GuessDiscSize(const DiscIO::VolumeDisc& disc); + void SetLidOpen(); void UpdateInterrupts(); @@ -420,6 +426,35 @@ void Shutdown() DVDThread::Stop(); } +static u64 GuessDiscSize(const DiscIO::VolumeDisc& disc) +{ + // Many Wii games intentionally try to read from an offset which is just past the end of a regular + // DVD but just before the end of a DVD-R, displaying "Error #001" and failing to boot if the read + // succeeds (see https://wiibrew.org/wiki//dev/di#0x8D_DVDLowUnencryptedRead for more details). + // + // We normally handle this by checking whether the DiscIO code considers the read to be out of + // bounds (this is done in DVDThread.cpp), but this doesn't work correctly for disc image formats + // that don't store the original size of the disc, most notably WBFS. Because of this, we have a + // hack here that guesses the original size of the disc if necessary. For Wii discs, there are + // two possibilities: single-layer discs (4.7 GiB) and dual-layer discs (7.93 GiB). Thankfully, + // 0x30-0x34 in BI2 tells us which disc type the game is expecting: 0x7ED40000 (0x1FB500000 >> 2) + // for games that expect dual-layer, and 0x00000000 for other games. The game uses this value + // when determining whether the attempted out of bounds read should be at 0x118280000 (just past + // the end of a of a single-layer DVD) or at 0x1FB500000 (just past the end of a dual-layer DVD). + // + // An alternative hack that also would work (which was used in Dolphin in the past) + // is to always reject DVDLowUnencryptedRead commands other than ones before 0x50000, + // since Wii games never do any such reads with the expectation that they will work. However, + // this must not be done for the raw Read command (which is used by GC games and CleanRip). + + if (disc.GetVolumeType() == DiscIO::Platform::GameCubeDisc) + return 0x57058000; + + const std::optional value = disc.ReadSwapped(0x440 + 0x30, disc.GetGamePartition()); + const bool dual_layer = value.has_value() && *value != 0; + return dual_layer ? 0x1FB4E0000 : 0x118240000; +} + void SetDisc(std::unique_ptr disc, std::optional> auto_disc_change_paths = {}) { @@ -439,6 +474,10 @@ void SetDisc(std::unique_ptr disc, if (had_disc != has_disc) ExpansionInterface::g_rtc_flags[ExpansionInterface::RTCFlag::DiscChanged] = true; + s_disc_has_accurate_size = has_disc && disc->IsSizeAccurate(); + if (has_disc && !s_disc_has_accurate_size) + s_guessed_disc_size = GuessDiscSize(*disc); + DVDThread::SetDisc(std::move(disc)); SetLidOpen(); @@ -748,6 +787,18 @@ bool ExecuteReadCommand(u64 dvd_offset, u32 output_address, u32 dvd_length, u32 dvd_length = output_length; } + if (SConfig::GetInstance().bWii && !s_disc_has_accurate_size && + partition == DiscIO::PARTITION_NONE) + { + WARN_LOG(DVDINTERFACE, "Unknown disc size, guessing %" PRIu64 " bytes", s_guessed_disc_size); + if (dvd_offset + dvd_length > s_guessed_disc_size) + { + SetHighError(DVDInterface::ERROR_BLOCK_OOB); + *interrupt_type = DIInterruptType::DEINT; + return false; + } + } + ScheduleReads(dvd_offset, dvd_length, partition, output_address, reply_type); return true; }