From 3e656d5465736a0df467a4be4894d8ff16e996d5 Mon Sep 17 00:00:00 2001 From: Mihai Parparita Date: Sun, 23 Aug 2026 16:53:46 -0700 Subject: [PATCH 1/3] imgfile: Keep the current image if opening another fails Create the replacement backend before swapping it in. This lets callers try a new image without losing the one that is already open. --- utils/imgfile_sdl.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/utils/imgfile_sdl.cpp b/utils/imgfile_sdl.cpp index a3904cb220..1073751188 100644 --- a/utils/imgfile_sdl.cpp +++ b/utils/imgfile_sdl.cpp @@ -67,8 +67,12 @@ ImgFile::~ImgFile() = default; bool ImgFile::open(const std::string &img_path) { - impl->backend = make_imgfile_backend(img_path); - return !!impl->backend; + auto backend = make_imgfile_backend(img_path); + if (!backend) + return false; + + impl->backend = std::move(backend); + return true; } void ImgFile::close() From a83edb06ca6ebf341b9bd78815a950a90935bea0 Mon Sep 17 00:00:00 2001 From: Mihai Parparita Date: Sun, 23 Aug 2026 16:53:58 -0700 Subject: [PATCH 2/3] cdrom: Support inserting and ejecting media Put runtime media changes behind a shared machine/device API, and reject insertion while a disc is still present. Guest eject requests now clear the mounted image, while host insertion reports the media change through SCSI and ATAPI unit attention. --- devices/common/ata/atapicdrom.cpp | 3 +- devices/common/scsi/scsi.h | 10 +++++++ devices/common/scsi/scsiblockcmds.cpp | 6 ++-- devices/common/scsi/scsibus.cpp | 3 +- devices/common/scsi/scsicdrom.h | 1 + devices/common/scsi/scsicdromcmds.cpp | 37 +++++++++++++++++++++++++ devices/common/scsi/scsicdromcmds.h | 2 ++ devices/storage/blockstoragedevice.cpp | 19 +++++++++---- devices/storage/cdromdrive.cpp | 38 ++++++++++++++++++++++++-- devices/storage/cdromdrive.h | 10 ++++++- machines/machinebase.cpp | 23 ++++++++++++++++ machines/machinebase.h | 10 +++++++ 12 files changed, 147 insertions(+), 15 deletions(-) diff --git a/devices/common/ata/atapicdrom.cpp b/devices/common/ata/atapicdrom.cpp index b3ca23d8b4..d43750ae50 100644 --- a/devices/common/ata/atapicdrom.cpp +++ b/devices/common/ata/atapicdrom.cpp @@ -71,7 +71,8 @@ int AtapiCdrom::device_postinit() { std::string cdr_image_path = GET_STR_PROP("cdr_img"); if (!cdr_image_path.empty()) { - this->insert_image(cdr_image_path); + if (!this->insert_image(cdr_image_path)) + return -1; } return 0; diff --git a/devices/common/scsi/scsi.h b/devices/common/scsi/scsi.h index c6844529bd..e026871723 100644 --- a/devices/common/scsi/scsi.h +++ b/devices/common/scsi/scsi.h @@ -175,6 +175,14 @@ enum ScsiCommand : uint8_t { READ_CD = 0xBE, }; +/** START STOP UNIT command flags in CDB byte 4. */ +namespace ScsiStartStopUnit { + enum : uint8_t { + START = 1 << 0, + LOAD_EJECT = 1 << 1, + }; +} + enum ScsiSense : uint8_t { NO_SENSE = 0x0, RECOVERED = 0x1, @@ -200,8 +208,10 @@ enum ScsiError : uint8_t { INVALID_CDB = 0x24, INVALID_LUN = 0x25, WRITE_PROTECT = 0x27, + MEDIUM_CHANGED = 0x28, SAVING_NOT_SUPPORTED = 0x39, MEDIUM_NOT_PRESENT = 0x3A, + REMOVAL_PREVENTED = 0x53, }; /** SCSI device types used in INQUIRY. */ diff --git a/devices/common/scsi/scsiblockcmds.cpp b/devices/common/scsi/scsiblockcmds.cpp index adda7c62f4..96d19347a5 100644 --- a/devices/common/scsi/scsiblockcmds.cpp +++ b/devices/common/scsi/scsiblockcmds.cpp @@ -179,11 +179,11 @@ int ScsiBlockCmds::write() { } int ScsiBlockCmds::start_stop_unit() { - if (this->cdb_ptr[4] & 1) { - if (this->cdb_ptr[4] & 2) + if (this->cdb_ptr[4] & ScsiStartStopUnit::START) { + if (this->cdb_ptr[4] & ScsiStartStopUnit::LOAD_EJECT) LOG_F(INFO, "START_STOP_UNIT: medium load requested"); } else { - if (this->cdb_ptr[4] & 2) { + if (this->cdb_ptr[4] & ScsiStartStopUnit::LOAD_EJECT) { LOG_F(INFO, "START_STOP_UNIT: medium eject requested"); } } diff --git a/devices/common/scsi/scsibus.cpp b/devices/common/scsi/scsibus.cpp index 432ddef29b..4e341fa0d9 100644 --- a/devices/common/scsi/scsibus.cpp +++ b/devices/common/scsi/scsibus.cpp @@ -354,7 +354,8 @@ void ScsiBus::attach_scsi_devices(const std::string bus_suffix) gMachineObj->add_device(scsi_device_name, std::unique_ptr(scsi_device)); this->register_device(scsi_id, scsi_device); - scsi_device->insert_image(path); + if (!scsi_device->insert_image(path)) + ABORT_F("Could not insert CD-ROM image, %s", path.c_str()); } else { LOG_F(ERROR, "%s: Too many devices. CD-ROM \"%s\" was not added.", diff --git a/devices/common/scsi/scsicdrom.h b/devices/common/scsi/scsicdrom.h index a46a75c8ec..ef9a693240 100644 --- a/devices/common/scsi/scsicdrom.h +++ b/devices/common/scsi/scsicdrom.h @@ -44,6 +44,7 @@ class ScsiCdrom : public ScsiPhysDevice, public ScsiCdromCmds { protected: bool is_device_ready() override { return this->is_ready; } + uint8_t not_ready_reason() override { return ScsiError::MEDIUM_NOT_PRESENT; } void mode_select_6(uint8_t param_len); diff --git a/devices/common/scsi/scsicdromcmds.cpp b/devices/common/scsi/scsicdromcmds.cpp index 481a298957..ef6097de3a 100644 --- a/devices/common/scsi/scsicdromcmds.cpp +++ b/devices/common/scsi/scsicdromcmds.cpp @@ -62,6 +62,43 @@ void ScsiCdromCmds::process_command() { phy_impl->switch_phase(next_phase); } +int ScsiCdromCmds::test_unit_ready() { + if (this->take_media_change()) { + this->sense_key = ScsiSense::UNIT_ATTENTION; + this->asc = ScsiError::MEDIUM_CHANGED; + this->ascq = 0; + phy_impl->set_status(ScsiStatus::CHECK_CONDITION, this->sense_key); + return ScsiPhase::STATUS; + } + + return ScsiCommonCmds::test_unit_ready(); +} + +int ScsiCdromCmds::start_stop_unit() { + bool load_eject = this->cdb_ptr[4] & ScsiStartStopUnit::LOAD_EJECT; + bool start = this->cdb_ptr[4] & ScsiStartStopUnit::START; + + if (!load_eject) + return ScsiPhase::STATUS; + + if (start) { + LOG_F(INFO, "START_STOP_UNIT: medium load requested"); + return ScsiPhase::STATUS; + } + + if (this->drive_locked) { + LOG_F(INFO, "START_STOP_UNIT: medium eject prevented"); + this->field_ptr_valid = false; + this->bit_ptr_valid = false; + this->illegal_request(ScsiError::REMOVAL_PREVENTED, 2, false); + } else { + LOG_F(INFO, "START_STOP_UNIT: medium eject requested"); + this->eject_image(); + } + + return ScsiPhase::STATUS; +} + int ScsiCdromCmds:: read_toc() { uint8_t start_track, session_num; int tot_tracks, resp_len = 0; diff --git a/devices/common/scsi/scsicdromcmds.h b/devices/common/scsi/scsicdromcmds.h index 85a91db571..28fa36f3d1 100644 --- a/devices/common/scsi/scsicdromcmds.h +++ b/devices/common/scsi/scsicdromcmds.h @@ -35,6 +35,8 @@ class ScsiCdromCmds : public ScsiBlockCmds, public CdromDrive { protected: virtual void process_command() override; + int test_unit_ready() override; + int start_stop_unit() override; int get_apple_page_49(uint8_t subpage, uint8_t ctrl, uint8_t *out_ptr, int avail_len); diff --git a/devices/storage/blockstoragedevice.cpp b/devices/storage/blockstoragedevice.cpp index f37ef47abf..c427ac76f7 100644 --- a/devices/storage/blockstoragedevice.cpp +++ b/devices/storage/blockstoragedevice.cpp @@ -39,16 +39,18 @@ BlockStorageDevice::BlockStorageDevice(const uint32_t cache_blocks, } BlockStorageDevice::~BlockStorageDevice() { - this->img_file.close(); + if (this->is_ready) + this->img_file.close(); } int BlockStorageDevice::set_host_file(std::string file_path) { - this->is_ready = false; - if (!this->img_file.open(file_path)) return -1; + this->is_ready = false; this->size_bytes = this->img_file.size(); + this->remain_size = 0; + this->write_size = 0; this->set_fpos(0); @@ -66,10 +68,15 @@ int BlockStorageDevice::set_block_size(const int blk_size) { return -1; } - this->cache_size = this->cache_blocks * this->raw_blk_size; + uint32_t new_cache_size = this->cache_blocks * this->raw_blk_size; + // Preserve pointers held by an in-progress transfer when the cache geometry + // does not change during a media replacement. + if (new_cache_size != this->cache_size) { + this->cache_size = new_cache_size; - // allocate data cache and fill it with zeroes - this->data_cache = std::unique_ptr(new char[this->cache_size] ()); + // allocate data cache and fill it with zeroes + this->data_cache = std::unique_ptr(new char[this->cache_size] ()); + } return 0; } diff --git a/devices/storage/cdromdrive.cpp b/devices/storage/cdromdrive.cpp index 18379e1114..9b3943bcd1 100644 --- a/devices/storage/cdromdrive.cpp +++ b/devices/storage/cdromdrive.cpp @@ -32,10 +32,18 @@ CdromDrive::CdromDrive() : BlockStorageDevice(31, CDR_STD_DATA_SIZE, 0xfffffffe) this->is_writeable = false; } -void CdromDrive::insert_image(std::string filename) { - if (this->set_host_file(filename) < 0) - ABORT_F("Could not open CD-ROM image file, %s", filename.c_str()); +bool CdromDrive::insert_image(const std::string& filename, bool notify_guest) { + if (this->medium_present()) { + LOG_F(ERROR, "Cannot insert CD-ROM image while media is present"); + return false; + } + + if (this->set_host_file(filename) < 0) { + LOG_F(ERROR, "Could not open CD-ROM image file, %s", filename.c_str()); + return false; + } + this->data_offset = 0; this->detect_raw_image(); // create single track descriptor @@ -45,6 +53,30 @@ void CdromDrive::insert_image(std::string filename) { // create Lead-out descriptor containing all data this->tracks[1] = {LEAD_OUT_TRK_NUM, /*.trk_num*/ 0x14, /*.adr_ctrl*/ static_cast(this->size_blocks + 1) /*.start_lba*/}; + + this->media_changed |= notify_guest; + return true; +} + +bool CdromDrive::eject_image() { + if (!this->is_ready) + return false; + + this->is_ready = false; + this->img_file.close(); + + this->size_bytes = 0; + this->size_blocks = 0; + this->cur_fpos = 0; + this->write_size = 0; + this->remain_size = 0; + this->raw_blk_size = this->block_size; + this->data_offset = 0; + this->num_tracks = 0; + this->media_changed = false; + std::memset(this->tracks, 0, sizeof(this->tracks)); + + return true; } bool CdromDrive::detect_raw_image() { diff --git a/devices/storage/cdromdrive.h b/devices/storage/cdromdrive.h index 4fbfb32783..5b8ac9ef28 100644 --- a/devices/storage/cdromdrive.h +++ b/devices/storage/cdromdrive.h @@ -81,7 +81,14 @@ class CdromDrive : public BlockStorageDevice { bool medium_present() { return this->is_ready; } - void insert_image(std::string filename); + bool insert_image(const std::string& filename, bool notify_guest = false); + bool eject_image(); + + bool take_media_change() { + bool changed = this->media_changed; + this->media_changed = false; + return changed; + } protected: uint8_t hex_to_bcd(const uint8_t val); @@ -101,6 +108,7 @@ class CdromDrive : public BlockStorageDevice { uint8_t sw_lock_sup = 1; // drive supports locking/unlocking via SW uint8_t sw_eject_sup = 1; // drive supports ejecting via SW uint8_t drive_locked = 0; // 1 - drive is currently locked + bool media_changed = false; uint8_t prevent_jump = 0; // prevent jumper not present uint8_t more_support = 0; uint16_t max_rd_speed = 706; // defaults to 4x diff --git a/machines/machinebase.cpp b/machines/machinebase.cpp index e6d07f29e3..c74f6ab4ea 100644 --- a/machines/machinebase.cpp +++ b/machines/machinebase.cpp @@ -20,6 +20,7 @@ along with this program. If not, see . */ #include +#include #include #include @@ -98,6 +99,28 @@ HWComponent* MachineBase::get_comp_by_type(HWCompType type) { } } +CdromInsertionResult MachineBase::insert_cdrom_image(const std::string& path, + uint32_t drive) { + uint32_t drive_index = 0; + + for (auto& device : this->device_map) { + auto* cdrom = dynamic_cast(device.second.get()); + if (!cdrom) + continue; + + if (drive_index++ == drive) { + if (cdrom->medium_present()) + return CdromInsertionResult::MEDIA_PRESENT; + + return cdrom->insert_image(path, true) + ? CdromInsertionResult::SUCCESS + : CdromInsertionResult::IMAGE_OPEN_FAILED; + } + } + + return CdromInsertionResult::NO_DRIVE; +} + int MachineBase::postinit_devices() { // Allow additional devices to be registered by device_postinit, in which diff --git a/machines/machinebase.h b/machines/machinebase.h index a7c177e466..60d1536831 100644 --- a/machines/machinebase.h +++ b/machines/machinebase.h @@ -27,6 +27,7 @@ along with this program. If not, see . #ifndef MACHINE_BASE_H #define MACHINE_BASE_H +#include #include #include #include @@ -34,6 +35,13 @@ along with this program. If not, see . class HWComponent; enum HWCompType : uint64_t; +enum class CdromInsertionResult { + SUCCESS, + NO_DRIVE, + MEDIA_PRESENT, + IMAGE_OPEN_FAILED, +}; + class MachineBase { public: MachineBase(std::string name); @@ -45,6 +53,8 @@ class MachineBase { HWComponent* get_comp_by_name(std::string name); HWComponent* get_comp_by_name_optional(std::string name); HWComponent* get_comp_by_type(HWCompType type); + CdromInsertionResult insert_cdrom_image(const std::string& path, + uint32_t drive = 0); int postinit_devices(); private: From 321d82209de1ddd4035337b3267f192a974ce8a2 Mon Sep 17 00:00:00 2001 From: Mihai Parparita Date: Sun, 23 Aug 2026 16:54:07 -0700 Subject: [PATCH 3/3] sdl: Insert dropped CD-ROM images Pass SDL file-drop events through the shared media API and report when the drive is unavailable or still occupied. Advertise ISO and Toast images in the macOS bundle so they can also be dropped on the app icon. --- CMakeLists.txt | 7 ++++ cmake/DingusPPCInfo.plist.in | 70 ++++++++++++++++++++++++++++++++++++ core/hostevents_sdl.cpp | 28 +++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 cmake/DingusPPCInfo.plist.in diff --git a/CMakeLists.txt b/CMakeLists.txt index c19576986a..dd092d6631 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -190,6 +190,13 @@ if (APPLE) # MACOSX_BUNDLE_ICON_FILE "icon.icns" ) + if (NOT IOS) + set_target_properties(dingusppc PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${PROJECT_SOURCE_DIR}/cmake/DingusPPCInfo.plist.in" + XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUNDLE_ID}" + ) + endif() + find_library(COCOA_LIBRARY Cocoa) find_library(IOKIT_LIBRARY IOKit) find_library(COREAUDIO_LIBRARY CoreAudio) diff --git a/cmake/DingusPPCInfo.plist.in b/cmake/DingusPPCInfo.plist.in new file mode 100644 index 0000000000..c3001e9d43 --- /dev/null +++ b/cmake/DingusPPCInfo.plist.in @@ -0,0 +1,70 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleDocumentTypes + + + CFBundleTypeName + CD-ROM image + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + public.iso-image + com.roxio.disk-image-toast + + + + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature + ???? + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + UTImportedTypeDeclarations + + + UTTypeConformsTo + + public.disk-image + + UTTypeDescription + Toast disk image + UTTypeIdentifier + com.roxio.disk-image-toast + UTTypeTagSpecification + + public.filename-extension + + toast + + + + + + diff --git a/core/hostevents_sdl.cpp b/core/hostevents_sdl.cpp index 91de0aac55..b0859e5382 100644 --- a/core/hostevents_sdl.cpp +++ b/core/hostevents_sdl.cpp @@ -23,6 +23,7 @@ along with this program. If not, see . #include #include #include +#include #include #include @@ -322,6 +323,33 @@ void EventManager::poll_events() { } break; + case SDL_DROPFILE: { + const char* path = event.drop.file; + CdromInsertionResult result = gMachineObj + ? gMachineObj->insert_cdrom_image(path) + : CdromInsertionResult::NO_DRIVE; + + switch (result) { + case CdromInsertionResult::SUCCESS: + LOG_F(INFO, "Inserted CD-ROM image: %s", path); + break; + case CdromInsertionResult::NO_DRIVE: + LOG_F(ERROR, "Cannot insert CD-ROM image; no CD-ROM drive is available: %s", + path); + break; + case CdromInsertionResult::MEDIA_PRESENT: + LOG_F(ERROR, "Cannot insert CD-ROM image; eject the current media first: %s", + path); + break; + case CdromInsertionResult::IMAGE_OPEN_FAILED: + LOG_F(ERROR, "Cannot insert CD-ROM image: %s", path); + break; + } + + SDL_free(event.drop.file); + } + break; + default: unhandled_events++; }