diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index b558f9532..a21f409f3 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -13,6 +13,7 @@ add_library(common cd_image_bin.cpp cd_image_cue.cpp cd_image_chd.cpp + cd_image_device.cpp cd_image_ecm.cpp cd_image_hasher.cpp cd_image_hasher.h diff --git a/src/common/cd_image.cpp b/src/common/cd_image.cpp index 6127156af..5c5dc5fb6 100644 --- a/src/common/cd_image.cpp +++ b/src/common/cd_image.cpp @@ -66,6 +66,9 @@ std::unique_ptr CDImage::Open(const char* filename, Common::Error* erro return OpenM3uImage(filename, error); } + if (IsDeviceName(filename)) + return OpenDeviceImage(filename, error); + #undef CASE_COMPARE Log_ErrorPrintf("Unknown extension '%s' from filename '%s'", extension, filename); diff --git a/src/common/cd_image.h b/src/common/cd_image.h index d9755a7f8..03d88b27d 100644 --- a/src/common/cd_image.h +++ b/src/common/cd_image.h @@ -203,6 +203,12 @@ public: // Helper functions. static u32 GetBytesPerSector(TrackMode mode); + /// Returns a list of physical CD-ROM devices, .first being the device path, .second being the device name. + static std::vector> GetDeviceList(); + + /// Returns true if the specified filename is a CD-ROM device name. + static bool IsDeviceName(const char* filename); + // Opening disc image. static std::unique_ptr Open(const char* filename, Common::Error* error); static std::unique_ptr OpenBinImage(const char* filename, Common::Error* error); @@ -212,6 +218,7 @@ public: static std::unique_ptr OpenMdsImage(const char* filename, Common::Error* error); static std::unique_ptr OpenPBPImage(const char* filename, Common::Error* error); static std::unique_ptr OpenM3uImage(const char* filename, Common::Error* error); + static std::unique_ptr OpenDeviceImage(const char* filename, Common::Error* error); static std::unique_ptr CreateMemoryImage(CDImage* image, ProgressCallback* progress = ProgressCallback::NullProgressCallback); static std::unique_ptr OverlayPPFPatch(const char* filename, std::unique_ptr parent_image, diff --git a/src/common/cd_image_device.cpp b/src/common/cd_image_device.cpp new file mode 100644 index 000000000..5087d0e0e --- /dev/null +++ b/src/common/cd_image_device.cpp @@ -0,0 +1,548 @@ +#include "assert.h" +#include "cd_image.h" +#include "error.h" +#include "log.h" +#include "string_util.h" +#include +#include +#include +#include +Log_SetChannel(CDImageDevice); + +enum : u32 +{ + MAX_TRACK_NUMBER = 99, + ALL_SUBCODE_SIZE = 96, +}; + +static u32 BEToU32(const u8* val) +{ + return (static_cast(val[0]) << 24) | (static_cast(val[1]) << 16) | (static_cast(val[2]) << 8) | + static_cast(val[3]); +} + +static void U16ToBE(u8* beval, u16 leval) +{ + beval[0] = static_cast(leval >> 8); + beval[1] = static_cast(leval); +} + +// Adapted from +// https://github.com/saramibreak/DiscImageCreator/blob/5a8fe21730872d67991211f1319c87f0780f2d0f/DiscImageCreator/convert.cpp +static void DeinterleaveSubcode(const u8* subcode_in, u8* subcode_out) +{ + std::memset(subcode_out, 0, ALL_SUBCODE_SIZE); + + int row = 0; + for (int bitNum = 0; bitNum < 8; bitNum++) + { + for (int nColumn = 0; nColumn < ALL_SUBCODE_SIZE; row++) + { + u32 mask = 0x80; + for (int nShift = 0; nShift < 8; nShift++, nColumn++) + { + const int n = nShift - bitNum; + if (n > 0) + { + subcode_out[row] |= static_cast((subcode_in[nColumn] >> n) & mask); + } + else + { + subcode_out[row] |= static_cast((subcode_in[nColumn] << std::abs(n)) & mask); + } + mask >>= 1; + } + } + } +} + +#if defined(_WIN32) && !defined(_UWP) + +// The include order here is critical. +// clang-format off +#include "windows_headers.h" +#include +#include +#include +// clang-format on + +class CDImageDeviceWin32 : public CDImage +{ +public: + CDImageDeviceWin32(); + ~CDImageDeviceWin32() override; + + bool Open(const char* filename, Common::Error* error); + + bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override; + bool HasNonStandardSubchannel() const override; + +protected: + bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override; + +private: + struct SPTDBuffer + { + SCSI_PASS_THROUGH_DIRECT cmd; + u8 sense[20]; + }; + + static void FillSPTD(SPTDBuffer* sptd, u32 sector_number, bool include_subq, void* buffer); + + bool ReadSectorToBuffer(u64 offset); + bool DetermineReadMode(); + + HANDLE m_hDevice = INVALID_HANDLE_VALUE; + + u64 m_buffer_offset = ~static_cast(0); + + bool m_use_sptd = true; + bool m_read_subcode = false; + + std::array m_buffer; + std::array m_deinterleaved_subcode; + std::array m_subq; +}; + +CDImageDeviceWin32::CDImageDeviceWin32() = default; + +CDImageDeviceWin32::~CDImageDeviceWin32() +{ + if (m_hDevice != INVALID_HANDLE_VALUE) + CloseHandle(m_hDevice); +} + +bool CDImageDeviceWin32::Open(const char* filename, Common::Error* error) +{ + m_filename = filename; + m_hDevice = CreateFile(filename, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, 0, NULL); + if (m_hDevice == INVALID_HANDLE_VALUE) + { + m_hDevice = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, NULL); + if (m_hDevice != INVALID_HANDLE_VALUE) + { + m_use_sptd = false; + } + else + { + Log_ErrorPrintf("CreateFile('%s') failed: %08X", filename, GetLastError()); + if (error) + error->SetWin32(GetLastError()); + + return false; + } + } + + // Set it to 4x speed. A good balance between readahead and spinning up way too high. + static constexpr u32 READ_SPEED_MULTIPLIER = 4; + static constexpr u32 READ_SPEED_KBS = (DATA_SECTOR_SIZE * FRAMES_PER_SECOND * 8) / 1024; + CDROM_SET_SPEED set_speed = {CdromSetSpeed, READ_SPEED_KBS, 0, CdromDefaultRotation}; + if (!DeviceIoControl(m_hDevice, IOCTL_CDROM_SET_SPEED, &set_speed, sizeof(set_speed), nullptr, 0, nullptr, nullptr)) + Log_WarningPrintf("DeviceIoControl(IOCTL_CDROM_SET_SPEED) failed: %08X", GetLastError()); + + CDROM_READ_TOC_EX read_toc_ex = {}; + read_toc_ex.Format = CDROM_READ_TOC_EX_FORMAT_TOC; + read_toc_ex.Msf = 0; + read_toc_ex.SessionTrack = 1; + + CDROM_TOC toc = {}; + U16ToBE(toc.Length, sizeof(toc) - sizeof(UCHAR) * 2); + + DWORD bytes_returned; + if (!DeviceIoControl(m_hDevice, IOCTL_CDROM_READ_TOC_EX, &read_toc_ex, sizeof(read_toc_ex), &toc, sizeof(toc), + &bytes_returned, nullptr) || + toc.LastTrack < toc.FirstTrack) + { + Log_ErrorPrintf("DeviceIoCtl(IOCTL_CDROM_READ_TOC_EX) failed: %08X", GetLastError()); + if (error) + error->SetWin32(GetLastError()); + + return false; + } + + DWORD last_track_address = 0; + LBA disc_lba = 0; + Log_DevPrintf("FirstTrack=%u, LastTrack=%u", toc.FirstTrack, toc.LastTrack); + + const u32 num_tracks_to_check = (toc.LastTrack - toc.FirstTrack) + 1 + 1; + for (u32 track_index = 0; track_index < num_tracks_to_check; track_index++) + { + const TRACK_DATA& td = toc.TrackData[track_index]; + const u8 track_num = td.TrackNumber; + const DWORD track_address = BEToU32(td.Address); + Log_DevPrintf(" [%u]: Num=%02X, Address=%u", track_index, track_num, track_address); + + // fill in the previous track's length + if (!m_tracks.empty()) + { + if (track_num < m_tracks.back().track_number) + { + Log_ErrorPrintf("Invalid TOC, track %u less than %u", track_num, m_tracks.back().track_number); + return false; + } + + const LBA previous_track_length = static_cast(track_address - last_track_address); + m_tracks.back().length += previous_track_length; + m_indices.back().length += previous_track_length; + disc_lba += previous_track_length; + } + + if (track_num == LEAD_OUT_TRACK_NUMBER) + { + AddLeadOutIndex(); + break; + } + + // precompute subchannel q flags for the whole track + SubChannelQ::Control control{}; + control.bits = td.Adr | (td.Control << 4); + + const LBA track_lba = static_cast(track_address); + const TrackMode track_mode = control.data ? CDImage::TrackMode::Mode2Raw : CDImage::TrackMode::Audio; + + // TODO: How the hell do we handle pregaps here? + const u32 pregap_frames = + (track_num <= MAX_TRACK_NUMBER && ((control.data && track_index == 0) || (!control.data && track_index != 0))) ? + 150 : + 0; + if (pregap_frames > 0) + { + Index pregap_index = {}; + pregap_index.start_lba_on_disc = disc_lba; + pregap_index.start_lba_in_track = static_cast(-static_cast(pregap_frames)); + pregap_index.length = pregap_frames; + pregap_index.track_number = track_num; + pregap_index.index_number = 0; + pregap_index.mode = track_mode; + pregap_index.control.bits = control.bits; + pregap_index.is_pregap = true; + m_indices.push_back(pregap_index); + disc_lba += pregap_frames; + } + + // index 1, will be filled in next iteration + if (track_num <= MAX_TRACK_NUMBER) + { + // add the track itself + m_tracks.push_back(Track{track_num, disc_lba, static_cast(m_indices.size()), 0, track_mode, control}); + + Index index1; + index1.start_lba_on_disc = disc_lba; + index1.start_lba_in_track = 0; + index1.length = 0; + index1.track_number = track_num; + index1.index_number = 1; + index1.file_index = 0; + index1.file_sector_size = 2048; + index1.file_offset = static_cast(track_address) * index1.file_sector_size; + index1.mode = track_mode; + index1.control.bits = control.bits; + index1.is_pregap = false; + m_indices.push_back(index1); + } + } + + if (m_tracks.empty()) + { + Log_ErrorPrintf("File '%s' contains no tracks", filename); + if (error) + error->SetFormattedMessage("File '%s' contains no tracks", filename); + return false; + } + + m_lba_count = disc_lba; + + Log_DevPrintf("%u tracks, %u indices, %u lbas", static_cast(m_tracks.size()), static_cast(m_indices.size()), + static_cast(m_lba_count)); + for (u32 i = 0; i < m_tracks.size(); i++) + { + Log_DevPrintf(" Track %u: Start %u, length %u, mode %u, control 0x%02X", i, + static_cast(m_tracks[i].track_number), static_cast(m_tracks[i].start_lba), + static_cast(m_tracks[i].mode), static_cast(m_tracks[i].control.bits)); + } + for (u32 i = 0; i < m_indices.size(); i++) + { + Log_DevPrintf(" Index %u: Track %u, Index %u, Start %u, length %u, file sector size %u, file offset %" PRIu64, i, + static_cast(m_indices[i].track_number), static_cast(m_indices[i].index_number), + static_cast(m_indices[i].start_lba_on_disc), static_cast(m_indices[i].length), + static_cast(m_indices[i].file_sector_size), m_indices[i].file_offset); + } + + if (!DetermineReadMode()) + { + Log_ErrorPrintf("Could not determine read mode"); + if (error) + error->SetMessage("Could not determine read mode"); + + return false; + } + + return Seek(1, Position{0, 0, 0}); +} + +bool CDImageDeviceWin32::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) +{ + if (index.file_sector_size == 0 || !m_read_subcode) + return CDImage::ReadSubChannelQ(subq, index, lba_in_index); + + const u64 offset = index.file_offset + static_cast(lba_in_index) * index.file_sector_size; + if (m_buffer_offset != offset && !ReadSectorToBuffer(offset)) + return false; + + // P, Q, ... + std::memcpy(subq->data.data(), m_subq.data(), SUBCHANNEL_BYTES_PER_FRAME); + return true; +} + +bool CDImageDeviceWin32::HasNonStandardSubchannel() const +{ + return true; +} + +bool CDImageDeviceWin32::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) +{ + if (index.file_sector_size == 0) + return false; + + const u64 offset = index.file_offset + static_cast(lba_in_index) * index.file_sector_size; + if (m_buffer_offset != offset && !ReadSectorToBuffer(offset)) + return false; + + std::memcpy(buffer, m_buffer.data(), RAW_SECTOR_SIZE); + return true; +} + +void CDImageDeviceWin32::FillSPTD(SPTDBuffer* sptd, u32 sector_number, bool include_subq, void* buffer) +{ + std::memset(sptd, 0, sizeof(SPTDBuffer)); + + sptd->cmd.Length = sizeof(sptd->cmd); + sptd->cmd.CdbLength = 12; + sptd->cmd.SenseInfoLength = sizeof(sptd->sense); + sptd->cmd.DataIn = SCSI_IOCTL_DATA_IN; + sptd->cmd.DataTransferLength = include_subq ? (RAW_SECTOR_SIZE + SUBCHANNEL_BYTES_PER_FRAME) : RAW_SECTOR_SIZE; + sptd->cmd.TimeOutValue = 10; + sptd->cmd.SenseInfoOffset = offsetof(SPTDBuffer, sense); + sptd->cmd.DataBuffer = buffer; + + sptd->cmd.Cdb[0] = 0xBE; // READ CD + sptd->cmd.Cdb[1] = 0x00; // sector type + sptd->cmd.Cdb[2] = Truncate8(sector_number >> 24); // Starting LBA + sptd->cmd.Cdb[3] = Truncate8(sector_number >> 16); + sptd->cmd.Cdb[4] = Truncate8(sector_number >> 8); + sptd->cmd.Cdb[5] = Truncate8(sector_number); + sptd->cmd.Cdb[6] = 0x00; // Transfer Count + sptd->cmd.Cdb[7] = 0x00; + sptd->cmd.Cdb[8] = 0x01; + sptd->cmd.Cdb[9] = (1 << 7) | // include sync + (0b11 << 5) | // include header codes + (1 << 4) | // include user data + (1 << 3) | // edc/ecc + (0 << 2); // don't include C2 data + sptd->cmd.Cdb[10] = (include_subq ? (0b010 << 0) : (0b000 << 0)); // subq selection +} + +bool CDImageDeviceWin32::ReadSectorToBuffer(u64 offset) +{ + if (m_use_sptd) + { + const u32 sector_number = static_cast(offset / 2048); + + SPTDBuffer sptd = {}; + FillSPTD(&sptd, sector_number, m_read_subcode, m_buffer.data()); + + const u32 expected_bytes = sptd.cmd.DataTransferLength; + DWORD bytes_returned; + if (!DeviceIoControl(m_hDevice, IOCTL_SCSI_PASS_THROUGH_DIRECT, &sptd, sizeof(sptd), &sptd, sizeof(sptd), + &bytes_returned, nullptr) && + sptd.cmd.ScsiStatus == 0x00) + { + Log_ErrorPrintf("DeviceIoControl(IOCTL_SCSI_PASS_THROUGH_DIRECT) for offset %" PRIu64 + " failed: %08X Status 0x%02X", + offset, GetLastError(), sptd.cmd.ScsiStatus); + return false; + } + + if (sptd.cmd.DataTransferLength != expected_bytes) + Log_WarningPrintf("Only read %u of %u bytes", static_cast(sptd.cmd.DataTransferLength), expected_bytes); + + if (m_read_subcode) + std::memcpy(m_subq.data(), &m_buffer[RAW_SECTOR_SIZE], SUBCHANNEL_BYTES_PER_FRAME); + } + else + { + RAW_READ_INFO rri; + rri.DiskOffset.QuadPart = offset; + rri.SectorCount = 1; + rri.TrackMode = RawWithSubCode; + + DWORD bytes_returned; + if (!DeviceIoControl(m_hDevice, IOCTL_CDROM_RAW_READ, &rri, sizeof(rri), m_buffer.data(), + static_cast(m_buffer.size()), &bytes_returned, nullptr)) + { + Log_ErrorPrintf("DeviceIoControl(IOCTL_CDROM_RAW_READ) for offset %" PRIu64 " failed: %08X", offset, + GetLastError()); + return false; + } + + if (bytes_returned != m_buffer.size()) + Log_WarningPrintf("Only read %u of %u bytes", bytes_returned, static_cast(m_buffer.size())); + + // P, Q, ... + DeinterleaveSubcode(&m_buffer[RAW_SECTOR_SIZE], m_deinterleaved_subcode.data()); + std::memcpy(m_subq.data(), &m_deinterleaved_subcode[SUBCHANNEL_BYTES_PER_FRAME], SUBCHANNEL_BYTES_PER_FRAME); + } + + m_buffer_offset = offset; + return true; +} + +bool CDImageDeviceWin32::DetermineReadMode() +{ + // Prefer raw reads if we can use them + RAW_READ_INFO rri; + rri.DiskOffset.QuadPart = 0; + rri.SectorCount = 1; + rri.TrackMode = RawWithSubCode; + + DWORD bytes_returned; + if (DeviceIoControl(m_hDevice, IOCTL_CDROM_RAW_READ, &rri, sizeof(rri), m_buffer.data(), + static_cast(m_buffer.size()), &bytes_returned, nullptr) && + bytes_returned == CD_RAW_SECTOR_WITH_SUBCODE_SIZE) + { + SubChannelQ subq; + DeinterleaveSubcode(&m_buffer[RAW_SECTOR_SIZE], m_deinterleaved_subcode.data()); + std::memcpy(&subq, &m_deinterleaved_subcode[SUBCHANNEL_BYTES_PER_FRAME], SUBCHANNEL_BYTES_PER_FRAME); + + m_use_sptd = false; + m_read_subcode = true; + + if (subq.IsCRCValid()) + { + Log_DevPrintf("Raw read returned invalid SubQ CRC (got %02X expected %02X)", static_cast(subq.crc), + static_cast(SubChannelQ::ComputeCRC(subq.data))); + + m_read_subcode = false; + } + else + { + Log_DevPrintf("Using raw reads with subcode"); + } + + return true; + } + + Log_DevPrintf("DeviceIoControl(IOCTL_CDROM_RAW_READ) failed: %08X, %u bytes returned, trying SPTD", GetLastError(), + bytes_returned); + + SPTDBuffer sptd = {}; + FillSPTD(&sptd, 0, true, m_buffer.data()); + + if (DeviceIoControl(m_hDevice, IOCTL_SCSI_PASS_THROUGH_DIRECT, &sptd, sizeof(sptd), &sptd, sizeof(sptd), + &bytes_returned, nullptr) && + sptd.cmd.ScsiStatus == 0x00) + { + // check the validity of the subchannel data. this assumes that the first sector has a valid subq, which it should + // in all PS1 games. + SubChannelQ subq; + std::memcpy(&subq, &m_buffer[RAW_SECTOR_SIZE], sizeof(subq)); + if (subq.IsCRCValid()) + { + Log_DevPrintf("Using SPTD reads with subq (%u, status 0x%02X)", sptd.cmd.DataTransferLength, sptd.cmd.ScsiStatus); + m_read_subcode = true; + m_use_sptd = true; + return true; + } + else + { + Log_DevPrintf("SPTD read returned invalid SubQ CRC (got %02X expected %02X)", static_cast(subq.crc), + static_cast(SubChannelQ::ComputeCRC(subq.data))); + } + } + + // try without subcode + FillSPTD(&sptd, 0, false, m_buffer.data()); + if (DeviceIoControl(m_hDevice, IOCTL_SCSI_PASS_THROUGH_DIRECT, &sptd, sizeof(sptd), &sptd, sizeof(sptd), + &bytes_returned, nullptr) && + sptd.cmd.ScsiStatus == 0x00) + { + Log_DevPrintf("Using SPTD reads without subq (%u, status 0x%02X)", sptd.cmd.DataTransferLength, + sptd.cmd.ScsiStatus); + m_read_subcode = false; + m_use_sptd = true; + return true; + } + + Log_ErrorPrintf("No working read mode found (status 0x%02X, err %08X)", sptd.cmd.ScsiStatus, GetLastError()); + return false; +} + +std::unique_ptr CDImage::OpenDeviceImage(const char* filename, Common::Error* error) +{ + std::unique_ptr image = std::make_unique(); + if (!image->Open(filename, error)) + return {}; + + return image; +} + +std::vector> CDImage::GetDeviceList() +{ + std::vector> ret; + + char buf[256]; + if (GetLogicalDriveStringsA(sizeof(buf), buf) != 0) + { + const char* ptr = buf; + while (*ptr != '\0') + { + std::size_t len = std::strlen(ptr); + const DWORD type = GetDriveTypeA(ptr); + if (type != DRIVE_CDROM) + { + ptr += len + 1u; + continue; + } + + // Drop the trailing slash. + const std::size_t append_len = (ptr[len - 1] == '\\') ? (len - 1) : len; + + std::string path; + path.append("\\\\.\\"); + path.append(ptr, append_len); + + std::string name(ptr, append_len); + + ret.emplace_back(std::move(path), std::move(name)); + + ptr += len + 1u; + } + } + + return ret; +} + +bool CDImage::IsDeviceName(const char* filename) +{ + return StringUtil::StartsWith(filename, "\\\\.\\"); +} + +#else + +std::unique_ptr CDImage::OpenDeviceImage(const char* filename, Common::Error* error) +{ + return {}; +} + +std::vector> CDImage::GetDeviceList() +{ + return {}; +} + +bool CDImage::IsDeviceName(const char* filename) +{ + return false; +} + +#endif \ No newline at end of file diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj index f14160ae9..b27d2fc4d 100644 --- a/src/common/common.vcxproj +++ b/src/common/common.vcxproj @@ -1,7 +1,6 @@  - @@ -93,6 +92,7 @@ + @@ -165,20 +165,15 @@ - {EE054E08-3799-4A59-A422-18259C105FFD} - - - $(IntDir)/%(RelativeDir)/ - - + \ No newline at end of file diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters index 1b68fbbbb..4195d1136 100644 --- a/src/common/common.vcxproj.filters +++ b/src/common/common.vcxproj.filters @@ -263,6 +263,7 @@ d3d12 + @@ -284,4 +285,4 @@ {358e11c4-34af-4169-9a66-ec66342a6a2f} - + \ No newline at end of file diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 969bedd07..1b8a96a32 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -3,6 +3,7 @@ #include "autoupdaterdialog.h" #include "cheatmanagerdialog.h" #include "common/assert.h" +#include "common/cd_image.h" #include "core/host_display.h" #include "core/settings.h" #include "core/system.h" @@ -27,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -501,7 +503,7 @@ void MainWindow::onApplicationStateChanged(Qt::ApplicationState state) } } -void MainWindow::onStartDiscActionTriggered() +void MainWindow::onStartFileActionTriggered() { QString filename = QDir::toNativeSeparators( QFileDialog::getOpenFileName(this, tr("Select Disc Image"), QString(), tr(DISC_IMAGE_FILTER), nullptr)); @@ -511,6 +513,44 @@ void MainWindow::onStartDiscActionTriggered() m_host_interface->bootSystem(std::make_shared(filename.toStdString())); } +void MainWindow::onStartDiscActionTriggered() +{ + const auto devices = CDImage::GetDeviceList(); + if (devices.empty()) + { + QMessageBox::critical(this, tr("Start Disc"), + tr("Could not find any CD-ROM devices. Please ensure you have a CD-ROM drive connected and " + "sufficient permissions to access it.")); + return; + } + + // if there's only one, select it automatically + if (devices.size() == 1) + { + m_host_interface->bootSystem(std::make_shared(std::move(devices.front().first))); + return; + } + + QStringList input_options; + for (const auto& [path, name] : devices) + input_options.append(tr("%1 (%2)").arg(QString::fromStdString(name)).arg(QString::fromStdString(path))); + + QInputDialog input_dialog(this); + input_dialog.setLabelText(tr("Select disc drive:")); + input_dialog.setInputMode(QInputDialog::TextInput); + input_dialog.setOptions(QInputDialog::UseListViewForComboBoxItems); + input_dialog.setComboBoxEditable(false); + input_dialog.setComboBoxItems(std::move(input_options)); + if (input_dialog.exec() == 0) + return; + + const int selected_index = input_dialog.comboBoxItems().indexOf(input_dialog.textValue()); + if (selected_index < 0 || static_cast(selected_index) >= devices.size()) + return; + + m_host_interface->bootSystem(std::make_shared(std::move(devices[selected_index].first))); +} + void MainWindow::onStartBIOSActionTriggered() { m_host_interface->bootSystem(std::make_shared()); @@ -895,6 +935,7 @@ void MainWindow::setupAdditionalUi() void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevos_challenge_mode) { + m_ui.actionStartFile->setDisabled(starting || running); m_ui.actionStartDisc->setDisabled(starting || running); m_ui.actionStartBios->setDisabled(starting || running); m_ui.actionResumeLastState->setDisabled(starting || running || cheevos_challenge_mode); @@ -1033,6 +1074,7 @@ void MainWindow::connectSignals() connect(qApp, &QGuiApplication::applicationStateChanged, this, &MainWindow::onApplicationStateChanged); + connect(m_ui.actionStartFile, &QAction::triggered, this, &MainWindow::onStartFileActionTriggered); connect(m_ui.actionStartDisc, &QAction::triggered, this, &MainWindow::onStartDiscActionTriggered); connect(m_ui.actionStartBios, &QAction::triggered, this, &MainWindow::onStartBIOSActionTriggered); connect(m_ui.actionResumeLastState, &QAction::triggered, m_host_interface, diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index 6aa21eda6..06c12600e 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -77,6 +77,7 @@ private Q_SLOTS: void onRunningGameChanged(const QString& filename, const QString& game_code, const QString& game_title); void onApplicationStateChanged(Qt::ApplicationState state); + void onStartFileActionTriggered(); void onStartDiscActionTriggered(); void onStartBIOSActionTriggered(); void onChangeDiscFromFileActionTriggered(); diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index 1a88075dc..e1cc0db1e 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -81,6 +81,7 @@ :/icons/document-save.png:/icons/document-save.png + @@ -254,7 +255,7 @@ false - + @@ -271,6 +272,15 @@ + + + + :/icons/media-optical.png:/icons/media-optical.png + + + Start &File... + +