diff --git a/Source/Core/Common/BitUtils.h b/Source/Core/Common/BitUtils.h index 79c1a7799e..538718f3aa 100644 --- a/Source/Core/Common/BitUtils.h +++ b/Source/Core/Common/BitUtils.h @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace Common @@ -299,4 +300,44 @@ void SetBit(T& value, size_t bit_number, bool bit_value) value &= ~(T{1} << bit_number); } +template +class FlagBit +{ +public: + FlagBit(std::underlying_type_t& bits, T bit) : m_bits(bits), m_bit(bit) {} + explicit operator bool() const + { + return (m_bits & static_cast>(m_bit)) != 0; + } + FlagBit& operator=(const bool rhs) + { + if (rhs) + m_bits |= static_cast>(m_bit); + else + m_bits &= ~static_cast>(m_bit); + return *this; + } + +private: + std::underlying_type_t& m_bits; + T m_bit; +}; + +template +class Flags +{ +public: + constexpr Flags() = default; + constexpr Flags(std::initializer_list bits) + { + for (auto bit : bits) + { + m_hex |= static_cast>(bit); + } + } + FlagBit operator[](T bit) { return FlagBit(m_hex, bit); } + + std::underlying_type_t m_hex = 0; +}; + } // namespace Common diff --git a/Source/Core/Common/ChunkFile.h b/Source/Core/Common/ChunkFile.h index aef3a2ec4a..d947339fcd 100644 --- a/Source/Core/Common/ChunkFile.h +++ b/Source/Core/Common/ChunkFile.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -144,6 +145,36 @@ public: Do(x.second); } + template + void Do(std::optional& x) + { + bool present = x.has_value(); + Do(present); + + switch (mode) + { + case MODE_READ: + if (present) + { + x = std::make_optional(); + Do(x.value()); + } + else + { + x = std::nullopt; + } + break; + + case MODE_WRITE: + case MODE_MEASURE: + case MODE_VERIFY: + if (present) + Do(x.value()); + + break; + } + } + template void DoArray(std::array& x) { diff --git a/Source/Core/Core/Analytics.cpp b/Source/Core/Core/Analytics.cpp index c5349fe28e..3b49173ed2 100644 --- a/Source/Core/Core/Analytics.cpp +++ b/Source/Core/Core/Analytics.cpp @@ -132,9 +132,16 @@ void DolphinAnalytics::ReportGameStart() } // Keep in sync with enum class GameQuirk definition. -constexpr std::array GAME_QUIRKS_NAMES{ +constexpr std::array GAME_QUIRKS_NAMES{ "icache-matters", "directly-reads-wiimote-input", + "uses-DVDLowStopLaser", + "uses-DVDLowOffset", + "uses-DVDLowReadDiskBca", + "uses-DVDLowRequestDiscStatus", + "uses-DVDLowRequestRetryNumber", + "uses-DVDLowSerMeasControl", + "uses-different-partition-command", }; static_assert(GAME_QUIRKS_NAMES.size() == static_cast(GameQuirk::COUNT), "Game quirks names and enum definition are out of sync."); diff --git a/Source/Core/Core/Analytics.h b/Source/Core/Core/Analytics.h index 98f26900b0..2d35029a46 100644 --- a/Source/Core/Core/Analytics.h +++ b/Source/Core/Core/Analytics.h @@ -29,6 +29,18 @@ enum class GameQuirk // "read" extension or IR data. This would break our current TAS/NetPlay implementation. DIRECTLY_READS_WIIMOTE_INPUT, + // Several Wii DI commands that are rarely/never used and not implemented by Dolphin + USES_DVD_LOW_STOP_LASER, + USES_DVD_LOW_OFFSET, + USES_DVD_LOW_READ_DISK_BCA, // NSMBW known to use this + USES_DVD_LOW_REQUEST_DISC_STATUS, + USES_DVD_LOW_REQUEST_RETRY_NUMBER, + USES_DVD_LOW_SER_MEAS_CONTROL, + + // Dolphin only implements the simple DVDLowOpenPartition, not any of the variants where some + // already-read data is provided + USES_DIFFERENT_PARTITION_COMMAND, + COUNT, }; diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp index 179afdaa92..a4f85208e3 100644 --- a/Source/Core/Core/Boot/Boot.cpp +++ b/Source/Core/Core/Boot/Boot.cpp @@ -244,6 +244,17 @@ bool CBoot::DVDRead(const DiscIO::VolumeDisc& disc, u64 dvd_offset, u32 output_a return true; } +bool CBoot::DVDReadDiscID(const DiscIO::VolumeDisc& disc, u32 output_address) +{ + std::array buffer; + if (!disc.Read(0, buffer.size(), buffer.data(), DiscIO::PARTITION_NONE)) + return false; + Memory::CopyToEmu(output_address, buffer.data(), buffer.size()); + // Clear ERROR_NO_DISKID_L, probably should check if that's currently set + DVDInterface::SetLowError(DVDInterface::ERROR_READY); + return true; +} + void CBoot::UpdateDebugger_MapLoaded() { Host_NotifyMapLoaded(); diff --git a/Source/Core/Core/Boot/Boot.h b/Source/Core/Core/Boot/Boot.h index c34e1738fe..48462c768d 100644 --- a/Source/Core/Core/Boot/Boot.h +++ b/Source/Core/Core/Boot/Boot.h @@ -104,6 +104,7 @@ public: private: static bool DVDRead(const DiscIO::VolumeDisc& disc, u64 dvd_offset, u32 output_address, u32 length, const DiscIO::Partition& partition); + static bool DVDReadDiscID(const DiscIO::VolumeDisc& disc, u32 output_address); static void RunFunction(u32 address); static void UpdateDebugger_MapLoaded(); diff --git a/Source/Core/Core/Boot/Boot_BS2Emu.cpp b/Source/Core/Core/Boot/Boot_BS2Emu.cpp index d73884df51..a85a939e79 100644 --- a/Source/Core/Core/Boot/Boot_BS2Emu.cpp +++ b/Source/Core/Core/Boot/Boot_BS2Emu.cpp @@ -19,8 +19,10 @@ #include "Core/ConfigManager.h" #include "Core/Core.h" #include "Core/HLE/HLE.h" +#include "Core/HW/DVD/DVDInterface.h" #include "Core/HW/EXI/EXI_DeviceIPL.h" #include "Core/HW/Memmap.h" +#include "Core/IOS/DI/DI.h" #include "Core/IOS/ES/ES.h" #include "Core/IOS/ES/Formats.h" #include "Core/IOS/FS/FileSystem.h" @@ -209,7 +211,22 @@ bool CBoot::EmulatedBS2_GC(const DiscIO::VolumeDisc& volume) SetupGCMemory(); - DVDRead(volume, /*offset*/ 0x00000000, /*address*/ 0x00000000, 0x20, DiscIO::PARTITION_NONE); + DVDReadDiscID(volume, 0x00000000); + + bool streaming = Memory::Read_U8(0x80000008); + if (streaming) + { + u8 streaming_size = Memory::Read_U8(0x80000009); + // If the streaming buffer size is 0, then BS2 uses a default size of 10 instead. + // No known game uses a size other than the default. + if (streaming_size == 0) + streaming_size = 10; + DVDInterface::AudioBufferConfig(true, streaming_size); + } + else + { + DVDInterface::AudioBufferConfig(false, 0); + } const bool ntsc = DiscIO::IsNTSC(SConfig::GetInstance().m_region); @@ -390,6 +407,9 @@ bool CBoot::EmulatedBS2_Wii(const DiscIO::VolumeDisc& volume) state->discstate = 0x01; }); + // The system menu clears the RTC flags + ExpansionInterface::g_rtc_flags.m_hex = 0; + // While reading a disc, the system menu reads the first partition table // (0x20 bytes from 0x00040020) and stores a pointer to the data partition entry. // When launching the disc game, it copies the partition type and offset to 0x3194 @@ -402,7 +422,13 @@ bool CBoot::EmulatedBS2_Wii(const DiscIO::VolumeDisc& volume) if (!SetupWiiMemory(console_type) || !IOS::HLE::GetIOS()->BootIOS(tmd.GetIOSId())) return false; - DVDRead(volume, 0x00000000, 0x00000000, 0x20, DiscIO::PARTITION_NONE); // Game Code + auto di = std::static_pointer_cast( + IOS::HLE::GetIOS()->GetDeviceByName("/dev/di")); + + di->InitializeIfFirstTime(); + di->ChangePartition(data_partition); + + DVDReadDiscID(volume, 0x00000000); // This is some kind of consistency check that is compared to the 0x00 // values as the game boots. This location keeps the 4 byte ID for as long diff --git a/Source/Core/Core/HW/DVD/DVDInterface.cpp b/Source/Core/Core/HW/DVD/DVDInterface.cpp index 97344187e5..5050cd906e 100644 --- a/Source/Core/Core/HW/DVD/DVDInterface.cpp +++ b/Source/Core/Core/HW/DVD/DVDInterface.cpp @@ -19,12 +19,14 @@ #include "Common/Config/Config.h" #include "Common/Logging/Log.h" +#include "Core/Analytics.h" #include "Core/Config/MainSettings.h" #include "Core/ConfigManager.h" #include "Core/CoreTiming.h" #include "Core/HW/AudioInterface.h" #include "Core/HW/DVD/DVDMath.h" #include "Core/HW/DVD/DVDThread.h" +#include "Core/HW/EXI/EXI_DeviceIPL.h" #include "Core/HW/MMIO.h" #include "Core/HW/Memmap.h" #include "Core/HW/ProcessorInterface.h" @@ -59,32 +61,6 @@ constexpr u64 BUFFER_TRANSFER_RATE = 32 * 1024 * 1024; namespace DVDInterface { -// "low" error codes -constexpr u32 ERROR_READY = 0x0000000; // Ready. -constexpr u32 ERROR_COVER_L = 0x01000000; // Cover is opened. -constexpr u32 ERROR_CHANGE_DISK = 0x02000000; // Disk change. -constexpr u32 ERROR_NO_DISK = 0x03000000; // No disk. -constexpr u32 ERROR_MOTOR_STOP_L = 0x04000000; // Motor stop. -constexpr u32 ERROR_NO_DISKID_L = 0x05000000; // Disk ID not read. - -// "high" error codes -constexpr u32 ERROR_NONE = 0x000000; // No error. -constexpr u32 ERROR_MOTOR_STOP_H = 0x020400; // Motor stopped. -constexpr u32 ERROR_NO_DISKID_H = 0x020401; // Disk ID not read. -constexpr u32 ERROR_COVER_H = 0x023a00; // Medium not present / Cover opened. -constexpr u32 ERROR_SEEK_NDONE = 0x030200; // No seek complete. -constexpr u32 ERROR_READ = 0x031100; // Unrecovered read error. -constexpr u32 ERROR_PROTOCOL = 0x040800; // Transfer protocol error. -constexpr u32 ERROR_INV_CMD = 0x052000; // Invalid command operation code. -constexpr u32 ERROR_AUDIO_BUF = 0x052001; // Audio Buffer not set. -constexpr u32 ERROR_BLOCK_OOB = 0x052100; // Logical block address out of bounds. -constexpr u32 ERROR_INV_FIELD = 0x052400; // Invalid field in command packet. -constexpr u32 ERROR_INV_AUDIO = 0x052401; // Invalid audio command. -constexpr u32 ERROR_INV_PERIOD = 0x052402; // Configuration out of permitted period. -constexpr u32 ERROR_END_USR_AREA = 0x056300; // End of user area encountered on this track. -constexpr u32 ERROR_MEDIUM = 0x062800; // Medium may have changed. -constexpr u32 ERROR_MEDIUM_REQ = 0x0b5a01; // Operator medium removal request. - // internal hardware addresses constexpr u32 DI_STATUS_REGISTER = 0x00; constexpr u32 DI_COVER_REGISTER = 0x04; @@ -97,23 +73,17 @@ constexpr u32 DI_DMA_CONTROL_REGISTER = 0x1C; constexpr u32 DI_IMMEDIATE_DATA_BUFFER = 0x20; constexpr u32 DI_CONFIG_REGISTER = 0x24; -// debug commands which may be ORd -constexpr u32 STOP_DRIVE = 0; -constexpr u32 START_DRIVE = 0x100; -constexpr u32 ACCEPT_COPY = 0x4000; -constexpr u32 DISC_CHECK = 0x8000; - // DI Status Register union UDISR { u32 Hex; struct { - u32 BREAK : 1; // Stop the Device + Interrupt - u32 DEINITMASK : 1; // Access Device Error Int Mask - u32 DEINT : 1; // Access Device Error Int - u32 TCINTMASK : 1; // Transfer Complete Int Mask - u32 TCINT : 1; // Transfer Complete Int + u32 BREAK : 1; // Stop the Device + Interrupt + u32 DEINTMASK : 1; // Access Device Error Int Mask + u32 DEINT : 1; // Access Device Error Int + u32 TCINTMASK : 1; // Transfer Complete Int Mask + u32 TCINT : 1; // Transfer Complete Int u32 BRKINTMASK : 1; u32 BRKINT : 1; // w 1: clear brkint u32 : 25; @@ -137,50 +107,6 @@ union UDICVR UDICVR(u32 _hex) { Hex = _hex; } }; -union UDICMDBUF -{ - u32 Hex; - struct - { - u8 CMDBYTE3; - u8 CMDBYTE2; - u8 CMDBYTE1; - u8 CMDBYTE0; - }; -}; - -// DI DMA Address Register -union UDIMAR -{ - u32 Hex; - struct - { - u32 Zerobits : 5; // Must be zero (32byte aligned) - u32 : 27; - }; - struct - { - u32 Address : 26; - u32 : 6; - }; -}; - -// DI DMA Address Length Register -union UDILENGTH -{ - u32 Hex; - struct - { - u32 Zerobits : 5; // Must be zero (32byte aligned) - u32 : 27; - }; - struct - { - u32 Length : 26; - u32 : 6; - }; -}; - // DI DMA Control Register union UDICR { @@ -194,18 +120,6 @@ union UDICR }; }; -union UDIIMMBUF -{ - u32 Hex; - struct - { - u8 REGVAL3; - u8 REGVAL2; - u8 REGVAL1; - u8 REGVAL0; - }; -}; - // DI Config Register union UDICFG { @@ -224,11 +138,11 @@ union UDICFG // Hardware registers static UDISR s_DISR; static UDICVR s_DICVR; -static UDICMDBUF s_DICMDBUF[3]; -static UDIMAR s_DIMAR; -static UDILENGTH s_DILENGTH; +static u32 s_DICMDBUF[3]; +static u32 s_DIMAR; +static u32 s_DILENGTH; static UDICR s_DICR; -static UDIIMMBUF s_DIIMMBUF; +static u32 s_DIIMMBUF; static UDICFG s_DICFG; static StreamADPCM::ADPCMDecoder s_adpcm_decoder; @@ -242,10 +156,12 @@ static u32 s_current_length; static u64 s_next_start; static u32 s_next_length; static u32 s_pending_samples; +static bool s_can_configure_dtk = true; +static bool s_enable_dtk = false; +static u8 s_dtk_buffer_length = 0; // TODO: figure out how this affects the regular buffer // Disc drive state static u32 s_error_code = 0; -static DiscIO::Partition s_current_partition; // Disc drive timing static u64 s_read_buffer_start_time; @@ -274,7 +190,6 @@ void SetLidOpen(); void UpdateInterrupts(); void GenerateDIInterrupt(DIInterruptType _DVDInterrupt); -void WriteImmediate(u32 value, u32 output_address, bool reply_to_ios); bool ExecuteReadCommand(u64 dvd_offset, u32 output_address, u32 dvd_length, u32 output_length, const DiscIO::Partition& partition, ReplyType reply_type, DIInterruptType* interrupt_type); @@ -303,9 +218,11 @@ void DoState(PointerWrap& p) p.Do(s_next_start); p.Do(s_next_length); p.Do(s_pending_samples); + p.Do(s_can_configure_dtk); + p.Do(s_enable_dtk); + p.Do(s_dtk_buffer_length); p.Do(s_error_code); - p.Do(s_current_partition); p.Do(s_read_buffer_start_time); p.Do(s_read_buffer_end_time); @@ -373,21 +290,31 @@ static u32 AdvanceDTK(u32 maximum_samples, u32* samples_to_process) return bytes_to_process; } -static void DTKStreamingCallback(const std::vector& audio_data, s64 cycles_late) +static void DTKStreamingCallback(DIInterruptType interrupt_type, const std::vector& audio_data, + s64 cycles_late) { - // Send audio to the mixer. - std::vector temp_pcm(s_pending_samples * 2, 0); - ProcessDTKSamples(&temp_pcm, audio_data); - g_sound_stream->GetMixer()->PushStreamingSamples(temp_pcm.data(), s_pending_samples); - // Determine which audio data to read next. static const int MAXIMUM_SAMPLES = 48000 / 2000 * 7; // 3.5ms of 48kHz samples u64 read_offset = 0; u32 read_length = 0; - if (s_stream && AudioInterface::IsPlaying()) + + if (interrupt_type == DIInterruptType::TCINT) { - read_offset = s_audio_position; - read_length = AdvanceDTK(MAXIMUM_SAMPLES, &s_pending_samples); + // Send audio to the mixer. + std::vector temp_pcm(s_pending_samples * 2, 0); + ProcessDTKSamples(&temp_pcm, audio_data); + g_sound_stream->GetMixer()->PushStreamingSamples(temp_pcm.data(), s_pending_samples); + + if (s_stream && AudioInterface::IsPlaying()) + { + read_offset = s_audio_position; + read_length = AdvanceDTK(MAXIMUM_SAMPLES, &s_pending_samples); + } + else + { + read_length = 0; + s_pending_samples = MAXIMUM_SAMPLES; + } } else { @@ -406,7 +333,7 @@ static void DTKStreamingCallback(const std::vector& audio_data, s64 cycles_l else { // There's nothing to read, so using DVDThread is unnecessary. - u64 userdata = PackFinishExecutingCommandUserdata(ReplyType::DTK, DIInterruptType::INT_TCINT); + u64 userdata = PackFinishExecutingCommandUserdata(ReplyType::DTK, DIInterruptType::TCINT); CoreTiming::ScheduleEvent(ticks_to_dtk, s_finish_executing_command, userdata); } } @@ -427,21 +354,23 @@ void Init() s_finish_executing_command = CoreTiming::RegisterEvent("FinishExecutingCommand", FinishExecutingCommandCallback); - u64 userdata = PackFinishExecutingCommandUserdata(ReplyType::DTK, DIInterruptType::INT_TCINT); + u64 userdata = PackFinishExecutingCommandUserdata(ReplyType::DTK, DIInterruptType::TCINT); CoreTiming::ScheduleEvent(0, s_finish_executing_command, userdata); } // This doesn't reset any inserted disc or the cover state. -void Reset() +void Reset(bool spinup) { + INFO_LOG(DVDINTERFACE, "Reset %s spinup", spinup ? "with" : "without"); + s_DISR.Hex = 0; - s_DICMDBUF[0].Hex = 0; - s_DICMDBUF[1].Hex = 0; - s_DICMDBUF[2].Hex = 0; - s_DIMAR.Hex = 0; - s_DILENGTH.Hex = 0; + s_DICMDBUF[0] = 0; + s_DICMDBUF[1] = 0; + s_DICMDBUF[2] = 0; + s_DIMAR = 0; + s_DILENGTH = 0; s_DICR.Hex = 0; - s_DIIMMBUF.Hex = 0; + s_DIIMMBUF = 0; s_DICFG.Hex = 0; s_DICFG.CONFIG = 1; // Disable bootrom descrambler @@ -453,16 +382,37 @@ void Reset() s_current_start = 0; s_current_length = 0; s_pending_samples = 0; + s_can_configure_dtk = true; + s_enable_dtk = false; + s_dtk_buffer_length = 0; - s_error_code = 0; + if (!IsDiscInside()) + { + // ERROR_COVER is used when the cover is open; + // ERROR_NO_DISK_L is used when the cover is closed but there is no disc. + // On the Wii, this can only happen if something other than a DVD is inserted into the disc + // drive (for instance, an audio CD) and only after it attempts to read it. Otherwise, it will + // report the cover as opened. + SetLowError(ERROR_COVER); + } + else if (!spinup) + { + // Wii hardware tests indicate that this is used when ejecting and inserting a new disc, or + // performing a reset without spinup. + SetLowError(ERROR_CHANGE_DISK); + } + else + { + SetLowError(ERROR_NO_DISKID_L); + } + + SetHighError(ERROR_NONE); // The buffer is empty at start s_read_buffer_start_offset = 0; s_read_buffer_end_offset = 0; s_read_buffer_start_time = 0; s_read_buffer_end_time = 0; - - s_disc_path_to_insert.clear(); } void Shutdown() @@ -473,8 +423,8 @@ void Shutdown() void SetDisc(std::unique_ptr disc, std::optional> auto_disc_change_paths = {}) { - if (disc) - s_current_partition = disc->GetGamePartition(); + bool had_disc = IsDiscInside(); + bool has_disc = static_cast(disc); if (auto_disc_change_paths) { @@ -485,8 +435,14 @@ void SetDisc(std::unique_ptr disc, s_auto_disc_change_index = 0; } + // Assume that inserting a disc requires having an empty disc before + if (had_disc != has_disc) + ExpansionInterface::g_rtc_flags[ExpansionInterface::RTCFlag::DiscChanged] = true; + DVDThread::SetDisc(std::move(disc)); SetLidOpen(); + + Reset(false); } bool IsDiscInside() @@ -517,9 +473,11 @@ static void InsertDiscCallback(u64 userdata, s64 cyclesLate) } // Must only be called on the CPU thread -void EjectDisc() +void EjectDisc(EjectCause cause) { CoreTiming::ScheduleEvent(0, s_eject_disc); + if (cause == EjectCause::User) + ExpansionInterface::g_rtc_flags[ExpansionInterface::RTCFlag::EjectButton] = true; } // Must only be called on the CPU thread @@ -545,7 +503,7 @@ void ChangeDisc(const std::string& new_path) return; } - EjectDisc(); + EjectDisc(EjectCause::User); s_disc_path_to_insert = new_path; CoreTiming::ScheduleEvent(SystemTimers::GetTicksPerSecond(), s_insert_disc); @@ -579,7 +537,7 @@ void SetLidOpen() u32 old_value = s_DICVR.CVR; s_DICVR.CVR = IsDiscInside() ? 0 : 1; if (s_DICVR.CVR != old_value) - GenerateDIInterrupt(INT_CVRINT); + GenerateDIInterrupt(DIInterruptType::CVRINT); } bool UpdateRunningGameMetadata(std::optional title_id) @@ -587,12 +545,8 @@ bool UpdateRunningGameMetadata(std::optional title_id) if (!DVDThread::HasDisc()) return false; - return DVDThread::UpdateRunningGameMetadata(s_current_partition, title_id); -} - -void ChangePartition(const DiscIO::Partition& partition) -{ - s_current_partition = partition; + return DVDThread::UpdateRunningGameMetadata(IOS::HLE::Device::DI::GetCurrentPartition(), + title_id); } void RegisterMMIO(MMIO::Mapping* mmio, u32 base) @@ -601,7 +555,7 @@ void RegisterMMIO(MMIO::Mapping* mmio, u32 base) MMIO::ComplexWrite([](u32, u32 val) { UDISR tmpStatusReg(val); - s_DISR.DEINITMASK = tmpStatusReg.DEINITMASK; + s_DISR.DEINTMASK = tmpStatusReg.DEINTMASK; s_DISR.TCINTMASK = tmpStatusReg.TCINTMASK; s_DISR.BRKINTMASK = tmpStatusReg.BRKINTMASK; s_DISR.BREAK = tmpStatusReg.BREAK; @@ -635,31 +589,32 @@ void RegisterMMIO(MMIO::Mapping* mmio, u32 base) UpdateInterrupts(); })); - // Command registers are very similar and we can register them with a - // simple loop. - for (int i = 0; i < 3; ++i) - mmio->Register(base | (DI_COMMAND_0 + 4 * i), MMIO::DirectRead(&s_DICMDBUF[i].Hex), - MMIO::DirectWrite(&s_DICMDBUF[i].Hex)); + // Command registers, which have no special logic + mmio->Register(base | DI_COMMAND_0, MMIO::DirectRead(&s_DICMDBUF[0]), + MMIO::DirectWrite(&s_DICMDBUF[0])); + mmio->Register(base | DI_COMMAND_1, MMIO::DirectRead(&s_DICMDBUF[1]), + MMIO::DirectWrite(&s_DICMDBUF[1])); + mmio->Register(base | DI_COMMAND_2, MMIO::DirectRead(&s_DICMDBUF[2]), + MMIO::DirectWrite(&s_DICMDBUF[2])); // DMA related registers. Mostly direct accesses (+ masking for writes to // handle things like address alignment) and complex write on the DMA // control register that will trigger the DMA. - mmio->Register(base | DI_DMA_ADDRESS_REGISTER, MMIO::DirectRead(&s_DIMAR.Hex), - MMIO::DirectWrite(&s_DIMAR.Hex, ~0xFC00001F)); - mmio->Register(base | DI_DMA_LENGTH_REGISTER, MMIO::DirectRead(&s_DILENGTH.Hex), - MMIO::DirectWrite(&s_DILENGTH.Hex, ~0x1F)); + mmio->Register(base | DI_DMA_ADDRESS_REGISTER, MMIO::DirectRead(&s_DIMAR), + MMIO::DirectWrite(&s_DIMAR, ~0x1F)); + mmio->Register(base | DI_DMA_LENGTH_REGISTER, MMIO::DirectRead(&s_DILENGTH), + MMIO::DirectWrite(&s_DILENGTH, ~0x1F)); mmio->Register(base | DI_DMA_CONTROL_REGISTER, MMIO::DirectRead(&s_DICR.Hex), MMIO::ComplexWrite([](u32, u32 val) { s_DICR.Hex = val & 7; if (s_DICR.TSTART) { - ExecuteCommand(s_DICMDBUF[0].Hex, s_DICMDBUF[1].Hex, s_DICMDBUF[2].Hex, - s_DIMAR.Hex, s_DILENGTH.Hex, false); + ExecuteCommand(ReplyType::Interrupt); } })); - mmio->Register(base | DI_IMMEDIATE_DATA_BUFFER, MMIO::DirectRead(&s_DIIMMBUF.Hex), - MMIO::DirectWrite(&s_DIIMMBUF.Hex)); + mmio->Register(base | DI_IMMEDIATE_DATA_BUFFER, MMIO::DirectRead(&s_DIIMMBUF), + MMIO::DirectWrite(&s_DIIMMBUF)); // DI config register is read only. mmio->Register(base | DI_CONFIG_REGISTER, MMIO::DirectRead(&s_DICFG.Hex), @@ -668,7 +623,7 @@ void RegisterMMIO(MMIO::Mapping* mmio, u32 base) void UpdateInterrupts() { - const bool set_mask = (s_DISR.DEINT & s_DISR.DEINITMASK) || (s_DISR.TCINT & s_DISR.TCINTMASK) || + const bool set_mask = (s_DISR.DEINT & s_DISR.DEINTMASK) || (s_DISR.TCINT & s_DISR.TCINTMASK) || (s_DISR.BRKINT & s_DISR.BRKINTMASK) || (s_DICVR.CVRINT & s_DICVR.CVRINTMASK); @@ -682,29 +637,91 @@ void GenerateDIInterrupt(DIInterruptType dvd_interrupt) { switch (dvd_interrupt) { - case INT_DEINT: - s_DISR.DEINT = 1; + case DIInterruptType::DEINT: + s_DISR.DEINT = true; break; - case INT_TCINT: - s_DISR.TCINT = 1; + case DIInterruptType::TCINT: + s_DISR.TCINT = true; break; - case INT_BRKINT: - s_DISR.BRKINT = 1; + case DIInterruptType::BRKINT: + s_DISR.BRKINT = true; break; - case INT_CVRINT: - s_DICVR.CVRINT = 1; + case DIInterruptType::CVRINT: + s_DICVR.CVRINT = true; break; } UpdateInterrupts(); } -void WriteImmediate(u32 value, u32 output_address, bool reply_to_ios) +void SetInterruptEnabled(DIInterruptType interrupt, bool enabled) { - if (reply_to_ios) - Memory::Write_U32(value, output_address); - else - s_DIIMMBUF.Hex = value; + switch (interrupt) + { + case DIInterruptType::DEINT: + s_DISR.DEINTMASK = enabled; + break; + case DIInterruptType::TCINT: + s_DISR.TCINTMASK = enabled; + break; + case DIInterruptType::BRKINT: + s_DISR.BRKINTMASK = enabled; + break; + case DIInterruptType::CVRINT: + s_DICVR.CVRINTMASK = enabled; + break; + } +} + +void ClearInterrupt(DIInterruptType interrupt) +{ + switch (interrupt) + { + case DIInterruptType::DEINT: + s_DISR.DEINT = false; + break; + case DIInterruptType::TCINT: + s_DISR.TCINT = false; + break; + case DIInterruptType::BRKINT: + s_DISR.BRKINT = false; + break; + case DIInterruptType::CVRINT: + s_DICVR.CVRINT = false; + break; + } +} + +// Checks the drive state to make sure a read-like command can be performed. +// If false is returned, SetHighError will have been called, and the caller +// should issue a DEINT interrupt. +static bool CheckReadPreconditions() +{ + if (!IsDiscInside()) // Implies ERROR_COVER or ERROR_NO_DISK + { + ERROR_LOG(DVDINTERFACE, "No disc inside."); + SetHighError(ERROR_NO_DISK_H); + return false; + } + if ((s_error_code & LOW_ERROR_MASK) == ERROR_CHANGE_DISK) + { + ERROR_LOG(DVDINTERFACE, "Disc changed (motor stopped)."); + SetHighError(ERROR_MEDIUM); + return false; + } + if ((s_error_code & LOW_ERROR_MASK) == ERROR_MOTOR_STOP_L) + { + ERROR_LOG(DVDINTERFACE, "Motor stopped."); + SetHighError(ERROR_MOTOR_STOP_H); + return false; + } + if ((s_error_code & LOW_ERROR_MASK) == ERROR_NO_DISKID_L) + { + ERROR_LOG(DVDINTERFACE, "Disc id not read."); + SetHighError(ERROR_NO_DISKID_H); + return false; + } + return true; } // Iff false is returned, ScheduleEvent must be used to finish executing the command @@ -712,17 +729,16 @@ bool ExecuteReadCommand(u64 dvd_offset, u32 output_address, u32 dvd_length, u32 const DiscIO::Partition& partition, ReplyType reply_type, DIInterruptType* interrupt_type) { - if (!IsDiscInside()) + if (!CheckReadPreconditions()) { // Disc read fails - s_error_code = ERROR_NO_DISK | ERROR_COVER_H; - *interrupt_type = INT_DEINT; + *interrupt_type = DIInterruptType::DEINT; return false; } else { // Disc read succeeds - *interrupt_type = INT_TCINT; + *interrupt_type = DIInterruptType::TCINT; } if (dvd_length > output_length) @@ -739,269 +755,235 @@ bool ExecuteReadCommand(u64 dvd_offset, u32 output_address, u32 dvd_length, u32 // When the command has finished executing, callback_event_type // will be called using CoreTiming::ScheduleEvent, // with the userdata set to the interrupt type. -void ExecuteCommand(u32 command_0, u32 command_1, u32 command_2, u32 output_address, - u32 output_length, bool reply_to_ios) +void ExecuteCommand(ReplyType reply_type) { - ReplyType reply_type = reply_to_ios ? ReplyType::IOS : ReplyType::Interrupt; - DIInterruptType interrupt_type = INT_TCINT; + DIInterruptType interrupt_type = DIInterruptType::TCINT; bool command_handled_by_thread = false; // DVDLowRequestError needs access to the error code set by the previous command - if (command_0 >> 24 != DVDLowRequestError) - s_error_code = 0; + if (static_cast(s_DICMDBUF[0] >> 24) != DICommand::RequestError) + SetHighError(0); - switch (command_0 >> 24) + switch (static_cast(s_DICMDBUF[0] >> 24)) { - // Seems to be used by both GC and Wii - case DVDLowInquiry: + // Used by both GC and Wii + case DICommand::Inquiry: // (shuffle2) Taken from my Wii - Memory::Write_U32(0x00000002, output_address); - Memory::Write_U32(0x20060526, output_address + 4); - // This was in the oubuf even though this cmd is only supposed to reply with 64bits - // However, this and other tests strongly suggest that the buffer is static, and it's never - - // or rarely cleared. - Memory::Write_U32(0x41000000, output_address + 8); + Memory::Write_U32(0x00000002, s_DIMAR); // Revision level, device code + Memory::Write_U32(0x20060526, s_DIMAR + 4); // Release date + Memory::Write_U32(0x41000000, s_DIMAR + 8); // Version - INFO_LOG(DVDINTERFACE, "DVDLowInquiry (Buffer 0x%08x, 0x%x)", output_address, output_length); + INFO_LOG(DVDINTERFACE, "DVDLowInquiry (Buffer 0x%08x, 0x%x)", s_DIMAR, s_DILENGTH); break; - // Only seems to be used from WII_IPC, not through direct access - case DVDLowReadDiskID: - INFO_LOG(DVDINTERFACE, "DVDLowReadDiskID"); - command_handled_by_thread = - ExecuteReadCommand(0, output_address, 0x20, output_length, DiscIO::PARTITION_NONE, - reply_type, &interrupt_type); - break; - - // Only used from WII_IPC. This is the only read command that decrypts data - case DVDLowRead: - INFO_LOG(DVDINTERFACE, "DVDLowRead: DVDAddr: 0x%09" PRIx64 ", Size: 0x%x", (u64)command_2 << 2, - command_1); - command_handled_by_thread = - ExecuteReadCommand((u64)command_2 << 2, output_address, command_1, output_length, - s_current_partition, reply_type, &interrupt_type); - break; - - // Probably only used by Wii - case DVDLowWaitForCoverClose: - INFO_LOG(DVDINTERFACE, "DVDLowWaitForCoverClose"); - interrupt_type = (DIInterruptType)4; // ??? - break; - - // "Set Extension"...not sure what it does. GC only? - case 0x55: + // GC-only patched drive firmware command, used by libogc + case DICommand::Unknown55: INFO_LOG(DVDINTERFACE, "SetExtension"); + SetHighError(ERROR_INV_CMD); + interrupt_type = DIInterruptType::DEINT; break; - // Probably only used though WII_IPC - case DVDLowGetCoverReg: - WriteImmediate(s_DICVR.Hex, output_address, reply_to_ios); - DEBUG_LOG(DVDINTERFACE, "DVDLowGetCoverReg 0x%08x", s_DICVR.Hex); - break; - - // Probably only used by Wii - case DVDLowNotifyReset: - ERROR_LOG(DVDINTERFACE, "DVDLowNotifyReset"); - PanicAlert("DVDLowNotifyReset"); - break; - // Probably only used by Wii - case DVDLowReadDvdPhysical: - ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdPhysical"); - PanicAlert("DVDLowReadDvdPhysical"); - break; - // Probably only used by Wii - case DVDLowReadDvdCopyright: - ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdCopyright"); - PanicAlert("DVDLowReadDvdCopyright"); - break; - // Probably only used by Wii - case DVDLowReadDvdDiscKey: - ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdDiscKey"); - PanicAlert("DVDLowReadDvdDiscKey"); - break; - - // Probably only used by Wii - case DVDLowClearCoverInterrupt: - DEBUG_LOG(DVDINTERFACE, "DVDLowClearCoverInterrupt"); - s_DICVR.CVRINT = 0; - break; - - // Probably only used by Wii - case DVDLowGetCoverStatus: - WriteImmediate(IsDiscInside() ? 2 : 1, output_address, reply_to_ios); - INFO_LOG(DVDINTERFACE, "DVDLowGetCoverStatus: Disc %sInserted", IsDiscInside() ? "" : "Not "); - break; - - // Probably only used by Wii - case DVDLowReset: - INFO_LOG(DVDINTERFACE, "DVDLowReset"); - break; - - // Probably only used by Wii - case DVDLowClosePartition: - INFO_LOG(DVDINTERFACE, "DVDLowClosePartition"); - break; - - // Probably only used by Wii - case DVDLowUnencryptedRead: - INFO_LOG(DVDINTERFACE, "DVDLowUnencryptedRead: DVDAddr: 0x%09" PRIx64 ", Size: 0x%x", - (u64)command_2 << 2, command_1); - - // We must make sure it is in a valid area! (#001 check) - // Are these checks correct? They seem to mix 32-bit offsets and 8-bit lengths - // * 0x00000000 - 0x00014000 (limit of older IOS versions) - // * 0x460a0000 - 0x460a0008 - // * 0x7ed40000 - 0x7ed40008 - if (((command_2 > 0x00000000 && command_2 < 0x00014000) || - (((command_2 + command_1) > 0x00000000) && (command_2 + command_1) < 0x00014000) || - (command_2 > 0x460a0000 && command_2 < 0x460a0008) || - (((command_2 + command_1) > 0x460a0000) && (command_2 + command_1) < 0x460a0008) || - (command_2 > 0x7ed40000 && command_2 < 0x7ed40008) || - (((command_2 + command_1) > 0x7ed40000) && (command_2 + command_1) < 0x7ed40008))) - { - command_handled_by_thread = - ExecuteReadCommand((u64)command_2 << 2, output_address, command_1, output_length, - DiscIO::PARTITION_NONE, reply_type, &interrupt_type); - } - else - { - WARN_LOG(DVDINTERFACE, "DVDLowUnencryptedRead: trying to read out of bounds @ %09" PRIx64, - (u64)command_2 << 2); - s_error_code = ERROR_READY | ERROR_BLOCK_OOB; - // Should cause software to call DVDLowRequestError - interrupt_type = INT_BRKINT; - } - - break; - - // Probably only used by Wii - case DVDLowEnableDvdVideo: - ERROR_LOG(DVDINTERFACE, "DVDLowEnableDvdVideo"); - break; - - // New Super Mario Bros. Wii sends these commands, - // but it seems we don't need to implement anything. - // Probably only used by Wii - case 0x95: - case 0x96: - ERROR_LOG(DVDINTERFACE, "Unimplemented BCA command 0x%08x (Buffer 0x%08x, 0x%x)", command_0, - output_address, output_length); - break; - - // Probably only used by Wii - case DVDLowReportKey: + // Wii-exclusive + case DICommand::ReportKey: INFO_LOG(DVDINTERFACE, "DVDLowReportKey"); // Does not work on retail discs/drives // Retail games send this command to see if they are running on real retail hw - s_error_code = ERROR_READY | ERROR_INV_CMD; - interrupt_type = INT_BRKINT; + SetHighError(ERROR_INV_CMD); + interrupt_type = DIInterruptType::DEINT; break; - // DMA Read from Disc. Only seems to be used through direct access, not WII_IPC - case 0xA8: - switch (command_0 & 0xFF) + // DMA Read from Disc. Only used through direct access on GC; direct use is prohibited by + // IOS (which uses it internally) + case DICommand::Read: + switch (s_DICMDBUF[0] & 0xFF) { case 0x00: // Read Sector { - u64 iDVDOffset = (u64)command_1 << 2; + u64 iDVDOffset = static_cast(s_DICMDBUF[1]) << 2; INFO_LOG(DVDINTERFACE, "Read: DVDOffset=%08" PRIx64 ", DMABuffer = %08x, SrcLength = %08x, DMALength = %08x", - iDVDOffset, output_address, command_2, output_length); + iDVDOffset, s_DIMAR, s_DICMDBUF[2], s_DILENGTH); + s_can_configure_dtk = false; command_handled_by_thread = - ExecuteReadCommand(iDVDOffset, output_address, command_2, output_length, - DiscIO::PARTITION_NONE, reply_type, &interrupt_type); + ExecuteReadCommand(iDVDOffset, s_DIMAR, s_DICMDBUF[2], s_DILENGTH, DiscIO::PARTITION_NONE, + reply_type, &interrupt_type); } break; case 0x40: // Read DiscID - INFO_LOG(DVDINTERFACE, "Read DiscID %08x", Memory::Read_U32(output_address)); - command_handled_by_thread = - ExecuteReadCommand(0, output_address, 0x20, output_length, DiscIO::PARTITION_NONE, - reply_type, &interrupt_type); + INFO_LOG(DVDINTERFACE, "Read DiscID: buffer %08x", s_DIMAR); + // TODO: It doesn't make sense to include ERROR_CHANGE_DISK here, as it implies that the drive + // is not spinning and reading the disc ID shouldn't change it. However, the Wii Menu breaks + // without it. + if ((s_error_code & LOW_ERROR_MASK) == ERROR_NO_DISKID_L || + (s_error_code & LOW_ERROR_MASK) == ERROR_CHANGE_DISK) + { + SetLowError(ERROR_READY); + } + else + { + // The first disc ID reading is required before DTK can be configured. + // If the disc ID is read again (or any other read occurs), it no longer can + // be configured. + s_can_configure_dtk = false; + } + + command_handled_by_thread = ExecuteReadCommand( + 0, s_DIMAR, 0x20, s_DILENGTH, DiscIO::PARTITION_NONE, reply_type, &interrupt_type); break; default: - ERROR_LOG(DVDINTERFACE, "Unknown read subcommand: %08x", command_0); + ERROR_LOG(DVDINTERFACE, "Unknown read subcommand: %08x", s_DICMDBUF[0]); break; } break; - // Seems to be used by both GC and Wii - case DVDLowSeek: + // Used by both GC and Wii + case DICommand::Seek: // Currently unimplemented - INFO_LOG(DVDINTERFACE, "Seek: offset=%09" PRIx64 " (ignoring)", (u64)command_1 << 2); + INFO_LOG(DVDINTERFACE, "Seek: offset=%09" PRIx64 " (ignoring)", + static_cast(s_DICMDBUF[1]) << 2); break; - // Probably only used by Wii - case DVDLowReadDvd: + // Wii-exclusive + case DICommand::ReadDVDMetadata: + switch ((s_DICMDBUF[0] >> 16) & 0xFF) + { + case 0: + ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdPhysical"); + break; + case 1: + ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdCopyright"); + break; + case 2: + ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdDiscKey"); + break; + default: + ERROR_LOG(DVDINTERFACE, "Unknown 0xAD subcommand in %08x", s_DICMDBUF[0]); + break; + } + SetHighError(ERROR_INV_CMD); + interrupt_type = DIInterruptType::DEINT; + break; + // Wii-exclusive + case DICommand::ReadDVD: ERROR_LOG(DVDINTERFACE, "DVDLowReadDvd"); + SetHighError(ERROR_INV_CMD); + interrupt_type = DIInterruptType::DEINT; break; - // Probably only used by Wii - case DVDLowReadDvdConfig: + // Wii-exclusive + case DICommand::ReadDVDConfig: ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdConfig"); + SetHighError(ERROR_INV_CMD); + interrupt_type = DIInterruptType::DEINT; break; - // Probably only used by Wii - case DVDLowStopLaser: + // Wii-exclusive + case DICommand::StopLaser: ERROR_LOG(DVDINTERFACE, "DVDLowStopLaser"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DVD_LOW_STOP_LASER); + SetHighError(ERROR_INV_CMD); + interrupt_type = DIInterruptType::DEINT; break; - // Probably only used by Wii - case DVDLowOffset: + // Wii-exclusive + case DICommand::Offset: ERROR_LOG(DVDINTERFACE, "DVDLowOffset"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DVD_LOW_OFFSET); + SetHighError(ERROR_INV_CMD); + interrupt_type = DIInterruptType::DEINT; break; - // Probably only used by Wii - case DVDLowReadDiskBca: - WARN_LOG(DVDINTERFACE, "DVDLowReadDiskBca"); - Memory::Write_U32(1, output_address + 0x30); + // Wii-exclusive + case DICommand::ReadBCA: + WARN_LOG(DVDINTERFACE, "DVDLowReadDiskBca - supplying dummy data to appease NSMBW"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DVD_LOW_READ_DISK_BCA); + // NSMBW checks that the first 0x33 bytes of the BCA are 0, then it expects a 1. + // Most (all?) other games have 0x34 0's at the start of the BCA, but don't actually + // read it. NSMBW doesn't care about the other 12 bytes (which contain manufacturing data?) + + // TODO: Read the .bca file that cleanrip generates, if it exists + // Memory::CopyToEmu(output_address, bca_data, 0x40); + Memory::Memset(s_DIMAR, 0, 0x40); + Memory::Write_U8(1, s_DIMAR + 0x33); break; - // Probably only used by Wii - case DVDLowRequestDiscStatus: + // Wii-exclusive + case DICommand::RequestDiscStatus: ERROR_LOG(DVDINTERFACE, "DVDLowRequestDiscStatus"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DVD_LOW_REQUEST_DISC_STATUS); + SetHighError(ERROR_INV_CMD); + interrupt_type = DIInterruptType::DEINT; break; - // Probably only used by Wii - case DVDLowRequestRetryNumber: + // Wii-exclusive + case DICommand::RequestRetryNumber: ERROR_LOG(DVDINTERFACE, "DVDLowRequestRetryNumber"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DVD_LOW_REQUEST_RETRY_NUMBER); + SetHighError(ERROR_INV_CMD); + interrupt_type = DIInterruptType::DEINT; break; - // Probably only used by Wii - case DVDLowSetMaximumRotation: + // Wii-exclusive + case DICommand::SetMaximumRotation: ERROR_LOG(DVDINTERFACE, "DVDLowSetMaximumRotation"); + SetHighError(ERROR_INV_CMD); + interrupt_type = DIInterruptType::DEINT; break; - // Probably only used by Wii - case DVDLowSerMeasControl: + // Wii-exclusive + case DICommand::SerMeasControl: ERROR_LOG(DVDINTERFACE, "DVDLowSerMeasControl"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DVD_LOW_SER_MEAS_CONTROL); + SetHighError(ERROR_INV_CMD); + interrupt_type = DIInterruptType::DEINT; break; // Used by both GC and Wii - case DVDLowRequestError: + case DICommand::RequestError: INFO_LOG(DVDINTERFACE, "Requesting error... (0x%08x)", s_error_code); - WriteImmediate(s_error_code, output_address, reply_to_ios); - s_error_code = 0; + s_DIIMMBUF = s_error_code; + SetHighError(0); break; - // Audio Stream (Immediate). Only seems to be used by some GC games + // Audio Stream (Immediate). Only used by some GC games, but does exist on the Wii // (command_0 >> 16) & 0xFF = Subcommand // command_1 << 2 = Offset on disc // command_2 = Length of the stream - case 0xE1: + case DICommand::AudioStream: { - u8 cancel_stream = (command_0 >> 16) & 0xFF; - if (cancel_stream) + if (!CheckReadPreconditions()) { - s_stop_at_track_end = false; - s_stream = false; + ERROR_LOG(DVDINTERFACE, "Cannot play audio (command %08x)", s_DICMDBUF[0]); + SetHighError(ERROR_AUDIO_BUF); + interrupt_type = DIInterruptType::DEINT; + break; } - else + if (!s_enable_dtk) { - if ((command_1 == 0) && (command_2 == 0)) + ERROR_LOG(DVDINTERFACE, + "Attempted to change playing audio while audio is disabled! (%08x %08x %08x)", + s_DICMDBUF[0], s_DICMDBUF[1], s_DICMDBUF[2]); + SetHighError(ERROR_AUDIO_BUF); + interrupt_type = DIInterruptType::DEINT; + break; + } + + s_can_configure_dtk = false; + + switch ((s_DICMDBUF[0] >> 16) & 0xFF) + { + case 0x00: + { + u64 offset = static_cast(s_DICMDBUF[1]) << 2; + u32 length = s_DICMDBUF[2]; + INFO_LOG(DVDINTERFACE, "(Audio) Start stream: offset: %08" PRIx64 " length: %08x", offset, + length); + + if ((offset == 0) && (length == 0)) { s_stop_at_track_end = true; } else if (!s_stop_at_track_end) { - s_next_start = static_cast(command_1) << 2; - s_next_length = command_2; + s_next_start = offset; + s_next_length = length; if (!s_stream) { s_current_start = s_next_start; @@ -1011,17 +993,35 @@ void ExecuteCommand(u32 command_0, u32 command_1, u32 command_2, u32 output_addr s_stream = true; } } + break; + } + case 0x01: + INFO_LOG(DVDINTERFACE, "(Audio) Stop stream"); + s_stop_at_track_end = false; + s_stream = false; + break; + default: + ERROR_LOG(DVDINTERFACE, "Invalid audio command! (%08x %08x %08x)", s_DICMDBUF[0], + s_DICMDBUF[1], s_DICMDBUF[2]); + SetHighError(ERROR_INV_AUDIO); + interrupt_type = DIInterruptType::DEINT; + break; } - - INFO_LOG(DVDINTERFACE, "(Audio) Stream cmd: %08x offset: %08" PRIx64 " length: %08x", command_0, - (u64)command_1 << 2, command_2); } break; - // Request Audio Status (Immediate). Only seems to be used by some GC games - case 0xE2: + // Request Audio Status (Immediate). Only used by some GC games, but does exist on the Wii + case DICommand::RequestAudioStatus: { - switch (command_0 >> 16 & 0xFF) + if (!s_enable_dtk) + { + ERROR_LOG(DVDINTERFACE, "Attempted to request audio status while audio is disabled!"); + SetHighError(ERROR_AUDIO_BUF); + interrupt_type = DIInterruptType::DEINT; + break; + } + + switch (s_DICMDBUF[0] >> 16 & 0xFF) { case 0x00: // Returns streaming status INFO_LOG(DVDINTERFACE, @@ -1030,38 +1030,42 @@ void ExecuteCommand(u32 command_0, u32 command_1, u32 command_2, u32 output_addr "CurrentStart:%08" PRIx64 " CurrentLength:%08x", s_audio_position, s_current_start + s_current_length, s_current_start, s_current_length); - WriteImmediate(s_stream ? 1 : 0, output_address, reply_to_ios); + s_DIIMMBUF = (s_stream ? 1 : 0); break; case 0x01: // Returns the current offset INFO_LOG(DVDINTERFACE, "(Audio): Stream Status: Request Audio status AudioPos:%08" PRIx64, s_audio_position); - WriteImmediate(static_cast((s_audio_position & 0xffffffffffff8000ull) >> 2), - output_address, reply_to_ios); + s_DIIMMBUF = static_cast((s_audio_position & 0xffffffffffff8000ull) >> 2); break; case 0x02: // Returns the start offset INFO_LOG(DVDINTERFACE, "(Audio): Stream Status: Request Audio status CurrentStart:%08" PRIx64, s_current_start); - WriteImmediate(static_cast(s_current_start >> 2), output_address, reply_to_ios); + s_DIIMMBUF = static_cast(s_current_start >> 2); break; case 0x03: // Returns the total length INFO_LOG(DVDINTERFACE, "(Audio): Stream Status: Request Audio status CurrentLength:%08x", s_current_length); - WriteImmediate(s_current_length, output_address, reply_to_ios); + s_DIIMMBUF = s_current_length; break; default: - INFO_LOG(DVDINTERFACE, "(Audio): Subcommand: %02x Request Audio status %s", - command_0 >> 16 & 0xFF, s_stream ? "on" : "off"); + ERROR_LOG(DVDINTERFACE, "Invalid audio status command! (%08x %08x %08x)", s_DICMDBUF[0], + s_DICMDBUF[1], s_DICMDBUF[2]); + SetHighError(ERROR_INV_AUDIO); + interrupt_type = DIInterruptType::DEINT; break; } } break; - case DVDLowStopMotor: + // Used by both GC and Wii + case DICommand::StopMotor: { - INFO_LOG(DVDINTERFACE, "DVDLowStopMotor %s %s", command_1 ? "eject" : "", - command_2 ? "kill!" : ""); + const bool eject = (s_DICMDBUF[0] & (1 << 17)); + const bool kill = (s_DICMDBUF[0] & (1 << 20)); + INFO_LOG(DVDINTERFACE, "DVDLowStopMotor%s%s", eject ? " eject" : "", kill ? " kill!" : ""); - const bool force_eject = command_1 && !command_2; + SetLowError(ERROR_MOTOR_STOP_L); + const bool force_eject = eject && !kill; if (Config::Get(Config::MAIN_AUTO_DISC_CHANGE) && !Movie::IsPlayingInput() && DVDThread::IsInsertedDiscRunning() && !s_auto_disc_change_paths.empty()) @@ -1072,50 +1076,59 @@ void ExecuteCommand(u32 command_0, u32 command_1, u32 command_2, u32 output_addr } else if (force_eject) { - EjectDiscCallback(0, 0); + EjectDisc(EjectCause::Software); } break; } - // DVD Audio Enable/Disable (Immediate). GC uses this, and apparently Wii also does...? - case DVDLowAudioBufferConfig: + // DVD Audio Enable/Disable (Immediate). GC uses this, and the Wii can use it to configure GC + // games. + case DICommand::AudioBufferConfig: // The IPL uses this command to enable or disable DTK audio depending on the value of byte 0x8 // in the disc header. See http://www.crazynation.org/GC/GC_DD_TECH/GCTech.htm for more info. // The link is dead, but you can access the page using the Wayback Machine at archive.org. - // TODO: Dolphin doesn't prevent the game from using DTK when the IPL doesn't enable it. - // Should we be failing with an error code when the game tries to use the 0xE1 command? - // (Not that this should matter normally, since games that use DTK set the header byte to 1) + // This command can only be used immediately after reading the disc ID, before any other + // reads. Too early, and you get ERROR_NO_DISKID. Too late, and you get ERROR_INV_PERIOD. + if (!s_can_configure_dtk) + { + ERROR_LOG(DVDINTERFACE, "Attempted to change DTK configuration after a read has been made!"); + SetHighError(ERROR_INV_PERIOD); + interrupt_type = DIInterruptType::DEINT; + break; + } - if ((command_0 >> 16) & 0xFF) - INFO_LOG(DVDINTERFACE, "DTK enabled"); - else - INFO_LOG(DVDINTERFACE, "DTK disabled"); + AudioBufferConfig((s_DICMDBUF[0] >> 16) & 1, s_DICMDBUF[0] & 0xf); break; - // yet another (GC?) command we prolly don't care about - case 0xEE: + // GC-only patched drive firmware command, used by libogc + case DICommand::UnknownEE: INFO_LOG(DVDINTERFACE, "SetStatus"); + SetHighError(ERROR_INV_CMD); + interrupt_type = DIInterruptType::DEINT; break; // Debug commands; see yagcd. We don't really care // NOTE: commands to stream data will send...a raw data stream // This will appear as unknown commands, unless the check is re-instated to catch such data. - // Can probably only be used through direct access - case 0xFE: - ERROR_LOG(DVDINTERFACE, "Unsupported DVD Drive debug command 0x%08x", command_0); + // Can only be used through direct access and only after unlocked. + case DICommand::Debug: + ERROR_LOG(DVDINTERFACE, "Unsupported DVD Drive debug command 0x%08x", s_DICMDBUF[0]); + SetHighError(ERROR_INV_CMD); + interrupt_type = DIInterruptType::DEINT; break; // Unlock Commands. 1: "MATSHITA" 2: "DVD-GAME" // Just for fun - // Can probably only be used through direct access - case 0xFF: + // Can only be used through direct access. The unlock command doesn't seem to work on the Wii. + case DICommand::DebugUnlock: { - if (command_0 == 0xFF014D41 && command_1 == 0x54534849 && command_2 == 0x54410200) + if (s_DICMDBUF[0] == 0xFF014D41 && s_DICMDBUF[1] == 0x54534849 && s_DICMDBUF[2] == 0x54410200) { INFO_LOG(DVDINTERFACE, "Unlock test 1 passed"); } - else if (command_0 == 0xFF004456 && command_1 == 0x442D4741 && command_2 == 0x4D450300) + else if (s_DICMDBUF[0] == 0xFF004456 && s_DICMDBUF[1] == 0x442D4741 && + s_DICMDBUF[2] == 0x4D450300) { INFO_LOG(DVDINTERFACE, "Unlock test 2 passed"); } @@ -1127,9 +1140,11 @@ void ExecuteCommand(u32 command_0, u32 command_1, u32 command_2, u32 output_addr break; default: - ERROR_LOG(DVDINTERFACE, "Unknown command 0x%08x (Buffer 0x%08x, 0x%x)", command_0, - output_address, output_length); - PanicAlertT("Unknown DVD command %08x - fatal error", command_0); + ERROR_LOG(DVDINTERFACE, "Unknown command 0x%08x (Buffer 0x%08x, 0x%x)", s_DICMDBUF[0], s_DIMAR, + s_DILENGTH); + PanicAlertT("Unknown DVD command %08x - fatal error", s_DICMDBUF[0]); + SetHighError(ERROR_INV_CMD); + interrupt_type = DIInterruptType::DEINT; break; } @@ -1142,6 +1157,35 @@ void ExecuteCommand(u32 command_0, u32 command_1, u32 command_2, u32 output_addr } } +void PerformDecryptingRead(u32 position, u32 length, u32 output_address, + const DiscIO::Partition& partition, ReplyType reply_type) +{ + DIInterruptType interrupt_type = DIInterruptType::TCINT; + s_can_configure_dtk = false; + + const bool command_handled_by_thread = + ExecuteReadCommand(static_cast(position) << 2, output_address, length, length, partition, + reply_type, &interrupt_type); + + if (!command_handled_by_thread) + { + // TODO: Needs testing to determine if COMMAND_LATENCY_US is accurate for this + CoreTiming::ScheduleEvent(COMMAND_LATENCY_US * (SystemTimers::GetTicksPerSecond() / 1000000), + s_finish_executing_command, + PackFinishExecutingCommandUserdata(reply_type, interrupt_type)); + } +} + +void AudioBufferConfig(bool enable_dtk, u8 dtk_buffer_length) +{ + s_enable_dtk = enable_dtk; + s_dtk_buffer_length = dtk_buffer_length; + if (s_enable_dtk) + INFO_LOG(DVDINTERFACE, "DTK enabled: buffer size %d", s_dtk_buffer_length); + else + INFO_LOG(DVDINTERFACE, "DTK disabled"); +} + u64 PackFinishExecutingCommandUserdata(ReplyType reply_type, DIInterruptType interrupt_type) { return (static_cast(reply_type) << 32) + static_cast(interrupt_type); @@ -1154,9 +1198,24 @@ void FinishExecutingCommandCallback(u64 userdata, s64 cycles_late) FinishExecutingCommand(reply_type, interrupt_type, cycles_late); } +void SetLowError(u32 low_error) +{ + DEBUG_ASSERT((low_error & HIGH_ERROR_MASK) == 0); + s_error_code = (s_error_code & HIGH_ERROR_MASK) | (low_error & LOW_ERROR_MASK); +} + +void SetHighError(u32 high_error) +{ + DEBUG_ASSERT((high_error & LOW_ERROR_MASK) == 0); + s_error_code = (s_error_code & LOW_ERROR_MASK) | (high_error & HIGH_ERROR_MASK); +} + void FinishExecutingCommand(ReplyType reply_type, DIInterruptType interrupt_type, s64 cycles_late, const std::vector& data) { + s_DIMAR += s_DILENGTH; + s_DILENGTH = 0; + switch (reply_type) { case ReplyType::NoReply: @@ -1169,7 +1228,6 @@ void FinishExecutingCommand(ReplyType reply_type, DIInterruptType interrupt_type if (s_DICR.TSTART) { s_DICR.TSTART = 0; - s_DILENGTH.Length = 0; GenerateDIInterrupt(interrupt_type); } break; @@ -1177,15 +1235,13 @@ void FinishExecutingCommand(ReplyType reply_type, DIInterruptType interrupt_type case ReplyType::IOS: { - auto di = IOS::HLE::GetIOS()->GetDeviceByName("/dev/di"); - if (di) - std::static_pointer_cast(di)->FinishIOCtl(interrupt_type); + IOS::HLE::Device::DI::InterruptFromDVDInterface(interrupt_type); break; } case ReplyType::DTK: { - DTKStreamingCallback(data, cycles_late); + DTKStreamingCallback(interrupt_type, data, cycles_late); break; } } diff --git a/Source/Core/Core/HW/DVD/DVDInterface.h b/Source/Core/Core/HW/DVD/DVDInterface.h index f7da0728c9..c0bd1bd4d0 100644 --- a/Source/Core/Core/HW/DVD/DVDInterface.h +++ b/Source/Core/Core/HW/DVD/DVDInterface.h @@ -24,46 +24,67 @@ class Mapping; namespace DVDInterface { -enum DICommand +enum class DICommand : u8 { - DVDLowInquiry = 0x12, - DVDLowReadDiskID = 0x70, - DVDLowRead = 0x71, - DVDLowWaitForCoverClose = 0x79, - DVDLowGetCoverReg = 0x7a, // DVDLowPrepareCoverRegister? - DVDLowNotifyReset = 0x7e, - DVDLowReadDvdPhysical = 0x80, - DVDLowReadDvdCopyright = 0x81, - DVDLowReadDvdDiscKey = 0x82, - DVDLowClearCoverInterrupt = 0x86, - DVDLowGetCoverStatus = 0x88, - DVDLowReset = 0x8a, - DVDLowOpenPartition = 0x8b, - DVDLowClosePartition = 0x8c, - DVDLowUnencryptedRead = 0x8d, - DVDLowEnableDvdVideo = 0x8e, - DVDLowReportKey = 0xa4, - DVDLowSeek = 0xab, - DVDLowReadDvd = 0xd0, - DVDLowReadDvdConfig = 0xd1, - DVDLowStopLaser = 0xd2, - DVDLowOffset = 0xd9, - DVDLowReadDiskBca = 0xda, - DVDLowRequestDiscStatus = 0xdb, - DVDLowRequestRetryNumber = 0xdc, - DVDLowSetMaximumRotation = 0xdd, - DVDLowSerMeasControl = 0xdf, - DVDLowRequestError = 0xe0, - DVDLowStopMotor = 0xe3, - DVDLowAudioBufferConfig = 0xe4 + Inquiry = 0x12, + ReportKey = 0xa4, + Read = 0xa8, + Seek = 0xab, + ReadDVDMetadata = 0xad, + ReadDVD = 0xd0, + ReadDVDConfig = 0xd1, + StopLaser = 0xd2, + Offset = 0xd9, + ReadBCA = 0xda, + RequestDiscStatus = 0xdb, + RequestRetryNumber = 0xdc, + SetMaximumRotation = 0xdd, + SerMeasControl = 0xdf, + RequestError = 0xe0, + AudioStream = 0xe1, + RequestAudioStatus = 0xe2, + StopMotor = 0xe3, + AudioBufferConfig = 0xe4, + Debug = 0xfe, + DebugUnlock = 0xff, + Unknown55 = 0x55, + UnknownEE = 0xee, }; -enum DIInterruptType : int +// "low" error codes +constexpr u32 ERROR_READY = 0x0000000; // Ready. +constexpr u32 ERROR_COVER = 0x01000000; // Cover is opened. +constexpr u32 ERROR_CHANGE_DISK = 0x02000000; // Disk change. +constexpr u32 ERROR_NO_DISK_L = 0x03000000; // No disk. +constexpr u32 ERROR_MOTOR_STOP_L = 0x04000000; // Motor stop. +constexpr u32 ERROR_NO_DISKID_L = 0x05000000; // Disk ID not read. +constexpr u32 LOW_ERROR_MASK = 0xff000000; + +// "high" error codes +constexpr u32 ERROR_NONE = 0x000000; // No error. +constexpr u32 ERROR_MOTOR_STOP_H = 0x020400; // Motor stopped. +constexpr u32 ERROR_NO_DISKID_H = 0x020401; // Disk ID not read. +constexpr u32 ERROR_NO_DISK_H = 0x023a00; // Medium not present / Cover opened. +constexpr u32 ERROR_SEEK_NDONE = 0x030200; // No seek complete. +constexpr u32 ERROR_READ = 0x031100; // Unrecovered read error. +constexpr u32 ERROR_PROTOCOL = 0x040800; // Transfer protocol error. +constexpr u32 ERROR_INV_CMD = 0x052000; // Invalid command operation code. +constexpr u32 ERROR_AUDIO_BUF = 0x052001; // Audio Buffer not set. +constexpr u32 ERROR_BLOCK_OOB = 0x052100; // Logical block address out of bounds. +constexpr u32 ERROR_INV_FIELD = 0x052400; // Invalid field in command packet. +constexpr u32 ERROR_INV_AUDIO = 0x052401; // Invalid audio command. +constexpr u32 ERROR_INV_PERIOD = 0x052402; // Configuration out of permitted period. +constexpr u32 ERROR_END_USR_AREA = 0x056300; // End of user area encountered on this track. +constexpr u32 ERROR_MEDIUM = 0x062800; // Medium may have changed. +constexpr u32 ERROR_MEDIUM_REQ = 0x0b5a01; // Operator medium removal request. +constexpr u32 HIGH_ERROR_MASK = 0x00ffffff; + +enum class DIInterruptType : int { - INT_DEINT = 0, - INT_TCINT = 1, - INT_BRKINT = 2, - INT_CVRINT = 3, + DEINT = 0, + TCINT = 1, + BRKINT = 2, + CVRINT = 3, }; enum class ReplyType : u32 @@ -71,11 +92,17 @@ enum class ReplyType : u32 NoReply, Interrupt, IOS, - DTK + DTK, +}; + +enum class EjectCause +{ + User, + Software, }; void Init(); -void Reset(); +void Reset(bool spinup = true); void Shutdown(); void DoState(PointerWrap& p); @@ -84,7 +111,7 @@ void RegisterMMIO(MMIO::Mapping* mmio, u32 base); void SetDisc(std::unique_ptr disc, std::optional> auto_disc_change_paths); bool IsDiscInside(); -void EjectDisc(); // Must only be called on the CPU thread +void EjectDisc(EjectCause cause); // Must only be called on the CPU thread void ChangeDisc(const std::vector& paths); // Must only be called on the CPU thread void ChangeDisc(const std::string& new_path); // Must only be called on the CPU thread bool AutoChangeDisc(); // Must only be called on the CPU thread @@ -97,12 +124,21 @@ bool UpdateRunningGameMetadata(std::optional title_id = {}); // Direct access to DI for IOS HLE (simpler to implement than how real IOS accesses DI, // and lets us skip encrypting/decrypting in some cases) -void ChangePartition(const DiscIO::Partition& partition); -void ExecuteCommand(u32 command_0, u32 command_1, u32 command_2, u32 output_address, - u32 output_length, bool reply_to_ios); +void ExecuteCommand(ReplyType reply_type); +void PerformDecryptingRead(u32 position, u32 length, u32 output_address, + const DiscIO::Partition& partition, ReplyType reply_type); +// Exposed for use by emulated BS2; does not perform any checks on drive state +void AudioBufferConfig(bool enable_dtk, u8 dtk_buffer_length); + +void SetLowError(u32 low_error); +void SetHighError(u32 high_error); // Used by DVDThread void FinishExecutingCommand(ReplyType reply_type, DIInterruptType interrupt_type, s64 cycles_late, const std::vector& data = std::vector()); +// Used by IOS HLE +void SetInterruptEnabled(DIInterruptType interrupt, bool enabled); +void ClearInterrupt(DIInterruptType interrupt); + } // end of namespace DVDInterface diff --git a/Source/Core/Core/HW/DVD/DVDThread.cpp b/Source/Core/Core/HW/DVD/DVDThread.cpp index a75f7a4306..5f28518a91 100644 --- a/Source/Core/Core/HW/DVD/DVDThread.cpp +++ b/Source/Core/Core/HW/DVD/DVDThread.cpp @@ -342,20 +342,34 @@ static void FinishRead(u64 id, s64 cycles_late) (CoreTiming::GetTicks() - request.time_started_ticks) / (SystemTimers::GetTicksPerSecond() / 1000000)); + DVDInterface::DIInterruptType interrupt; if (buffer.size() != request.length) { - PanicAlertT("The disc could not be read (at 0x%" PRIx64 " - 0x%" PRIx64 ").", - request.dvd_offset, request.dvd_offset + request.length); + if (request.dvd_offset != 0x118280000 && request.dvd_offset != 0x1FB500000) + { + PanicAlertT("The disc could not be read (at 0x%" PRIx64 " - 0x%" PRIx64 ").", + request.dvd_offset, request.dvd_offset + request.length); + } + else + { + // Part of the error 001 check. + INFO_LOG(DVDINTERFACE, "Ignoring out of bounds test read (at 0x%" PRIx64 " - 0x%" PRIx64 ")", + request.dvd_offset, request.dvd_offset + request.length); + } + + DVDInterface::SetHighError(DVDInterface::ERROR_BLOCK_OOB); + interrupt = DVDInterface::DIInterruptType::DEINT; } else { if (request.copy_to_ram) Memory::CopyToEmu(request.output_address, buffer.data(), request.length); + + interrupt = DVDInterface::DIInterruptType::TCINT; } // Notify the emulated software that the command has been executed - DVDInterface::FinishExecutingCommand(request.reply_type, DVDInterface::INT_TCINT, cycles_late, - buffer); + DVDInterface::FinishExecutingCommand(request.reply_type, interrupt, cycles_late, buffer); } static void DVDThread() diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp index 6839ec92ef..7ca1e007b3 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp +++ b/Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include "Core/HW/EXI/EXI_DeviceIPL.h" +#include "Core/HW/DVD/DVDInterface.h" #include #include @@ -43,6 +44,8 @@ static const char iplverPAL[0x100] = "(C) 1999-2001 Nintendo. All rights reserv static const char iplverNTSC[0x100] = "(C) 1999-2001 Nintendo. All rights reserved." "(C) 1999 ArtX Inc. All rights reserved."; +Common::Flags g_rtc_flags; + // bootrom descrambler reversed by segher // Copyright 2008 Segher Boessenkool void CEXIIPL::Descrambler(u8* data, u32 size) @@ -149,6 +152,7 @@ CEXIIPL::~CEXIIPL() void CEXIIPL::DoState(PointerWrap& p) { p.Do(g_SRAM.rtc); + p.Do(g_rtc_flags); p.Do(m_command); p.Do(m_command_bytes_received); p.Do(m_cursor); @@ -361,10 +365,12 @@ void CEXIIPL::TransferByte(u8& data) break; } } - else if (IN_RANGE(WII_RTC)) + else if (IN_RANGE(WII_RTC) && DEV_ADDR(WII_RTC) == 0x20) { - // Wii only RTC flags... afaik only the Wii Menu initializes it - // Seems to be 4bytes at dev_addr 0x20 + if (m_command.is_write()) + g_rtc_flags.m_hex = data; + else + data = g_rtc_flags.m_hex; } else if (IN_RANGE(EUART)) { diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceIPL.h b/Source/Core/Core/HW/EXI/EXI_DeviceIPL.h index 4e1c0d9c44..cfbf1f7a3b 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceIPL.h +++ b/Source/Core/Core/HW/EXI/EXI_DeviceIPL.h @@ -7,6 +7,7 @@ #include #include +#include "Common/BitUtils.h" #include "Core/HW/EXI/EXI_Device.h" class PointerWrap; @@ -78,4 +79,17 @@ private: static std::string FindIPLDump(const std::string& path_prefix); }; + +// Used to indicate disc changes on the Wii, as insane as that sounds. +// However, the name is definitely RTCFlag, as the code that gets it is __OSGetRTCFlags and +// __OSClearRTCFlags in OSRtc.o (based on symbols from Kirby's Dream Collection) +// This may simply be a single byte that gets repeated 4 times by some EXI quirk, +// as reading it gives the value repeated 4 times but code only checks the first bit. +enum class RTCFlag : u32 +{ + EjectButton = 0x01010101, + DiscChanged = 0x02020202, +}; + +extern Common::Flags g_rtc_flags; } // namespace ExpansionInterface diff --git a/Source/Core/Core/HW/WII_IPC.cpp b/Source/Core/Core/HW/WII_IPC.cpp index d231adcfd2..bf0aef3e53 100644 --- a/Source/Core/Core/HW/WII_IPC.cpp +++ b/Source/Core/Core/HW/WII_IPC.cpp @@ -8,6 +8,7 @@ #include "Common/CommonTypes.h" #include "Common/Logging/Log.h" #include "Core/CoreTiming.h" +#include "Core/HW/DVD/DVDInterface.h" #include "Core/HW/MMIO.h" #include "Core/HW/ProcessorInterface.h" #include "Core/IOS/IOS.h" @@ -97,7 +98,11 @@ static u32 ppc_irq_masks; static u32 arm_irq_flags; static u32 arm_irq_masks; -static u32 sensorbar_power; // do we need to care about this? +// Indicates which pins are accessible by broadway. Writable by starlet only. +static constexpr Common::Flags gpio_owner = {GPIO::SLOT_LED, GPIO::SLOT_IN, GPIO::SENSOR_BAR, + GPIO::DO_EJECT, GPIO::AVE_SCL, GPIO::AVE_SDA}; +static Common::Flags gpio_dir; +Common::Flags g_gpio_out; static CoreTiming::EventType* updateInterrupts; static void UpdateInterrupts(u64 = 0, s64 cyclesLate = 0); @@ -111,7 +116,7 @@ void DoState(PointerWrap& p) p.Do(ppc_irq_masks); p.Do(arm_irq_flags); p.Do(arm_irq_masks); - p.Do(sensorbar_power); + p.Do(g_gpio_out); } static void InitState() @@ -125,7 +130,9 @@ static void InitState() arm_irq_flags = 0; arm_irq_masks = 0; - sensorbar_power = 0; + // The only input broadway has is SLOT_IN; all the others it has access to are outputs + gpio_dir = {GPIO::SLOT_LED, GPIO::SENSOR_BAR, GPIO::DO_EJECT, GPIO::AVE_SCL, GPIO::AVE_SDA}; + g_gpio_out = {}; ppc_irq_masks |= INT_CAUSE_IPC_BROADWAY; } @@ -181,14 +188,29 @@ void RegisterMMIO(MMIO::Mapping* mmio, u32 base) CoreTiming::ScheduleEvent(0, updateInterrupts, 0); })); - mmio->Register(base | GPIOB_OUT, MMIO::Constant(0), - MMIO::DirectWrite(&sensorbar_power)); + mmio->Register(base | GPIOB_OUT, MMIO::DirectRead(&g_gpio_out.m_hex), + MMIO::ComplexWrite([](u32, u32 val) { + g_gpio_out.m_hex = val & gpio_owner.m_hex; + if (g_gpio_out[GPIO::DO_EJECT]) + { + INFO_LOG(WII_IPC, "Ejecting disc due to GPIO write"); + DVDInterface::EjectDisc(DVDInterface::EjectCause::Software); + } + // SENSOR_BAR is checked by WiimoteEmu::CameraLogic + // TODO: AVE, SLOT_LED + })); + mmio->Register(base | GPIOB_DIR, MMIO::DirectRead(&gpio_dir.m_hex), + MMIO::DirectWrite(&gpio_dir.m_hex)); + mmio->Register(base | GPIOB_IN, MMIO::ComplexRead([](u32) { + Common::Flags gpio_in; + gpio_in[GPIO::SLOT_IN] = DVDInterface::IsDiscInside(); + return gpio_in.m_hex; + }), + MMIO::Nop()); // Register some stubbed/unknown MMIOs required to make Wii games work. mmio->Register(base | PPCSPEED, MMIO::InvalidRead(), MMIO::Nop()); mmio->Register(base | VISOLID, MMIO::InvalidRead(), MMIO::Nop()); - mmio->Register(base | GPIOB_DIR, MMIO::Constant(0), MMIO::Nop()); - mmio->Register(base | GPIOB_IN, MMIO::Constant(0), MMIO::Nop()); mmio->Register(base | UNK_180, MMIO::Constant(0), MMIO::Nop()); mmio->Register(base | UNK_1CC, MMIO::Constant(0), MMIO::Nop()); mmio->Register(base | UNK_1D0, MMIO::Constant(0), MMIO::Nop()); diff --git a/Source/Core/Core/HW/WII_IPC.h b/Source/Core/Core/HW/WII_IPC.h index 34419b8988..f13d824f26 100644 --- a/Source/Core/Core/HW/WII_IPC.h +++ b/Source/Core/Core/HW/WII_IPC.h @@ -4,6 +4,7 @@ #pragma once +#include "Common/BitUtils.h" #include "Common/CommonTypes.h" class PointerWrap; @@ -35,6 +36,36 @@ enum StarletInterruptCause INT_CAUSE_IPC_STARLET = 0x80000000 }; +enum class GPIO : u32 +{ + POWER = 0x1, + SHUTDOWN = 0x2, + FAN = 0x4, + DC_DC = 0x8, + DI_SPIN = 0x10, + SLOT_LED = 0x20, + EJECT_BTN = 0x40, + SLOT_IN = 0x80, + SENSOR_BAR = 0x100, + DO_EJECT = 0x200, + EEP_CS = 0x400, + EEP_CLK = 0x800, + EEP_MOSI = 0x1000, + EEP_MISO = 0x2000, + AVE_SCL = 0x4000, + AVE_SDA = 0x8000, + DEBUG0 = 0x10000, + DEBUG1 = 0x20000, + DEBUG2 = 0x40000, + DEBUG3 = 0x80000, + DEBUG4 = 0x100000, + DEBUG5 = 0x200000, + DEBUG6 = 0x400000, + DEBUG7 = 0x800000, +}; + +extern Common::Flags g_gpio_out; + void Init(); void Reset(); void Shutdown(); diff --git a/Source/Core/Core/HW/WiimoteEmu/Camera.cpp b/Source/Core/Core/HW/WiimoteEmu/Camera.cpp index 10966ce2a2..25b542bc8a 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Camera.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/Camera.cpp @@ -12,6 +12,7 @@ #include "Common/MathUtil.h" #include "Common/Matrix.h" +#include "Core/HW/WII_IPC.h" #include "Core/HW/WiimoteCommon/WiimoteReport.h" namespace WiimoteEmu @@ -103,23 +104,31 @@ void CameraLogic::Update(const Common::Matrix44& transform) std::array camera_points; - std::transform(leds.begin(), leds.end(), camera_points.begin(), [&](const Vec3& v) { - const auto point = camera_view * Vec4(v, 1.0); + if (IOS::g_gpio_out[IOS::GPIO::SENSOR_BAR]) + { + std::transform(leds.begin(), leds.end(), camera_points.begin(), [&](const Vec3& v) { + const auto point = camera_view * Vec4(v, 1.0); - if (point.z > 0) - { - // FYI: Casting down vs. rounding seems to produce more symmetrical output. - const auto x = s32((1 - point.x / point.w) * CAMERA_WIDTH / 2); - const auto y = s32((1 - point.y / point.w) * CAMERA_HEIGHT / 2); + if (point.z > 0) + { + // FYI: Casting down vs. rounding seems to produce more symmetrical output. + const auto x = s32((1 - point.x / point.w) * CAMERA_WIDTH / 2); + const auto y = s32((1 - point.y / point.w) * CAMERA_HEIGHT / 2); - const auto point_size = std::lround(MAX_POINT_SIZE / point.w / 2); + const auto point_size = std::lround(MAX_POINT_SIZE / point.w / 2); - if (x >= 0 && y >= 0 && x < CAMERA_WIDTH && y < CAMERA_HEIGHT) - return CameraPoint{u16(x), u16(y), u8(point_size)}; - } + if (x >= 0 && y >= 0 && x < CAMERA_WIDTH && y < CAMERA_HEIGHT) + return CameraPoint{u16(x), u16(y), u8(point_size)}; + } - return INVISIBLE_POINT; - }); + return INVISIBLE_POINT; + }); + } + else + { + // Sensor bar is off + camera_points.fill(INVISIBLE_POINT); + } // IR data is read from offset 0x37 on real hardware auto& data = reg_data.camera_data; diff --git a/Source/Core/Core/IOS/DI/DI.cpp b/Source/Core/Core/IOS/DI/DI.cpp index 21a4f71e46..5a53e76446 100644 --- a/Source/Core/Core/IOS/DI/DI.cpp +++ b/Source/Core/Core/IOS/DI/DI.cpp @@ -13,15 +13,37 @@ #include "Common/CommonTypes.h" #include "Common/Logging/Log.h" #include "Common/MsgHandler.h" +#include "Core/Analytics.h" +#include "Core/CoreTiming.h" #include "Core/HW/DVD/DVDInterface.h" #include "Core/HW/DVD/DVDThread.h" +#include "Core/HW/MMIO.h" #include "Core/HW/Memmap.h" #include "Core/IOS/ES/ES.h" #include "Core/IOS/ES/Formats.h" #include "DiscIO/Volume.h" +template +class RegisterWrapper +{ +public: + operator u32() const { return Memory::mmio_mapping->Read(addr); } + void operator=(u32 rhs) { Memory::mmio_mapping->Write(addr, rhs); } +}; +static RegisterWrapper<0x0D806000> DISR; +static RegisterWrapper<0x0D806004> DICVR; +static RegisterWrapper<0x0D806008> DICMDBUF0; +static RegisterWrapper<0x0D80600C> DICMDBUF1; +static RegisterWrapper<0x0D806010> DICMDBUF2; +static RegisterWrapper<0x0D806014> DIMAR; +static RegisterWrapper<0x0D806018> DILENGTH; +static RegisterWrapper<0x0D80601C> DICR; +static RegisterWrapper<0x0D806020> DIIMMBUF; + namespace IOS::HLE::Device { +CoreTiming::EventType* DI::s_finish_executing_di_command; + DI::DI(Kernel& ios, const std::string& device_name) : Device(ios, device_name) { } @@ -30,40 +52,50 @@ void DI::DoState(PointerWrap& p) { DoStateShared(p); p.Do(m_commands_to_execute); + p.Do(m_executing_command); + p.Do(m_current_partition); + p.Do(m_has_initialized); + p.Do(m_last_length); +} + +IPCCommandResult DI::Open(const OpenRequest& request) +{ + InitializeIfFirstTime(); + return Device::Open(request); } IPCCommandResult DI::IOCtl(const IOCtlRequest& request) { - // DI IOCtls are handled in a special way by Dolphin - // compared to other IOS functions. - // This is a wrapper around DVDInterface's ExecuteCommand, - // which will execute commands more or less asynchronously. - // Only one command can be executed at a time, so commands - // are queued until DVDInterface is ready to handle them. + InitializeIfFirstTime(); - bool ready_to_execute = m_commands_to_execute.empty(); + // DI IOCtls are handled in a special way by Dolphin compared to other IOS functions. + // Many of them use DVDInterface's ExecuteCommand, which will execute commands more or less + // asynchronously. The rest are also treated as async to match this. Only one command can be + // executed at a time, so commands are queued until DVDInterface is ready to handle them. + + const u8 command = Memory::Read_U8(request.buffer_in); + if (request.request != command) + { + WARN_LOG( + IOS_DI, + "IOCtl: Received conflicting commands: ioctl 0x%02x, buffer 0x%02x. Using ioctl command.", + request.request, command); + } + + bool ready_to_execute = !m_executing_command.has_value(); m_commands_to_execute.push_back(request.address); - if (ready_to_execute) - StartIOCtl(request); - // DVDInterface handles the timing and will call FinishIOCtl after the command - // has been executed to reply to the request, so we shouldn't reply here. + if (ready_to_execute) + { + ProcessQueuedIOCtl(); + } + + // FinishIOCtl will be called after the command has been executed + // to reply to the request, so we shouldn't reply here. return GetNoReply(); } -void DI::StartIOCtl(const IOCtlRequest& request) -{ - const u32 command_0 = Memory::Read_U32(request.buffer_in); - const u32 command_1 = Memory::Read_U32(request.buffer_in + 4); - const u32 command_2 = Memory::Read_U32(request.buffer_in + 8); - - // DVDInterface's ExecuteCommand handles most of the work. - // The IOCtl callback is used to generate a reply afterwards. - DVDInterface::ExecuteCommand(command_0, command_1, command_2, request.buffer_out, - request.buffer_out_size, true); -} - -void DI::FinishIOCtl(DVDInterface::DIInterruptType interrupt_type) +void DI::ProcessQueuedIOCtl() { if (m_commands_to_execute.empty()) { @@ -71,52 +103,629 @@ void DI::FinishIOCtl(DVDInterface::DIInterruptType interrupt_type) return; } - // This command has been executed, so it's removed from the queue - u32 command_address = m_commands_to_execute.front(); + m_executing_command = {m_commands_to_execute.front()}; m_commands_to_execute.pop_front(); - m_ios.EnqueueIPCReply(IOCtlRequest{command_address}, interrupt_type); + + IOCtlRequest request{m_executing_command->m_request_address}; + auto finished = StartIOCtl(request); + if (finished) + { + CoreTiming::ScheduleEvent(2700 * SystemTimers::TIMER_RATIO, s_finish_executing_di_command, + static_cast(finished.value())); + return; + } +} + +std::optional DI::WriteIfFits(const IOCtlRequest& request, u32 value) +{ + if (request.buffer_out_size < 4) + { + WARN_LOG(IOS_DI, "Output buffer is too small to contain result; returning security error"); + return DIResult::SecurityError; + } + else + { + Memory::Write_U32(value, request.buffer_out); + return DIResult::Success; + } +} + +std::optional DI::StartIOCtl(const IOCtlRequest& request) +{ + if (request.buffer_in_size != 0x20) + { + ERROR_LOG(IOS_DI, "IOCtl: Received bad input buffer size 0x%02x, should be 0x20", + request.buffer_in_size); + return DIResult::SecurityError; + } + + // DVDInterface's ExecuteCommand handles most of the work for most of these. + // The IOCtl callback is used to generate a reply afterwards. + switch (static_cast(request.request)) + { + case DIIoctl::DVDLowInquiry: + INFO_LOG(IOS_DI, "DVDLowInquiry"); + DICMDBUF0 = 0x12000000; + DICMDBUF1 = 0; + return StartDMATransfer(0x20, request); + case DIIoctl::DVDLowReadDiskID: + INFO_LOG(IOS_DI, "DVDLowReadDiskID"); + DICMDBUF0 = 0xA8000040; + DICMDBUF1 = 0; + DICMDBUF2 = 0x20; + return StartDMATransfer(0x20, request); + // TODO: Include an additional read that happens on Wii discs, or at least + // emulate its side effect of disabling DTK configuration + // if (Memory::Read_U32(request.buffer_out + 24) == 0x5d1c9ea3) { // Wii Magic + // if (!m_has_read_encryption_info) { + // // Read 0x44 (=> 0x60) bytes starting from offset 8 or byte 0x20; + // // byte 0x60 is disable hashing and byte 0x61 is disable encryption + // } + // } + case DIIoctl::DVDLowRead: + { + const u32 length = Memory::Read_U32(request.buffer_in + 4); + const u32 position = Memory::Read_U32(request.buffer_in + 8); + INFO_LOG(IOS_DI, "DVDLowRead: offset 0x%08x (byte 0x%09" PRIx64 "), length 0x%x", position, + static_cast(position) << 2, length); + if (m_current_partition == DiscIO::PARTITION_NONE) + { + ERROR_LOG(IOS_DI, "Attempted to perform a decrypting read when no partition is open!"); + return DIResult::SecurityError; + } + if (request.buffer_out_size < length) + { + WARN_LOG(IOS_DI, + "Output buffer is too small for the result of the read (%d bytes given, needed at " + "least %d); returning security error", + request.buffer_out_size, length); + return DIResult::SecurityError; + } + m_last_length = position; // An actual mistake in IOS + DVDInterface::PerformDecryptingRead(position, length, request.buffer_out, m_current_partition, + DVDInterface::ReplyType::IOS); + return {}; + } + case DIIoctl::DVDLowWaitForCoverClose: + // This is a bit awkward to implement, as it doesn't return for a long time, so just act as if + // the cover was immediately closed + INFO_LOG(IOS_DI, "DVDLowWaitForCoverClose - skipping"); + return DIResult::CoverClosed; + case DIIoctl::DVDLowGetCoverRegister: + { + u32 dicvr = DICVR; + DEBUG_LOG(IOS_DI, "DVDLowGetCoverRegister 0x%08x", dicvr); + return WriteIfFits(request, dicvr); + } + case DIIoctl::DVDLowNotifyReset: + INFO_LOG(IOS_DI, "DVDLowNotifyReset"); + ResetDIRegisters(); + return DIResult::Success; + case DIIoctl::DVDLowSetSpinupFlag: + ERROR_LOG(IOS_DI, "DVDLowSetSpinupFlag - not a valid command, rejecting"); + return DIResult::BadArgument; + case DIIoctl::DVDLowReadDvdPhysical: + { + const u8 position = Memory::Read_U8(request.buffer_in + 7); + INFO_LOG(IOS_DI, "DVDLowReadDvdPhysical: position 0x%02x", position); + DICMDBUF0 = 0xAD000000 | (position << 8); + DICMDBUF1 = 0; + DICMDBUF2 = 0; + return StartDMATransfer(0x800, request); + } + case DIIoctl::DVDLowReadDvdCopyright: + { + const u8 position = Memory::Read_U8(request.buffer_in + 7); + INFO_LOG(IOS_DI, "DVDLowReadDvdCopyright: position 0x%02x", position); + DICMDBUF0 = 0xAD010000 | (position << 8); + DICMDBUF1 = 0; + DICMDBUF2 = 0; + return StartImmediateTransfer(request); + } + case DIIoctl::DVDLowReadDvdDiscKey: + { + const u8 position = Memory::Read_U8(request.buffer_in + 7); + INFO_LOG(IOS_DI, "DVDLowReadDvdDiscKey: position 0x%02x", position); + DICMDBUF0 = 0xAD020000 | (position << 8); + DICMDBUF1 = 0; + DICMDBUF2 = 0; + return StartDMATransfer(0x800, request); + } + case DIIoctl::DVDLowGetLength: + INFO_LOG(IOS_DI, "DVDLowGetLength 0x%08x", m_last_length); + return WriteIfFits(request, m_last_length); + case DIIoctl::DVDLowGetImmBuf: + { + u32 diimmbuf = DIIMMBUF; + INFO_LOG(IOS_DI, "DVDLowGetImmBuf 0x%08x", diimmbuf); + return WriteIfFits(request, diimmbuf); + } + case DIIoctl::DVDLowUnmaskCoverInterrupt: + INFO_LOG(IOS_DI, "DVDLowUnmaskCoverInterrupt"); + DVDInterface::SetInterruptEnabled(DVDInterface::DIInterruptType::CVRINT, false); + return DIResult::Success; + case DIIoctl::DVDLowClearCoverInterrupt: + DEBUG_LOG(IOS_DI, "DVDLowClearCoverInterrupt"); + DVDInterface::ClearInterrupt(DVDInterface::DIInterruptType::CVRINT); + return DIResult::Success; + case DIIoctl::DVDLowGetCoverStatus: + // TODO: handle resetting case + INFO_LOG(IOS_DI, "DVDLowGetCoverStatus: Disc %sInserted", + DVDInterface::IsDiscInside() ? "" : "Not "); + return WriteIfFits(request, DVDInterface::IsDiscInside() ? 2 : 1); + case DIIoctl::DVDLowEnableCoverInterrupt: + INFO_LOG(IOS_DI, "DVDLowEnableCoverInterrupt"); + DVDInterface::SetInterruptEnabled(DVDInterface::DIInterruptType::CVRINT, true); + return DIResult::Success; + case DIIoctl::DVDLowReset: + { + const bool spinup = Memory::Read_U32(request.address + 4); + INFO_LOG(IOS_DI, "DVDLowReset %s spinup", spinup ? "with" : "without"); + DVDInterface::Reset(spinup); + ResetDIRegisters(); + return DIResult::Success; + } + case DIIoctl::DVDLowOpenPartition: + ERROR_LOG(IOS_DI, "DVDLowOpenPartition as an ioctl - rejecting"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DIFFERENT_PARTITION_COMMAND); + return DIResult::SecurityError; + case DIIoctl::DVDLowClosePartition: + INFO_LOG(IOS_DI, "DVDLowClosePartition"); + ChangePartition(DiscIO::PARTITION_NONE); + return DIResult::Success; + case DIIoctl::DVDLowUnencryptedRead: + { + const u32 length = Memory::Read_U32(request.buffer_in + 4); + const u32 position = Memory::Read_U32(request.buffer_in + 8); + const u32 end = position + (length >> 2); // a 32-bit offset + INFO_LOG(IOS_DI, "DVDLowUnencryptedRead: offset 0x%08x (byte 0x%09" PRIx64 "), length 0x%x", + position, static_cast(position) << 2, length); + // (start, end) as 32-bit offsets + // Older IOS versions only accept the first range. Later versions added the extra ranges to + // check how the drive responds to out of bounds reads (an error 001 check). + constexpr std::array, 3> valid_ranges = { + std::make_pair(0, 0x14000), // "System area" + std::make_pair(0x460A0000, 0x460A0008), + std::make_pair(0x7ED40000, 0x7ED40008), + }; + for (auto range : valid_ranges) + { + if (range.first <= position && position <= range.second && range.first <= end && + end <= range.second) + { + DICMDBUF0 = 0xA8000000; + DICMDBUF1 = position; + DICMDBUF2 = length; + return StartDMATransfer(length, request); + } + } + WARN_LOG(IOS_DI, "DVDLowUnencryptedRead: trying to read from an illegal region!"); + return DIResult::SecurityError; + } + case DIIoctl::DVDLowEnableDvdVideo: + ERROR_LOG(IOS_DI, "DVDLowEnableDvdVideo - rejecting"); + return DIResult::SecurityError; + // There are a bunch of ioctlvs that are also (unintentionally) usable as ioctls; reject these in + // Dolphin as games are unlikely to use them. + case DIIoctl::DVDLowGetNoDiscOpenPartitionParams: + ERROR_LOG(IOS_DI, "DVDLowGetNoDiscOpenPartitionParams as an ioctl - rejecting"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DIFFERENT_PARTITION_COMMAND); + return DIResult::SecurityError; + case DIIoctl::DVDLowNoDiscOpenPartition: + ERROR_LOG(IOS_DI, "DVDLowNoDiscOpenPartition as an ioctl - rejecting"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DIFFERENT_PARTITION_COMMAND); + return DIResult::SecurityError; + case DIIoctl::DVDLowGetNoDiscBufferSizes: + ERROR_LOG(IOS_DI, "DVDLowGetNoDiscBufferSizes as an ioctl - rejecting"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DIFFERENT_PARTITION_COMMAND); + return DIResult::SecurityError; + case DIIoctl::DVDLowOpenPartitionWithTmdAndTicket: + ERROR_LOG(IOS_DI, "DVDLowOpenPartitionWithTmdAndTicket as an ioctl - rejecting"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DIFFERENT_PARTITION_COMMAND); + return DIResult::SecurityError; + case DIIoctl::DVDLowOpenPartitionWithTmdAndTicketView: + ERROR_LOG(IOS_DI, "DVDLowOpenPartitionWithTmdAndTicketView as an ioctl - rejecting"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DIFFERENT_PARTITION_COMMAND); + return DIResult::SecurityError; + case DIIoctl::DVDLowGetStatusRegister: + { + u32 disr = DISR; + INFO_LOG(IOS_DI, "DVDLowGetStatusRegister: 0x%08x", disr); + return WriteIfFits(request, disr); + } + case DIIoctl::DVDLowGetControlRegister: + { + u32 dicr = DICR; + INFO_LOG(IOS_DI, "DVDLowGetControlRegister: 0x%08x", dicr); + return WriteIfFits(request, dicr); + } + case DIIoctl::DVDLowReportKey: + { + const u8 param1 = Memory::Read_U8(request.buffer_in + 7); + const u32 param2 = Memory::Read_U32(request.buffer_in + 8); + INFO_LOG(IOS_DI, "DVDLowReportKey: param1 0x%02x, param2 0x%06x", param1, param2); + DICMDBUF0 = 0xA4000000 | (param1 << 16); + DICMDBUF1 = param2 & 0xFFFFFF; + DICMDBUF2 = 0; + return StartDMATransfer(0x20, request); + } + case DIIoctl::DVDLowSeek: + { + const u32 position = Memory::Read_U32(request.buffer_in + 4); // 32-bit offset + INFO_LOG(IOS_DI, "DVDLowSeek: position 0x%08x, translated to 0x%08x", position, + position); // TODO: do partition translation! + DICMDBUF0 = 0xAB000000; + DICMDBUF1 = position; + return StartImmediateTransfer(request, false); + } + case DIIoctl::DVDLowReadDvd: + { + const u8 flag1 = Memory::Read_U8(request.buffer_in + 7); + const u8 flag2 = Memory::Read_U8(request.buffer_in + 11); + const u32 length = Memory::Read_U32(request.buffer_in + 12); + const u32 position = Memory::Read_U32(request.buffer_in + 16); + INFO_LOG(IOS_DI, "DVDLowReadDvd(%d, %d): position 0x%06x, length 0x%06x", flag1, flag2, + position, length); + DICMDBUF0 = 0xD0000000 | ((flag1 & 1) << 7) | ((flag2 & 1) << 6); + DICMDBUF1 = position & 0xFFFFFF; + DICMDBUF2 = length & 0xFFFFFF; + return StartDMATransfer(0x800 * length, request); + } + case DIIoctl::DVDLowReadDvdConfig: + { + const u8 flag1 = Memory::Read_U8(request.buffer_in + 7); + const u8 param2 = Memory::Read_U8(request.buffer_in + 11); + const u32 position = Memory::Read_U32(request.buffer_in + 12); + INFO_LOG(IOS_DI, "DVDLowReadDvdConfig(%d, %d): position 0x%06x", flag1, param2, position); + DICMDBUF0 = 0xD1000000 | ((flag1 & 1) << 16) | param2; + DICMDBUF1 = position & 0xFFFFFF; + DICMDBUF2 = 0; + return StartImmediateTransfer(request); + } + case DIIoctl::DVDLowStopLaser: + INFO_LOG(IOS_DI, "DVDLowStopLaser"); + DICMDBUF0 = 0xD2000000; + return StartImmediateTransfer(request); + case DIIoctl::DVDLowOffset: + { + const u8 flag = Memory::Read_U8(request.buffer_in + 7); + const u32 offset = Memory::Read_U32(request.buffer_in + 8); + INFO_LOG(IOS_DI, "DVDLowOffset(%d): offset 0x%08x", flag, offset); + DICMDBUF0 = 0xD9000000 | ((flag & 1) << 16); + DICMDBUF1 = offset; + return StartImmediateTransfer(request); + } + case DIIoctl::DVDLowReadDiskBca: + INFO_LOG(IOS_DI, "DVDLowReadDiskBca"); + DICMDBUF0 = 0xDA000000; + return StartDMATransfer(0x40, request); + case DIIoctl::DVDLowRequestDiscStatus: + INFO_LOG(IOS_DI, "DVDLowRequestDiscStatus"); + DICMDBUF0 = 0xDB000000; + return StartImmediateTransfer(request); + case DIIoctl::DVDLowRequestRetryNumber: + INFO_LOG(IOS_DI, "DVDLowRequestRetryNumber"); + DICMDBUF0 = 0xDC000000; + return StartImmediateTransfer(request); + case DIIoctl::DVDLowSetMaximumRotation: + { + const u8 speed = Memory::Read_U8(request.buffer_in + 7); + INFO_LOG(IOS_DI, "DVDLowSetMaximumRotation: speed %d", speed); + DICMDBUF0 = 0xDD000000 | ((speed & 3) << 16); + return StartImmediateTransfer(request, false); + } + case DIIoctl::DVDLowSerMeasControl: + { + const u8 flag1 = Memory::Read_U8(request.buffer_in + 7); + const u8 flag2 = Memory::Read_U8(request.buffer_in + 11); + INFO_LOG(IOS_DI, "DVDLowSerMeasControl(%d, %d)", flag1, flag2); + DICMDBUF0 = 0xDF000000 | ((flag1 & 1) << 17) | ((flag2 & 1) << 16); + return StartDMATransfer(0x20, request); + } + case DIIoctl::DVDLowRequestError: + INFO_LOG(IOS_DI, "DVDLowRequestError"); + DICMDBUF0 = 0xE0000000; + return StartImmediateTransfer(request); + case DIIoctl::DVDLowAudioStream: + { + const u8 mode = Memory::Read_U8(request.buffer_in + 7); + const u32 length = Memory::Read_U32(request.buffer_in + 8); + const u32 position = Memory::Read_U32(request.buffer_in + 12); + INFO_LOG(IOS_DI, "DVDLowAudioStream(%d): offset 0x%08x (byte 0x%09" PRIx64 "), length 0x%x", + mode, position, static_cast(position) << 2, length); + DICMDBUF0 = 0xE1000000 | ((mode & 3) << 16); + DICMDBUF1 = position; + DICMDBUF2 = length; + return StartImmediateTransfer(request, false); + } + case DIIoctl::DVDLowRequestAudioStatus: + { + const u8 mode = Memory::Read_U8(request.buffer_in + 7); + INFO_LOG(IOS_DI, "DVDLowRequestAudioStatus(%d)", mode); + DICMDBUF0 = 0xE2000000 | ((mode & 3) << 16); + DICMDBUF1 = 0; + // Note that this command does not copy the value written to DIIMMBUF, which makes it rather + // useless (to actually use it, DVDLowGetImmBuf would need to be used afterwards) + return StartImmediateTransfer(request, false); + } + case DIIoctl::DVDLowStopMotor: + { + const u8 eject = Memory::Read_U8(request.buffer_in + 7); + const u8 kill = Memory::Read_U8(request.buffer_in + 11); + INFO_LOG(IOS_DI, "DVDLowStopMotor(%d, %d)", eject, kill); + DICMDBUF0 = 0xE3000000 | ((eject & 1) << 17) | ((kill & 1) << 20); + DICMDBUF1 = 0; + return StartImmediateTransfer(request); + } + case DIIoctl::DVDLowAudioBufferConfig: + { + const u8 enable = Memory::Read_U8(request.buffer_in + 7); + const u8 buffer_size = Memory::Read_U8(request.buffer_in + 11); + INFO_LOG(IOS_DI, "DVDLowAudioBufferConfig: %s, buffer size %d", enable ? "enabled" : "disabled", + buffer_size); + DICMDBUF0 = 0xE4000000 | ((enable & 1) << 16) | (buffer_size & 0xf); + DICMDBUF1 = 0; + // On the other hand, this command *does* copy DIIMMBUF, but the actual code in the drive never + // writes anything to it, so it just copies over a stale value (e.g. from DVDLowRequestError). + return StartImmediateTransfer(request); + } + default: + ERROR_LOG(IOS_DI, "Unknown ioctl 0x%02x", request.request); + return DIResult::SecurityError; + } +} + +std::optional DI::StartDMATransfer(u32 command_length, const IOCtlRequest& request) +{ + if (request.buffer_out_size < command_length) + { + // Actual /dev/di will still send a command, but won't write the length or output address, + // causing it to eventually time out after 15 seconds. Just immediately time out here, + // instead. + WARN_LOG(IOS_DI, + "Output buffer is too small for the result of the command (%d bytes given, needed at " + "least %d); returning read timed out (immediately, instead of waiting)", + request.buffer_out_size, command_length); + return DIResult::ReadTimedOut; + } + + if ((command_length & 0x1f) != 0 || (request.buffer_out & 0x1f) != 0) + { + // In most cases, the actual DI driver just hangs for unaligned data, but let's be a bit more + // gentle. + WARN_LOG(IOS_DI, + "Output buffer or length is incorrectly aligned (buffer 0x%08x, buffer length %x, " + "command length %x)", + request.buffer_out, request.buffer_out_size, command_length); + return DIResult::BadArgument; + } + + DIMAR = request.buffer_out; + m_last_length = command_length; + DILENGTH = command_length; + + DVDInterface::ExecuteCommand(DVDInterface::ReplyType::IOS); + // Reply will be posted when done by FinishIOCtl. + return {}; +} +std::optional DI::StartImmediateTransfer(const IOCtlRequest& request, + bool write_to_buf) +{ + if (write_to_buf && request.buffer_out_size < 4) + { + WARN_LOG(IOS_DI, + "Output buffer size is too small for an immediate transfer (%d bytes, should be at " + "least 4). Performing transfer anyways.", + request.buffer_out_size); + } + + m_executing_command->m_copy_diimmbuf = write_to_buf; + + DVDInterface::ExecuteCommand(DVDInterface::ReplyType::IOS); + // Reply will be posted when done by FinishIOCtl. + return {}; +} + +void DI::InterruptFromDVDInterface(DVDInterface::DIInterruptType interrupt_type) +{ + DIResult result; + switch (interrupt_type) + { + case DVDInterface::DIInterruptType::TCINT: + result = DIResult::Success; + break; + case DVDInterface::DIInterruptType::DEINT: + result = DIResult::DriveError; + break; + default: + PanicAlert("IOS::HLE::Device::DI: Unexpected DVDInterface interrupt %d!", + static_cast(interrupt_type)); + result = DIResult::DriveError; + break; + } + + auto di = IOS::HLE::GetIOS()->GetDeviceByName("/dev/di"); + if (di) + { + std::static_pointer_cast(di)->FinishDICommand(result); + } + else + { + PanicAlert("IOS::HLE::Device::DI: Received interrupt from DVDInterface when device wasn't " + "registered!"); + } +} + +void DI::FinishDICommandCallback(u64 userdata, s64 ticksbehind) +{ + const DIResult result = static_cast(userdata); + + auto di = IOS::HLE::GetIOS()->GetDeviceByName("/dev/di"); + if (di) + std::static_pointer_cast(di)->FinishDICommand(result); + else + PanicAlert("IOS::HLE::Device::DI: Received interrupt from DI when device wasn't registered!"); +} + +void DI::FinishDICommand(DIResult result) +{ + if (!m_executing_command.has_value()) + { + PanicAlert("IOS::HLE::Device::DI: There is no command to finish!"); + return; + } + + IOCtlRequest request{m_executing_command->m_request_address}; + if (m_executing_command->m_copy_diimmbuf) + Memory::Write_U32(DIIMMBUF, request.buffer_out); + + m_ios.EnqueueIPCReply(request, static_cast(result)); + + m_executing_command.reset(); // DVDInterface is now ready to execute another command, // so we start executing a command from the queue if there is one if (!m_commands_to_execute.empty()) { - IOCtlRequest next_request{m_commands_to_execute.front()}; - StartIOCtl(next_request); + ProcessQueuedIOCtl(); } } IPCCommandResult DI::IOCtlV(const IOCtlVRequest& request) { - for (const auto& vector : request.io_vectors) - Memory::Memset(vector.address, 0, vector.size); - s32 return_value = IPC_SUCCESS; - switch (request.request) + // IOCtlVs are not queued since they don't (currently) go into DVDInterface and act + // asynchronously. This does mean that an IOCtlV can be executed while an IOCtl is in progress, + // which isn't ideal. + InitializeIfFirstTime(); + + if (request.in_vectors[0].size != 0x20) { - case DVDInterface::DVDLowOpenPartition: + ERROR_LOG(IOS_DI, "IOCtlV: Received bad input buffer size 0x%02x, should be 0x20", + request.in_vectors[0].size); + return GetDefaultReply(static_cast(DIResult::BadArgument)); + } + const u8 command = Memory::Read_U8(request.in_vectors[0].address); + if (request.request != command) { - DEBUG_ASSERT_MSG(IOS_DI, request.in_vectors[1].address == 0, "DVDLowOpenPartition with ticket"); - DEBUG_ASSERT_MSG(IOS_DI, request.in_vectors[2].address == 0, - "DVDLowOpenPartition with cert chain"); + WARN_LOG(IOS_DI, + "IOCtlV: Received conflicting commands: ioctl 0x%02x, buffer 0x%02x. Using ioctlv " + "command.", + request.request, command); + } + + DIResult return_value = DIResult::BadArgument; + switch (static_cast(request.request)) + { + case DIIoctl::DVDLowOpenPartition: + { + if (request.in_vectors.size() != 3 || request.io_vectors.size() != 2) + { + ERROR_LOG(IOS_DI, "DVDLowOpenPartition: bad vector count %zu in/%zu out", + request.in_vectors.size(), request.io_vectors.size()); + break; + } + if (request.in_vectors[1].address != 0) + { + ERROR_LOG(IOS_DI, + "DVDLowOpenPartition with ticket - not implemented, ignoring ticket parameter"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DIFFERENT_PARTITION_COMMAND); + } + if (request.in_vectors[2].address != 0) + { + ERROR_LOG(IOS_DI, + "DVDLowOpenPartition with cert chain - not implemented, ignoring certs parameter"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DIFFERENT_PARTITION_COMMAND); + } const u64 partition_offset = static_cast(Memory::Read_U32(request.in_vectors[0].address + 4)) << 2; - const DiscIO::Partition partition(partition_offset); - DVDInterface::ChangePartition(partition); + ChangePartition(DiscIO::Partition(partition_offset)); - INFO_LOG(IOS_DI, "DVDLowOpenPartition: partition_offset 0x%016" PRIx64, partition_offset); + INFO_LOG(IOS_DI, "DVDLowOpenPartition: partition_offset 0x%09" PRIx64, partition_offset); // Read TMD to the buffer - const IOS::ES::TMDReader tmd = DVDThread::GetTMD(partition); + const IOS::ES::TMDReader tmd = DVDThread::GetTMD(m_current_partition); const std::vector& raw_tmd = tmd.GetBytes(); Memory::CopyToEmu(request.io_vectors[0].address, raw_tmd.data(), raw_tmd.size()); - m_ios.GetES()->DIVerify(tmd, DVDThread::GetTicket(partition)); - return_value = 1; + ReturnCode es_result = m_ios.GetES()->DIVerify(tmd, DVDThread::GetTicket(m_current_partition)); + Memory::Write_U32(es_result, request.io_vectors[1].address); + + return_value = DIResult::Success; break; } + case DIIoctl::DVDLowGetNoDiscOpenPartitionParams: + ERROR_LOG(IOS_DI, "DVDLowGetNoDiscOpenPartitionParams - dummied out"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DIFFERENT_PARTITION_COMMAND); + request.DumpUnknown(GetDeviceName(), Common::Log::IOS_DI); + break; + case DIIoctl::DVDLowNoDiscOpenPartition: + ERROR_LOG(IOS_DI, "DVDLowNoDiscOpenPartition - dummied out"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DIFFERENT_PARTITION_COMMAND); + request.DumpUnknown(GetDeviceName(), Common::Log::IOS_DI); + break; + case DIIoctl::DVDLowGetNoDiscBufferSizes: + ERROR_LOG(IOS_DI, "DVDLowGetNoDiscBufferSizes - dummied out"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DIFFERENT_PARTITION_COMMAND); + request.DumpUnknown(GetDeviceName(), Common::Log::IOS_DI); + break; + case DIIoctl::DVDLowOpenPartitionWithTmdAndTicket: + ERROR_LOG(IOS_DI, "DVDLowOpenPartitionWithTmdAndTicket - not implemented"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DIFFERENT_PARTITION_COMMAND); + break; + case DIIoctl::DVDLowOpenPartitionWithTmdAndTicketView: + ERROR_LOG(IOS_DI, "DVDLowOpenPartitionWithTmdAndTicketView - not implemented"); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DIFFERENT_PARTITION_COMMAND); + break; default: + ERROR_LOG(IOS_DI, "Unknown ioctlv 0x%02x", request.request); request.DumpUnknown(GetDeviceName(), Common::Log::IOS_DI); } - return GetDefaultReply(return_value); + return GetDefaultReply(static_cast(return_value)); +} + +void DI::ChangePartition(const DiscIO::Partition partition) +{ + m_current_partition = partition; +} + +DiscIO::Partition DI::GetCurrentPartition() +{ + auto di = IOS::HLE::GetIOS()->GetDeviceByName("/dev/di"); + // Note that this function is called in Gamecube mode for UpdateRunningGameMetadata, + // so both cases are hit in normal circumstances. + if (!di) + return DiscIO::PARTITION_NONE; + return std::static_pointer_cast(di)->m_current_partition; +} + +void DI::InitializeIfFirstTime() +{ + // Match the behavior of Nintendo's initDvdDriverStage2, which is called the first time + // an open/ioctl/ioctlv occurs. This behavior is observable by directly reading the DI registers, + // so it shouldn't be handled in the constructor. + // Note that ResetDIRegisters also clears the current partition, which actually normally happens + // earlier; however, that is not observable. + // Also, registers are not cleared if syscall_check_di_reset (0x46) returns true (bit 10 of + // HW_RESETS is set), which is not currently emulated. + if (!m_has_initialized) + { + ResetDIRegisters(); + m_has_initialized = true; + } +} + +void DI::ResetDIRegisters() +{ + // Clear transfer complete and error interrupts (normally r/z, but here we just directly write + // zero) + DVDInterface::ClearInterrupt(DVDInterface::DIInterruptType::TCINT); + DVDInterface::ClearInterrupt(DVDInterface::DIInterruptType::DEINT); + // Enable transfer complete and error interrupts, and disable cover interrupt + DVDInterface::SetInterruptEnabled(DVDInterface::DIInterruptType::TCINT, true); + DVDInterface::SetInterruptEnabled(DVDInterface::DIInterruptType::DEINT, true); + DVDInterface::SetInterruptEnabled(DVDInterface::DIInterruptType::CVRINT, false); + // Close the current partition, if there is one + ChangePartition(DiscIO::PARTITION_NONE); } } // namespace IOS::HLE::Device diff --git a/Source/Core/Core/IOS/DI/DI.h b/Source/Core/Core/IOS/DI/DI.h index 2b01784cc1..975b0adc51 100644 --- a/Source/Core/Core/IOS/DI/DI.h +++ b/Source/Core/Core/IOS/DI/DI.h @@ -5,17 +5,29 @@ #pragma once #include +#include #include #include "Common/CommonTypes.h" #include "Core/IOS/Device.h" #include "Core/IOS/IOS.h" +#include "DiscIO/Volume.h" +class CBoot; class PointerWrap; namespace DVDInterface { -enum DIInterruptType : int; +enum class DIInterruptType : int; +} +namespace CoreTiming +{ +struct EventType; +} + +namespace IOS::HLE +{ +void Init(); } namespace IOS::HLE::Device @@ -25,16 +37,112 @@ class DI : public Device public: DI(Kernel& ios, const std::string& device_name); + static void InterruptFromDVDInterface(DVDInterface::DIInterruptType interrupt_type); + static DiscIO::Partition GetCurrentPartition(); + void DoState(PointerWrap& p) override; + IPCCommandResult Open(const OpenRequest& request) override; IPCCommandResult IOCtl(const IOCtlRequest& request) override; IPCCommandResult IOCtlV(const IOCtlVRequest& request) override; - void FinishIOCtl(DVDInterface::DIInterruptType interrupt_type); + enum class DIIoctl : u32 + { + DVDLowInquiry = 0x12, + DVDLowReadDiskID = 0x70, + DVDLowRead = 0x71, + DVDLowWaitForCoverClose = 0x79, + DVDLowGetCoverRegister = 0x7a, // DVDLowPrepareCoverRegister + DVDLowNotifyReset = 0x7e, + DVDLowSetSpinupFlag = 0x7f, + DVDLowReadDvdPhysical = 0x80, + DVDLowReadDvdCopyright = 0x81, + DVDLowReadDvdDiscKey = 0x82, + DVDLowGetLength = 0x83, + DVDLowGetImmBuf = 0x84, // Unconfirmed name + DVDLowUnmaskCoverInterrupt = 0x85, + DVDLowClearCoverInterrupt = 0x86, + // 0x87 is a dummied out command + DVDLowGetCoverStatus = 0x88, + DVDLowEnableCoverInterrupt = 0x89, // Unconfirmed name + DVDLowReset = 0x8a, + DVDLowOpenPartition = 0x8b, // ioctlv only + DVDLowClosePartition = 0x8c, + DVDLowUnencryptedRead = 0x8d, + DVDLowEnableDvdVideo = 0x8e, + DVDLowGetNoDiscOpenPartitionParams = 0x90, // ioctlv, dummied out + DVDLowNoDiscOpenPartition = 0x91, // ioctlv, dummied out + DVDLowGetNoDiscBufferSizes = 0x92, // ioctlv, dummied out + DVDLowOpenPartitionWithTmdAndTicket = 0x93, // ioctlv + DVDLowOpenPartitionWithTmdAndTicketView = 0x94, // ioctlv + DVDLowGetStatusRegister = 0x95, // DVDLowPrepareStatusRegsiter + DVDLowGetControlRegister = 0x96, // DVDLowPrepareControlRegister + DVDLowReportKey = 0xa4, + // 0xa8 is unusable externally + DVDLowSeek = 0xab, + DVDLowReadDvd = 0xd0, + DVDLowReadDvdConfig = 0xd1, + DVDLowStopLaser = 0xd2, + DVDLowOffset = 0xd9, + DVDLowReadDiskBca = 0xda, + DVDLowRequestDiscStatus = 0xdb, + DVDLowRequestRetryNumber = 0xdc, + DVDLowSetMaximumRotation = 0xdd, + DVDLowSerMeasControl = 0xdf, + DVDLowRequestError = 0xe0, + DVDLowAudioStream = 0xe1, + DVDLowRequestAudioStatus = 0xe2, + DVDLowStopMotor = 0xe3, + DVDLowAudioBufferConfig = 0xe4, + }; + + enum class DIResult : s32 + { + Success = 1, + DriveError = 2, + CoverClosed = 4, + ReadTimedOut = 16, + SecurityError = 32, + VerifyError = 64, + BadArgument = 128, + }; private: - void StartIOCtl(const IOCtlRequest& request); + struct ExecutingCommandInfo + { + ExecutingCommandInfo() {} + ExecutingCommandInfo(u32 request_address) + : m_request_address(request_address), m_copy_diimmbuf(false) + { + } + u32 m_request_address; + bool m_copy_diimmbuf; + }; + friend class ::CBoot; + friend void ::IOS::HLE::Init(); + + void ProcessQueuedIOCtl(); + std::optional StartIOCtl(const IOCtlRequest& request); + std::optional WriteIfFits(const IOCtlRequest& request, u32 value); + std::optional StartDMATransfer(u32 command_length, const IOCtlRequest& request); + std::optional StartImmediateTransfer(const IOCtlRequest& request, + bool write_to_buf = true); + + void ChangePartition(const DiscIO::Partition partition); + void InitializeIfFirstTime(); + void ResetDIRegisters(); + static void FinishDICommandCallback(u64 userdata, s64 ticksbehind); + void FinishDICommand(DIResult result); + + static CoreTiming::EventType* s_finish_executing_di_command; + + std::optional m_executing_command; std::deque m_commands_to_execute; + + DiscIO::Partition m_current_partition = DiscIO::PARTITION_NONE; + + bool m_has_initialized = false; + u32 m_last_length = 0; }; } // namespace IOS::HLE::Device diff --git a/Source/Core/Core/IOS/IOS.cpp b/Source/Core/Core/IOS/IOS.cpp index 72c304b772..7ce51f2338 100644 --- a/Source/Core/Core/IOS/IOS.cpp +++ b/Source/Core/Core/IOS/IOS.cpp @@ -782,6 +782,9 @@ void Init() device->EventNotify(); }); + Device::DI::s_finish_executing_di_command = + CoreTiming::RegisterEvent("FinishDICommand", Device::DI::FinishDICommandCallback); + // Start with IOS80 to simulate part of the Wii boot process. s_ios = std::make_unique(Titles::SYSTEM_MENU_IOS); // On a Wii, boot2 launches the system menu IOS, which then launches the system menu diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp index 93f0b33318..db97cea52b 100644 --- a/Source/Core/Core/State.cpp +++ b/Source/Core/Core/State.cpp @@ -73,7 +73,7 @@ static Common::Event g_compressAndDumpStateSyncEvent; static std::thread g_save_thread; // Don't forget to increase this after doing changes on the savestate system -constexpr u32 STATE_VERSION = 113; // Last changed in PR 8506 +constexpr u32 STATE_VERSION = 114; // Last changed in PR 8394 // Maps savestate versions to Dolphin versions. // Versions after 42 don't need to be added to this list, diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 7216b00206..88e5aa3f9a 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -686,7 +686,7 @@ void MainWindow::ChangeDisc() void MainWindow::EjectDisc() { - Core::RunAsCPUThread(DVDInterface::EjectDisc); + Core::RunAsCPUThread([] { DVDInterface::EjectDisc(DVDInterface::EjectCause::User); }); } void MainWindow::Open()