diff --git a/src/core/cdrom.cpp b/src/core/cdrom.cpp index 09676887f..533eb47bc 100644 --- a/src/core/cdrom.cpp +++ b/src/core/cdrom.cpp @@ -85,6 +85,13 @@ enum : u32 DOUBLE_SPEED_SECTORS_PER_SECOND = 150, // 2X speed is 150 sectors per second. }; +enum ManualLidControl : u8 +{ + MANUAL_LID_CONTROL_DISABLED = 0x00, + MANUAL_LID_CONTROL_ENABLED = 0x01, + MANUAL_LID_CONTROL_OPEN = 0x02, +}; + static constexpr u8 INTERRUPT_REGISTER_MASK = 0x1F; static constexpr TickCount MIN_SEEK_TICKS = 30000; @@ -181,6 +188,7 @@ enum StatBits : u8 enum ErrorReason : u8 { + ERROR_REASON_SHELL_OPEN = 0x08, ERROR_REASON_INVALID_ARGUMENT = 0x10, ERROR_REASON_INCORRECT_NUMBER_OF_PARAMETERS = 0x20, ERROR_REASON_INVALID_COMMAND = 0x40, @@ -416,6 +424,7 @@ struct CDROMState Command command_second_response = Command::None; DriveState drive_state = DriveState::Idle; DiscRegion disc_region = DiscRegion::NonPS1; + ManualLidControl manual_lid_control = MANUAL_LID_CONTROL_DISABLED; StatusRegister status = {}; @@ -555,6 +564,7 @@ static std::array s_command_info = {{ void CDROM::Initialize() { s_state.disc_region = DiscRegion::NonPS1; + s_state.manual_lid_control = MANUAL_LID_CONTROL_DISABLED; if (g_settings.cdrom_readahead_sectors > 0) s_reader.StartThread(g_settings.cdrom_readahead_sectors); @@ -585,7 +595,9 @@ void CDROM::Reset() s_state.status.bits = 0; s_state.secondary_status.bits = 0; s_state.secondary_status.motor_on = CanReadMedia(); - s_state.secondary_status.shell_open = !CanReadMedia(); + s_state.secondary_status.shell_open = (s_state.manual_lid_control & MANUAL_LID_CONTROL_ENABLED) ? + (s_state.manual_lid_control & MANUAL_LID_CONTROL_OPEN) : + !CanReadMedia(); s_state.mode.bits = 0; s_state.mode.read_raw_sector = true; s_state.interrupt_enable_register = INTERRUPT_REGISTER_MASK; @@ -639,7 +651,9 @@ TickCount CDROM::SoftReset() ClearDriveState(); s_state.secondary_status.bits = 0; s_state.secondary_status.motor_on = CanReadMedia(); - s_state.secondary_status.shell_open = !CanReadMedia(); + s_state.secondary_status.shell_open = (s_state.manual_lid_control & MANUAL_LID_CONTROL_ENABLED) ? + (s_state.manual_lid_control & MANUAL_LID_CONTROL_OPEN) : + !CanReadMedia(); s_state.mode.bits = 0; s_state.mode.read_raw_sector = true; s_state.request_register.bits = 0; @@ -935,7 +949,8 @@ bool CDROM::IsReadingOrPlaying() bool CDROM::CanReadMedia() { - return (s_state.drive_state != DriveState::ShellOpening && s_reader.HasMedia()); + return (s_state.drive_state != DriveState::ShellOpening && + s_state.manual_lid_control != (MANUAL_LID_CONTROL_ENABLED | MANUAL_LID_CONTROL_OPEN) && s_reader.HasMedia()); } bool CDROM::InsertMedia(std::unique_ptr& media, DiscRegion region, std::string_view serial, @@ -961,7 +976,7 @@ bool CDROM::InsertMedia(std::unique_ptr& media, DiscRegion region, std: SetHoldPosition(0, 0); // motor automatically spins up - if (s_state.drive_state != DriveState::ShellOpening) + if (s_state.drive_state != DriveState::ShellOpening && !(s_state.manual_lid_control & MANUAL_LID_CONTROL_OPEN)) StartMotor(); if (s_state.show_current_file) @@ -1002,10 +1017,10 @@ std::unique_ptr CDROM::RemoveMedia(bool for_disc_swap) // The console sends an interrupt when the shell is opened regardless of whether a command was executing. ClearAsyncInterrupt(); - SendAsyncErrorResponse(STAT_ERROR, 0x08); + SendAsyncErrorResponse(STAT_ERROR, ERROR_REASON_SHELL_OPEN); // Begin spin-down timer, we can't swap the new disc in immediately for some games (e.g. Metal Gear Solid). - if (for_disc_swap) + if (for_disc_swap && !s_state.manual_lid_control) { s_state.drive_state = DriveState::ShellOpening; s_state.drive_event.SetIntervalAndSchedule(stop_ticks); @@ -1014,6 +1029,35 @@ std::unique_ptr CDROM::RemoveMedia(bool for_disc_swap) return image; } +void CDROM::SetLidState(bool manual_control, bool manual_state) +{ + INFO_LOG("Setting lid state: manual_control={}, manual_state={}", manual_control, manual_state); + + const bool current_state = s_state.secondary_status.shell_open; + s_state.manual_lid_control = + manual_control ? + static_cast(MANUAL_LID_CONTROL_ENABLED | (manual_state ? MANUAL_LID_CONTROL_OPEN : 0)) : + MANUAL_LID_CONTROL_DISABLED; + + // Set shell open bit when manually opened, and start motor when closed. + if (!CanReadMedia()) + s_state.secondary_status.shell_open = true; + else if (!s_state.secondary_status.motor_on) + StartMotor(); + + if (current_state == s_state.secondary_status.shell_open) + return; + + // Notify when opened. + if (s_state.secondary_status.shell_open) + { + if (IsReadingOrPlaying()) + StopReadingWithError(ERROR_REASON_SHELL_OPEN); + else + SendAsyncErrorResponse(STAT_ERROR, ERROR_REASON_SHELL_OPEN); + } +} + bool CDROM::PrecacheMedia() { if (!s_reader.HasMedia()) @@ -1865,9 +1909,13 @@ void CDROM::ExecuteCommand(void*, TickCount ticks) // if bit 0 or 2 is set, send an additional byte SendACKAndStat(); - // shell open bit is cleared after sending the status - if (CanReadMedia()) + // shell open bit is cleared after sending the status and door is closed + if ((s_state.manual_lid_control & MANUAL_LID_CONTROL_ENABLED) ? + !(s_state.manual_lid_control & MANUAL_LID_CONTROL_OPEN) : + CanReadMedia()) + { s_state.secondary_status.shell_open = false; + } EndCommand(); return; @@ -4257,6 +4305,12 @@ void CDROM::DrawDebugWindow(float scale) ImGui::NextColumn(); ImGui::TextColored(s_state.secondary_status.shell_open ? active_color : inactive_color, "Shell Open: %s", s_state.secondary_status.shell_open ? "Yes" : "No"); + if (s_state.manual_lid_control & MANUAL_LID_CONTROL_ENABLED) + { + const bool open = (s_state.manual_lid_control & MANUAL_LID_CONTROL_OPEN); + ImGui::SameLine(); + ImGui::TextColored(open ? active_color : inactive_color, "Lid: %s", open ? "Open" : "Closed"); + } ImGui::NextColumn(); ImGui::TextColored(s_state.mode.ignore_bit ? active_color : inactive_color, "Ignore Bit: %s", s_state.mode.ignore_bit ? "Yes" : "No"); diff --git a/src/core/cdrom.h b/src/core/cdrom.h index 02f27f5e5..134413a02 100644 --- a/src/core/cdrom.h +++ b/src/core/cdrom.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #pragma once @@ -36,6 +36,9 @@ std::unique_ptr RemoveMedia(bool for_disc_swap); bool PrecacheMedia(); bool HasNonStandardOrReplacementSubQ(); +// nullopt = automatic +void SetLidState(bool manual_control, bool manual_state); + void CPUClockChanged(); // I/O diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 011df4b36..7c476398d 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -584,6 +584,7 @@ void MainWindow::onSystemStarting() s_locals.system_starting = true; s_locals.system_valid = false; s_locals.system_paused = false; + m_ui.actionCDROMLidStateAutomatic->setChecked(true); updateLogWidget(); switchToEmulationView(); @@ -2240,6 +2241,8 @@ void MainWindow::updateEmulationActions() m_ui.actionMemoryScanner->setDisabled(achievements_hardcore_mode); m_ui.actionFreeCamera->setDisabled(achievements_hardcore_mode); m_ui.actionReloadTextureReplacements->setDisabled(starting_or_not_running); + m_ui.menuCDROMLidState->setDisabled(starting_or_not_running); + m_ui.actionCDROMLidState->setDisabled(starting_or_not_running); m_ui.actionDumpRAM->setDisabled(starting_or_not_running || achievements_hardcore_mode); m_ui.actionDumpVRAM->setDisabled(starting_or_not_running || achievements_hardcore_mode); m_ui.actionDumpSPURAM->setDisabled(starting_or_not_running || achievements_hardcore_mode); @@ -2666,6 +2669,9 @@ void MainWindow::connectSignals() &Settings::GetLogLevelName, &Settings::GetLogLevelDisplayName, Log::DEFAULT_LOG_LEVEL, Log::Level::MaxCount); connect(m_ui.menuLogChannels, &QMenu::aboutToShow, this, &MainWindow::onDebugLogChannelsMenuAboutToShow); + connect(m_ui.actionCDROMLidStateAutomatic, &QAction::triggered, this, &MainWindow::onDebugCDROMLidStateChanged); + connect(m_ui.actionCDROMLidStateOpen, &QAction::triggered, this, &MainWindow::onDebugCDROMLidStateChanged); + connect(m_ui.actionCDROMLidStateClosed, &QAction::triggered, this, &MainWindow::onDebugCDROMLidStateChanged); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionLogToSystemConsole, "Logging", "LogToConsole", false); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionLogToFile, "Logging", "LogToFile", false); @@ -3582,6 +3588,13 @@ void MainWindow::onDebugLogChannelsMenuAboutToShow() LogWindow::populateFilterMenu(m_ui.menuLogChannels); } +void MainWindow::onDebugCDROMLidStateChanged() +{ + const bool manual_control = !m_ui.actionCDROMLidStateAutomatic->isChecked(); + const bool manual_open = (manual_control && m_ui.actionCDROMLidStateOpen->isChecked()); + g_core_thread->setLidState(manual_control, manual_open); +} + MainWindow::SystemLock MainWindow::pauseAndLockSystem() { // To switch out of fullscreen when displaying a popup, or not to? diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index d66e691f9..1409f8bdd 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -316,6 +316,7 @@ private: void onGameListSortIndicatorOrderChanged(int column, Qt::SortOrder order); void onDebugLogChannelsMenuAboutToShow(); + void onDebugCDROMLidStateChanged(); void openCPUDebugger(); Ui::MainWindow m_ui; diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index 2539cedb5..0c3651b66 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -157,9 +157,18 @@ Log Channels + + + CD-ROM Lid Control + + + + + + @@ -1542,6 +1551,35 @@ System Log + + + + true + + + true + + + Automatic + + + + + true + + + Closed + + + + + true + + + Open + + + diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 47b76c6b6..8dc9aaf5c 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -15,6 +15,7 @@ #include "core/achievements.h" #include "core/bus.h" +#include "core/cdrom.h" #include "core/cheats.h" #include "core/core.h" #include "core/core_private.h" @@ -1522,6 +1523,20 @@ void CoreThread::changeDiscFromPlaylist(quint32 index) errorReported(tr("Error"), tr("Failed to switch to subimage %1").arg(index)); } +void CoreThread::setLidState(bool manual_control, bool manual_state) +{ + if (!isCurrentThread()) + { + QMetaObject::invokeMethod(this, &CoreThread::setLidState, Qt::QueuedConnection, manual_control, manual_state); + return; + } + + if (!System::IsValid()) + return; + + CDROM::SetLidState(manual_control, manual_state); +} + void CoreThread::reloadCheats(bool reload_files, bool reload_enabled_list, bool verbose, bool verbose_if_changed) { if (!isCurrentThread()) diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index 99161922a..566e2ca46 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -153,6 +153,7 @@ public: void setSystemPaused(bool paused); void changeDisc(const QString& new_disc_path, bool reset_system, bool check_memcard_busy); void changeDiscFromPlaylist(quint32 index); + void setLidState(bool manual_control, bool manual_state); void loadState(const QString& path); void loadState(bool global, qint32 slot); void saveState(const QString& path);