Merge pull request #2441 from stenzek/cd-image-device

CDImage: Add CD-ROM device implementation
This commit is contained in:
Connor McLaughlin 2021-07-26 01:33:02 +10:00 committed by GitHub
commit b62ed5561c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 618 additions and 10 deletions

View file

@ -13,6 +13,7 @@ add_library(common
cd_image_bin.cpp cd_image_bin.cpp
cd_image_cue.cpp cd_image_cue.cpp
cd_image_chd.cpp cd_image_chd.cpp
cd_image_device.cpp
cd_image_ecm.cpp cd_image_ecm.cpp
cd_image_hasher.cpp cd_image_hasher.cpp
cd_image_hasher.h cd_image_hasher.h

View file

@ -66,6 +66,9 @@ std::unique_ptr<CDImage> CDImage::Open(const char* filename, Common::Error* erro
return OpenM3uImage(filename, error); return OpenM3uImage(filename, error);
} }
if (IsDeviceName(filename))
return OpenDeviceImage(filename, error);
#undef CASE_COMPARE #undef CASE_COMPARE
Log_ErrorPrintf("Unknown extension '%s' from filename '%s'", extension, filename); Log_ErrorPrintf("Unknown extension '%s' from filename '%s'", extension, filename);

View file

@ -203,6 +203,12 @@ public:
// Helper functions. // Helper functions.
static u32 GetBytesPerSector(TrackMode mode); 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<std::pair<std::string, std::string>> GetDeviceList();
/// Returns true if the specified filename is a CD-ROM device name.
static bool IsDeviceName(const char* filename);
// Opening disc image. // Opening disc image.
static std::unique_ptr<CDImage> Open(const char* filename, Common::Error* error); static std::unique_ptr<CDImage> Open(const char* filename, Common::Error* error);
static std::unique_ptr<CDImage> OpenBinImage(const char* filename, Common::Error* error); static std::unique_ptr<CDImage> OpenBinImage(const char* filename, Common::Error* error);
@ -212,6 +218,7 @@ public:
static std::unique_ptr<CDImage> OpenMdsImage(const char* filename, Common::Error* error); static std::unique_ptr<CDImage> OpenMdsImage(const char* filename, Common::Error* error);
static std::unique_ptr<CDImage> OpenPBPImage(const char* filename, Common::Error* error); static std::unique_ptr<CDImage> OpenPBPImage(const char* filename, Common::Error* error);
static std::unique_ptr<CDImage> OpenM3uImage(const char* filename, Common::Error* error); static std::unique_ptr<CDImage> OpenM3uImage(const char* filename, Common::Error* error);
static std::unique_ptr<CDImage> OpenDeviceImage(const char* filename, Common::Error* error);
static std::unique_ptr<CDImage> static std::unique_ptr<CDImage>
CreateMemoryImage(CDImage* image, ProgressCallback* progress = ProgressCallback::NullProgressCallback); CreateMemoryImage(CDImage* image, ProgressCallback* progress = ProgressCallback::NullProgressCallback);
static std::unique_ptr<CDImage> OverlayPPFPatch(const char* filename, std::unique_ptr<CDImage> parent_image, static std::unique_ptr<CDImage> OverlayPPFPatch(const char* filename, std::unique_ptr<CDImage> parent_image,

View file

@ -0,0 +1,548 @@
#include "assert.h"
#include "cd_image.h"
#include "error.h"
#include "log.h"
#include "string_util.h"
#include <algorithm>
#include <cerrno>
#include <cinttypes>
#include <cmath>
Log_SetChannel(CDImageDevice);
enum : u32
{
MAX_TRACK_NUMBER = 99,
ALL_SUBCODE_SIZE = 96,
};
static u32 BEToU32(const u8* val)
{
return (static_cast<u32>(val[0]) << 24) | (static_cast<u32>(val[1]) << 16) | (static_cast<u32>(val[2]) << 8) |
static_cast<u32>(val[3]);
}
static void U16ToBE(u8* beval, u16 leval)
{
beval[0] = static_cast<u8>(leval >> 8);
beval[1] = static_cast<u8>(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<u8>((subcode_in[nColumn] >> n) & mask);
}
else
{
subcode_out[row] |= static_cast<u8>((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 <winioctl.h>
#include <ntddcdrm.h>
#include <ntddscsi.h>
// 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<u64>(0);
bool m_use_sptd = true;
bool m_read_subcode = false;
std::array<u8, CD_RAW_SECTOR_WITH_SUBCODE_SIZE> m_buffer;
std::array<u8, ALL_SUBCODE_SIZE> m_deinterleaved_subcode;
std::array<u8, SUBCHANNEL_BYTES_PER_FRAME> 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<LBA>(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<LBA>(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<LBA>(-static_cast<s32>(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<u32>(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<u64>(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<u32>(m_tracks.size()), static_cast<u32>(m_indices.size()),
static_cast<u32>(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<u32>(m_tracks[i].track_number), static_cast<u32>(m_tracks[i].start_lba),
static_cast<u32>(m_tracks[i].mode), static_cast<u32>(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<u32>(m_indices[i].track_number), static_cast<u32>(m_indices[i].index_number),
static_cast<u32>(m_indices[i].start_lba_on_disc), static_cast<u32>(m_indices[i].length),
static_cast<u32>(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<u64>(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<u64>(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<u32>(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<u32>(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<DWORD>(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<unsigned>(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<DWORD>(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<unsigned>(subq.crc),
static_cast<unsigned>(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<unsigned>(subq.crc),
static_cast<unsigned>(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> CDImage::OpenDeviceImage(const char* filename, Common::Error* error)
{
std::unique_ptr<CDImageDeviceWin32> image = std::make_unique<CDImageDeviceWin32>();
if (!image->Open(filename, error))
return {};
return image;
}
std::vector<std::pair<std::string, std::string>> CDImage::GetDeviceList()
{
std::vector<std::pair<std::string, std::string>> 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> CDImage::OpenDeviceImage(const char* filename, Common::Error* error)
{
return {};
}
std::vector<std::pair<std::string, std::string>> CDImage::GetDeviceList()
{
return {};
}
bool CDImage::IsDeviceName(const char* filename)
{
return false;
}
#endif

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\dep\msvc\vsprops\Configurations.props" /> <Import Project="..\..\dep\msvc\vsprops\Configurations.props" />
<ItemGroup> <ItemGroup>
<ClInclude Include="align.h" /> <ClInclude Include="align.h" />
<ClInclude Include="assert.h" /> <ClInclude Include="assert.h" />
@ -93,6 +92,7 @@
<ClCompile Include="cd_image_bin.cpp" /> <ClCompile Include="cd_image_bin.cpp" />
<ClCompile Include="cd_image_chd.cpp" /> <ClCompile Include="cd_image_chd.cpp" />
<ClCompile Include="cd_image_cue.cpp" /> <ClCompile Include="cd_image_cue.cpp" />
<ClCompile Include="cd_image_device.cpp" />
<ClCompile Include="cd_image_ecm.cpp" /> <ClCompile Include="cd_image_ecm.cpp" />
<ClCompile Include="cd_image_hasher.cpp" /> <ClCompile Include="cd_image_hasher.cpp" />
<ClCompile Include="cd_image_m3u.cpp" /> <ClCompile Include="cd_image_m3u.cpp" />
@ -165,20 +165,15 @@
<ItemGroup> <ItemGroup>
<Natvis Include="bitfield.natvis" /> <Natvis Include="bitfield.natvis" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Globals"> <PropertyGroup Label="Globals">
<ProjectGuid>{EE054E08-3799-4A59-A422-18259C105FFD}</ProjectGuid> <ProjectGuid>{EE054E08-3799-4A59-A422-18259C105FFD}</ProjectGuid>
</PropertyGroup> </PropertyGroup>
<Import Project="..\..\dep\msvc\vsprops\StaticLibrary.props" /> <Import Project="..\..\dep\msvc\vsprops\StaticLibrary.props" />
<Import Project="common.props" /> <Import Project="common.props" />
<ItemDefinitionGroup> <ItemDefinitionGroup>
<ClCompile> <ClCompile>
<ObjectFileName>$(IntDir)/%(RelativeDir)/</ObjectFileName> <ObjectFileName>$(IntDir)/%(RelativeDir)/</ObjectFileName>
</ClCompile> </ClCompile>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<Import Project="..\..\dep\msvc\vsprops\Targets.props" /> <Import Project="..\..\dep\msvc\vsprops\Targets.props" />
</Project> </Project>

View file

@ -263,6 +263,7 @@
<ClCompile Include="d3d12\shader_cache.cpp"> <ClCompile Include="d3d12\shader_cache.cpp">
<Filter>d3d12</Filter> <Filter>d3d12</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="cd_image_device.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Natvis Include="bitfield.natvis" /> <Natvis Include="bitfield.natvis" />

View file

@ -3,6 +3,7 @@
#include "autoupdaterdialog.h" #include "autoupdaterdialog.h"
#include "cheatmanagerdialog.h" #include "cheatmanagerdialog.h"
#include "common/assert.h" #include "common/assert.h"
#include "common/cd_image.h"
#include "core/host_display.h" #include "core/host_display.h"
#include "core/settings.h" #include "core/settings.h"
#include "core/system.h" #include "core/system.h"
@ -27,6 +28,7 @@
#include <QtGui/QCursor> #include <QtGui/QCursor>
#include <QtGui/QWindowStateChangeEvent> #include <QtGui/QWindowStateChangeEvent>
#include <QtWidgets/QFileDialog> #include <QtWidgets/QFileDialog>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QMessageBox> #include <QtWidgets/QMessageBox>
#include <QtWidgets/QStyleFactory> #include <QtWidgets/QStyleFactory>
#include <cmath> #include <cmath>
@ -501,7 +503,7 @@ void MainWindow::onApplicationStateChanged(Qt::ApplicationState state)
} }
} }
void MainWindow::onStartDiscActionTriggered() void MainWindow::onStartFileActionTriggered()
{ {
QString filename = QDir::toNativeSeparators( QString filename = QDir::toNativeSeparators(
QFileDialog::getOpenFileName(this, tr("Select Disc Image"), QString(), tr(DISC_IMAGE_FILTER), nullptr)); 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<SystemBootParameters>(filename.toStdString())); m_host_interface->bootSystem(std::make_shared<SystemBootParameters>(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<SystemBootParameters>(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<u32>(selected_index) >= devices.size())
return;
m_host_interface->bootSystem(std::make_shared<SystemBootParameters>(std::move(devices[selected_index].first)));
}
void MainWindow::onStartBIOSActionTriggered() void MainWindow::onStartBIOSActionTriggered()
{ {
m_host_interface->bootSystem(std::make_shared<SystemBootParameters>()); m_host_interface->bootSystem(std::make_shared<SystemBootParameters>());
@ -895,6 +935,7 @@ void MainWindow::setupAdditionalUi()
void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevos_challenge_mode) 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.actionStartDisc->setDisabled(starting || running);
m_ui.actionStartBios->setDisabled(starting || running); m_ui.actionStartBios->setDisabled(starting || running);
m_ui.actionResumeLastState->setDisabled(starting || running || cheevos_challenge_mode); m_ui.actionResumeLastState->setDisabled(starting || running || cheevos_challenge_mode);
@ -1033,6 +1074,7 @@ void MainWindow::connectSignals()
connect(qApp, &QGuiApplication::applicationStateChanged, this, &MainWindow::onApplicationStateChanged); 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.actionStartDisc, &QAction::triggered, this, &MainWindow::onStartDiscActionTriggered);
connect(m_ui.actionStartBios, &QAction::triggered, this, &MainWindow::onStartBIOSActionTriggered); connect(m_ui.actionStartBios, &QAction::triggered, this, &MainWindow::onStartBIOSActionTriggered);
connect(m_ui.actionResumeLastState, &QAction::triggered, m_host_interface, connect(m_ui.actionResumeLastState, &QAction::triggered, m_host_interface,

View file

@ -77,6 +77,7 @@ private Q_SLOTS:
void onRunningGameChanged(const QString& filename, const QString& game_code, const QString& game_title); void onRunningGameChanged(const QString& filename, const QString& game_code, const QString& game_title);
void onApplicationStateChanged(Qt::ApplicationState state); void onApplicationStateChanged(Qt::ApplicationState state);
void onStartFileActionTriggered();
void onStartDiscActionTriggered(); void onStartDiscActionTriggered();
void onStartBIOSActionTriggered(); void onStartBIOSActionTriggered();
void onChangeDiscFromFileActionTriggered(); void onChangeDiscFromFileActionTriggered();

View file

@ -81,6 +81,7 @@
<normaloff>:/icons/document-save.png</normaloff>:/icons/document-save.png</iconset> <normaloff>:/icons/document-save.png</normaloff>:/icons/document-save.png</iconset>
</property> </property>
</widget> </widget>
<addaction name="actionStartFile"/>
<addaction name="actionStartDisc"/> <addaction name="actionStartDisc"/>
<addaction name="actionStartBios"/> <addaction name="actionStartBios"/>
<addaction name="actionResumeLastState"/> <addaction name="actionResumeLastState"/>
@ -254,7 +255,7 @@
<attribute name="toolBarBreak"> <attribute name="toolBarBreak">
<bool>false</bool> <bool>false</bool>
</attribute> </attribute>
<addaction name="actionStartDisc"/> <addaction name="actionStartFile"/>
<addaction name="actionStartBios"/> <addaction name="actionStartBios"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionResumeLastState"/> <addaction name="actionResumeLastState"/>
@ -271,6 +272,15 @@
<addaction name="actionSettings"/> <addaction name="actionSettings"/>
</widget> </widget>
<widget class="QStatusBar" name="statusBar"/> <widget class="QStatusBar" name="statusBar"/>
<action name="actionStartFile">
<property name="icon">
<iconset resource="resources/resources.qrc">
<normaloff>:/icons/media-optical.png</normaloff>:/icons/media-optical.png</iconset>
</property>
<property name="text">
<string>Start &amp;File...</string>
</property>
</action>
<action name="actionStartDisc"> <action name="actionStartDisc">
<property name="icon"> <property name="icon">
<iconset resource="resources/resources.qrc"> <iconset resource="resources/resources.qrc">