From c22d2b51bbaed3d4d504fb8bf5602c6758c2827f Mon Sep 17 00:00:00 2001 From: kd-11 Date: Thu, 8 Jan 2026 02:32:47 +0300 Subject: [PATCH 01/47] gl: Do not overwrite texture-cache image dimensions when updating format - The code was an outdated relic from the early days. The calculated pitch was wrong as it did not take subregions (incomplete rows) into account. --- rpcs3/Emu/RSX/GL/GLTextureCache.h | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rpcs3/Emu/RSX/GL/GLTextureCache.h b/rpcs3/Emu/RSX/GL/GLTextureCache.h index 486087d56a..8dc2c27664 100644 --- a/rpcs3/Emu/RSX/GL/GLTextureCache.h +++ b/rpcs3/Emu/RSX/GL/GLTextureCache.h @@ -669,8 +669,8 @@ namespace gl } else { - //TODO: More tests on byte order - //ARGB8+native+unswizzled is confirmed with Dark Souls II character preview + // TODO: More tests on byte order + // ARGB8+native+unswizzled is confirmed with Dark Souls II character preview switch (gcm_format) { case CELL_GCM_TEXTURE_A8R8G8B8: @@ -697,8 +697,7 @@ namespace gl fmt::throw_exception("Unexpected gcm format 0x%X", gcm_format); } - //NOTE: Protection is handled by the caller - cached.set_dimensions(width, height, depth, (rsx_range.length() / height)); + // NOTE: Protection is handled by the caller no_access_range = cached.get_min_max(no_access_range, rsx::section_bounds::locked_range); } From ef5a4bf7e4cd6dddc3dbf10782eba5925b8bd981 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Wed, 7 Jan 2026 21:46:05 +0100 Subject: [PATCH 02/47] Qt/input: fix pressure intensity button availability --- rpcs3/Emu/Io/PadHandler.cpp | 2 +- rpcs3/Emu/Io/PadHandler.h | 2 +- rpcs3/Input/sdl_pad_handler.cpp | 2 +- rpcs3/rpcs3qt/pad_settings_dialog.cpp | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rpcs3/Emu/Io/PadHandler.cpp b/rpcs3/Emu/Io/PadHandler.cpp index d25ba1379c..9110687a9c 100644 --- a/rpcs3/Emu/Io/PadHandler.cpp +++ b/rpcs3/Emu/Io/PadHandler.cpp @@ -219,7 +219,7 @@ pad_capabilities PadHandlerBase::get_capabilities(const std::string& /*pad_id*/) .has_rumble = b_has_rumble, .has_accel = b_has_motion, .has_gyro = b_has_motion, - .has_pressure_sensitivity = b_has_pressure_intensity_button + .has_pressure_intensity_button = b_has_pressure_intensity_button }; } diff --git a/rpcs3/Emu/Io/PadHandler.h b/rpcs3/Emu/Io/PadHandler.h index a27e0bcc50..98cca72a7d 100644 --- a/rpcs3/Emu/Io/PadHandler.h +++ b/rpcs3/Emu/Io/PadHandler.h @@ -90,7 +90,7 @@ struct pad_capabilities bool has_rumble = false; bool has_accel = false; bool has_gyro = false; - bool has_pressure_sensitivity = false; + bool has_pressure_intensity_button = true; }; using pad_preview_values = std::array; diff --git a/rpcs3/Input/sdl_pad_handler.cpp b/rpcs3/Input/sdl_pad_handler.cpp index 1b6ddbc40c..c47514f6ae 100644 --- a/rpcs3/Input/sdl_pad_handler.cpp +++ b/rpcs3/Input/sdl_pad_handler.cpp @@ -738,7 +738,7 @@ pad_capabilities sdl_pad_handler::get_capabilities(const std::string& pad_id) capabilities.has_rumble &= dev->sdl.has_rumble; capabilities.has_accel &= dev->sdl.has_accel; capabilities.has_gyro &= dev->sdl.has_gyro; - capabilities.has_pressure_sensitivity &= dev->sdl.is_ds3_with_pressure_buttons; + capabilities.has_pressure_intensity_button &= !dev->sdl.is_ds3_with_pressure_buttons; // Only allow if there's not pressure sensitivity return capabilities; } diff --git a/rpcs3/rpcs3qt/pad_settings_dialog.cpp b/rpcs3/rpcs3qt/pad_settings_dialog.cpp index 36ece3b0ec..2940143a27 100644 --- a/rpcs3/rpcs3qt/pad_settings_dialog.cpp +++ b/rpcs3/rpcs3qt/pad_settings_dialog.cpp @@ -482,7 +482,7 @@ void pad_settings_dialog::InitButtons() if ((!is_connected || !m_remap_timer.isActive()) && ( is_connected != m_enable_buttons || (is_connected && ( - !capabilities.has_pressure_sensitivity != m_enable_pressure_intensity_button || + capabilities.has_pressure_intensity_button != m_enable_pressure_intensity_button || capabilities.has_rumble != m_enable_rumble || capabilities.has_battery_led != m_enable_battery_led || (capabilities.has_led || capabilities.has_mono_led) != m_enable_led || @@ -490,7 +490,7 @@ void pad_settings_dialog::InitButtons() { if (is_connected) { - m_enable_pressure_intensity_button = !capabilities.has_pressure_sensitivity; + m_enable_pressure_intensity_button = capabilities.has_pressure_intensity_button; m_enable_rumble = capabilities.has_rumble; m_enable_battery_led = capabilities.has_battery_led; m_enable_led = capabilities.has_led || capabilities.has_mono_led; From 6c083d6184c77b8643afa9b6c49aac97bc00f1a4 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Sun, 7 Dec 2025 05:36:22 +0000 Subject: [PATCH 03/47] ISO: Add ISO reader code and ISO device Adds infrastructure to read files/data from ISOs. Also adds classes extending fs::device_base and fs::file_base in order to allow reading files through fs namespace functions. This approach should allow ISO data to be read by the emulator with the least changes to existing code. --- Utilities/File.h | 2 +- rpcs3/Emu/CMakeLists.txt | 1 + rpcs3/Loader/ISO.cpp | 600 +++++++++++++++++++++++++++++++++++++++ rpcs3/Loader/ISO.h | 112 ++++++++ rpcs3/emucore.vcxproj | 4 +- 5 files changed, 717 insertions(+), 2 deletions(-) create mode 100644 rpcs3/Loader/ISO.cpp create mode 100644 rpcs3/Loader/ISO.h diff --git a/Utilities/File.h b/Utilities/File.h index 90453f16f0..f7765507e8 100644 --- a/Utilities/File.h +++ b/Utilities/File.h @@ -155,7 +155,7 @@ namespace fs // Virtual device struct device_base { - const std::string fs_prefix; + std::string fs_prefix; device_base(); virtual ~device_base(); diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index 8754df84c1..a96ddfa2b3 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -125,6 +125,7 @@ target_sources(rpcs3_emu PRIVATE ../Loader/PSF.cpp ../Loader/PUP.cpp ../Loader/TAR.cpp + ../Loader/ISO.cpp ../Loader/TROPUSR.cpp ../Loader/TRP.cpp ) diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp new file mode 100644 index 0000000000..db11f14c84 --- /dev/null +++ b/rpcs3/Loader/ISO.cpp @@ -0,0 +1,600 @@ +#include "stdafx.h" + +#include "ISO.h" + +#include +#include +#include +#include + +// TODO: replace with file check for iso! +bool is_file_iso(const std::string& path) +{ + if (fs::is_dir(path)) return false; + + return is_file_iso(fs::file(path)); +} + +bool is_file_iso(const fs::file& file) +{ + if (!file) return false; + if (file.size() < 32768 + 6) return false; + + file.seek(32768); + + char magic[5]; + file.read_at(32768 + 1, magic, 5); + + return magic[0] == 'C' && magic[1] == 'D' + && magic[2] == '0' && magic[3] == '0' + && magic[4] == '1'; +} + +const int ISO_BLOCK_SIZE = 2048; + +template +inline T read_both_endian_int(fs::file& file) +{ + T out; + + if (std::endian::little == std::endian::native) + { + out = file.read(); + file.seek(sizeof(T), fs::seek_cur); + } + else + { + file.seek(sizeof(T), fs::seek_cur); + out = file.read(); + } + + return out; +} + +// assumed that directory_entry is at file head +std::optional iso_read_directory_entry(fs::file& file, bool names_in_ucs2 = false) +{ + auto start_pos = file.pos(); + u8 entry_length = file.read(); + + if (entry_length == 0) return std::nullopt; + + file.seek(1, fs::seek_cur); + u32 start_sector = read_both_endian_int(file); + u32 file_size = read_both_endian_int(file); + + std::tm file_date = {}; + file_date.tm_year = file.read(); + file_date.tm_mon = file.read() - 1; + file_date.tm_mday = file.read(); + file_date.tm_hour = file.read(); + file_date.tm_min = file.read(); + file_date.tm_sec = file.read(); + s16 timezone_value = file.read(); + s16 timezone_offset = (timezone_value - 50) * 15 * 60; + + std::time_t date_time = std::mktime(&file_date) + timezone_offset; + + u8 flags = file.read(); + + // 2nd flag bit indicates whether a given fs node is a directory + bool is_directory = flags & 0b00000010; + bool has_more_extents = flags & 0b10000000; + + file.seek(6, fs::seek_cur); + + u8 file_name_length = file.read(); + + std::string file_name; + file.read(file_name, file_name_length); + + if (file_name_length == 1 && file_name[0] == 0) + { + file_name = "."; + } + else if (file_name == "\1") + { + file_name = ".."; + } + else if (names_in_ucs2) // for strings in joliet descriptor + { + std::string new_file_name = ""; + int read = 0; + const u8* raw_str = reinterpret_cast(file_name.c_str()); + while(read < file_name_length) + { + // characters are stored in big endian format. + const u16 upper = raw_str[read]; + const u8 lower = raw_str[read + 1]; + + const u16 code_point = (upper << 8) + lower; + + std::wstring_convert, char16_t> convert; + new_file_name += convert.to_bytes(code_point); + + read += 2; + } + + file_name = new_file_name; + } + + if (file_name.ends_with(";1")) + { + file_name.erase(file_name.end() - 2, file_name.end()); + } + + if (file_name_length > 1 && file_name.ends_with(".")) + { + file_name.pop_back(); + } + + // skip the rest of the entry. + file.seek(entry_length + start_pos); + + return iso_fs_metadata + { + .name = file_name, + .time = date_time, + .is_directory = is_directory, + .has_multiple_extents = has_more_extents, + .extents = + { + iso_extent_info + { + .start = start_sector, + .size = file_size + } + } + }; +} + +void iso_form_hierarchy(fs::file& file, iso_fs_node& node, + bool use_ucs2_decoding = false, std::string parent_path = "") +{ + if (!node.metadata.is_directory) return; + + std::vector multi_extent_node_indices; + + // assuming the directory spans a single extent + const auto& directory_extent = node.metadata.extents[0]; + + file.seek(directory_extent.start * ISO_BLOCK_SIZE); + + u64 end_pos = directory_extent.size + (directory_extent.start * ISO_BLOCK_SIZE); + + while(file.pos() < end_pos) + { + auto entry = iso_read_directory_entry(file, use_ucs2_decoding); + if (!entry) + { + float block_size = ISO_BLOCK_SIZE; + float t = std::floor(file.pos() / block_size); + u64 new_sector = t+1; + file.seek(new_sector * ISO_BLOCK_SIZE); + continue; + } + + bool extent_added = false; + + // find previous extent and merge into it, otherwise we push this node's index + for (int index : multi_extent_node_indices) + { + auto& selected_node = node.children.at(index); + if (selected_node->metadata.name.compare(entry->name) == 0) + { + // merge into selected_node + selected_node->metadata.extents.push_back(entry->extents[0]); + + extent_added = true; + } + } + + if (extent_added) continue; + + if (entry->has_multiple_extents) + { + // haven't pushed entry to node.children yet so node.children::size() == entry_index + multi_extent_node_indices.push_back(node.children.size()); + } + + node.children.push_back(std::make_unique(iso_fs_node{ + .metadata = *entry + })); + } + + for (auto& child_node : node.children) + { + if (child_node->metadata.name != "." && child_node->metadata.name != "..") + { + iso_form_hierarchy(file, *child_node, use_ucs2_decoding, parent_path + "/" + node.metadata.name); + } + } +} + +u64 iso_fs_metadata::size() const +{ + u64 total_size = 0; + for (const auto& extent : extents) + { + total_size += extent.size; + } + + return total_size; +} + +iso_archive::iso_archive(const std::string& path) +{ + m_path = path; + m_file = fs::file(path); + + if (!is_file_iso(m_file)) + { + // not iso... TODO: throw something?? + return; + } + + u8 descriptor_type = -2; + bool use_ucs2_decoding = false; + + do + { + auto descriptor_start = m_file.pos(); + + descriptor_type = m_file.read(); + + // 1 = primary vol descriptor, 2 = joliet SVD + if (descriptor_type == 1 || descriptor_type == 2) + { + use_ucs2_decoding = descriptor_type == 2; + + // skip the rest of descriptor's data + m_file.seek(155, fs::seek_cur); + + m_root = iso_fs_node + { + .metadata = iso_read_directory_entry(m_file, use_ucs2_decoding).value(), + }; + + m_file.seek(descriptor_start); + } + + m_file.seek(descriptor_start + ISO_BLOCK_SIZE); + } + while(descriptor_type != 255); + + iso_form_hierarchy(m_file, m_root, use_ucs2_decoding); +} + +iso_fs_node* iso_archive::retrieve(const std::string& passed_path) +{ + std::string path = std::filesystem::path(passed_path).lexically_normal().string(); + + size_t start = 0; + size_t end = path.find_first_of(fs::delim); + + auto dir_entry = &m_root; + + do + { + if (end == std::string::npos) + { + end = path.size(); + } + + auto path_component = path.substr(start, end-start); + + bool found = false; + for (const auto& entry : dir_entry->children) + { + if (entry->metadata.name.compare(path_component) == 0) + { + dir_entry = entry.get(); + + start = end + 1; + end = path.find_first_of(fs::delim, start); + found = true; + break; + } + } + + if (!found) + { + return nullptr; + } + } + while(start < path.size()); + + return dir_entry; +} + +bool iso_archive::exists(const std::string& path) +{ + return retrieve(path) != nullptr; +} + +bool iso_archive::is_file(const std::string& path) +{ + auto file_node = retrieve(path); + if (!file_node) return false; + + return !file_node->metadata.is_directory; +} + +iso_file iso_archive::open(const std::string& path) +{ + return iso_file(fs::file(m_path), *retrieve(path)); +} + +iso_file::iso_file(fs::file&& iso_handle, const iso_fs_node& node) + : m_file(std::move(iso_handle)), m_meta(node.metadata), m_pos(0) +{ + m_file.seek(ISO_BLOCK_SIZE * node.metadata.extents[0].start); +} + +fs::stat_t iso_file::get_stat() +{ + return fs::stat_t + { + .is_directory = false, + .is_symlink = false, + .is_writable = false, + .size = size(), + .atime = m_meta.time, + .mtime = m_meta.time, + .ctime = m_meta.time + }; +} + +bool iso_file::trunc(u64) +{ + fs::g_tls_error = fs::error::readonly; + return false; +} + +std::pair iso_file::get_extent_pos(u64 pos) const +{ + auto it = m_meta.extents.begin(); + + while(pos >= it->size && it < m_meta.extents.end() - 1) + { + pos -= it->size; + + it++; + } + + return {pos, *it}; +} + +// assumed valid and in bounds. +u64 iso_file::file_offset(u64 pos) const +{ + auto [local_pos, extent] = get_extent_pos(pos); + + return (extent.start * ISO_BLOCK_SIZE) + local_pos; +} + +u64 iso_file::local_extent_remaining(u64 pos) const +{ + auto [local_pos, extent] = get_extent_pos(pos); + + return extent.size - local_pos; +} + +u64 iso_file::local_extent_size(u64 pos) const +{ + return get_extent_pos(pos).second.size; +} + +u64 iso_file::read(void* buffer, u64 size) +{ + auto r = read_at(m_pos, buffer, size); + m_pos += r; + return r; +} + +u64 iso_file::read_at(u64 offset, void* buffer, u64 size) +{ + const u64 bad_res = -1; + u64 local_remaining = local_extent_remaining(offset); + + u64 total_read = m_file.read_at(file_offset(offset), buffer, std::min(size, local_remaining)); + if (total_read == bad_res) return -1; + + auto total_size = this->size(); + + if (size > total_read && (offset + total_read) < total_size) + { + u64 second_total_read = read_at(offset + total_read, + static_cast(buffer) + total_read, + size - total_read + ); + + if (second_total_read == bad_res) return -1; + + return total_read + second_total_read; + } + + return total_read; +} + +u64 iso_file::write(const void*, u64) +{ + fs::g_tls_error = fs::error::readonly; + return 0; +} + +u64 iso_file::seek(s64 offset, fs::seek_mode whence) +{ + const s64 total_size = size(); + const s64 new_pos = + whence == fs::seek_set ? offset : + whence == fs::seek_cur ? offset + m_pos : + whence == fs::seek_end ? offset + total_size : -1; + + if (new_pos < 0) + { + fs::g_tls_error = fs::error::inval; + return -1; + } + + const u64 bad_res = -1; + + u64 result = m_file.seek(file_offset(m_pos)); + if (result == bad_res) return -1; + + m_pos = new_pos; + return m_pos; +} + +u64 iso_file::size() +{ + u64 extent_sizes = 0; + for (const auto& extent : m_meta.extents) + { + extent_sizes += extent.size; + } + + return extent_sizes; +} + +void iso_file::release() +{ + m_file.release(); +} + +bool iso_dir::read(fs::dir_entry& entry) +{ + if (m_pos < m_node.children.size()) + { + auto& selected = m_node.children[m_pos].get()->metadata; + u64 size = selected.size(); + + entry.name = selected.name; + entry.atime = selected.time; + entry.mtime = selected.time; + entry.ctime = selected.time; + entry.is_directory = selected.is_directory; + entry.is_symlink = false; + entry.is_writable = false; + entry.size = size; + + m_pos++; + + return true; + } + + return false; +} + +bool iso_device::stat(const std::string& path, fs::stat_t& info) +{ + auto relative_path = std::filesystem::relative(std::filesystem::path(path), + std::filesystem::path(fs_prefix)).string(); + + auto node = m_archive.retrieve(relative_path); + if (!node) + { + fs::g_tls_error = fs::error::noent; + return false; + } + + auto& meta = node->metadata; + u64 size = meta.size(); + + info = fs::stat_t + { + .is_directory = meta.is_directory, + .is_symlink = false, + .is_writable = false, + .size = size, + .atime = meta.time, + .mtime = meta.time, + .ctime = meta.time + }; + + return true; +} + +bool iso_device::statfs(const std::string& path, fs::device_stat& info) +{ + auto relative_path = std::filesystem::relative(std::filesystem::path(path), + std::filesystem::path(fs_prefix)).string(); + + auto node = m_archive.retrieve(relative_path); + if (!node) + { + fs::g_tls_error = fs::error::noent; + return false; + } + + auto& meta = node->metadata; + u64 size = meta.size(); + + info = fs::device_stat + { + .block_size=size, + .total_size=size, + .total_free=0, + .avail_free=0 + }; + + return false; +} + +std::unique_ptr iso_device::open(const std::string& path, bs_t mode) +{ + auto relative_path = std::filesystem::relative(std::filesystem::path(path), + std::filesystem::path(fs_prefix)).string(); + + auto node = m_archive.retrieve(relative_path); + if (!node) + { + fs::g_tls_error = fs::error::noent; + return nullptr; + } + + if (node->metadata.is_directory) + { + fs::g_tls_error = fs::error::isdir; + return nullptr; + } + + return std::make_unique(fs::file(iso_path), *node); +} + +std::unique_ptr iso_device::open_dir(const std::string& path) +{ + auto relative_path = std::filesystem::relative(std::filesystem::path(path), + std::filesystem::path(fs_prefix)).string(); + + auto node = m_archive.retrieve(relative_path); + if (!node) + { + fs::g_tls_error = fs::error::noent; + return nullptr; + } + + if (!node->metadata.is_directory) + { + // fs::dir::open -> ::readdir should return ENOTDIR when path is + // pointing to a file instead of a folder, which translates to error::unknown. + // doing the same here. + fs::g_tls_error = fs::error::unknown; + } + + return std::make_unique(*node); +} + +void iso_dir::rewind() +{ + m_pos = 0; +} + +void load_iso(const std::string& path) +{ + fs::set_virtual_device("iso_overlay_fs_dev", + stx::shared_ptr()); + + fs::set_virtual_device("iso_overlay_fs_dev", + stx::make_shared(path)); +} diff --git a/rpcs3/Loader/ISO.h b/rpcs3/Loader/ISO.h new file mode 100644 index 0000000000..bc3d1faa2b --- /dev/null +++ b/rpcs3/Loader/ISO.h @@ -0,0 +1,112 @@ +#pragma once + +#include "Utilities/File.h" +#include "util/types.hpp" + +bool is_file_iso(const std::string& path); +bool is_file_iso(const fs::file& path); + +void load_iso(const std::string& path); + +struct iso_extent_info +{ + u64 start; + u64 size; +}; + +struct iso_fs_metadata +{ + std::string name; + s64 time; + bool is_directory; + bool has_multiple_extents; + std::vector extents; + + u64 size() const; +}; + +struct iso_fs_node +{ + iso_fs_metadata metadata; + std::vector> children; +}; + +class iso_file : public fs::file_base +{ + fs::file m_file; + iso_fs_metadata m_meta; + u64 m_pos; + + std::pair get_extent_pos(u64 pos) const; + u64 file_offset(u64 pos) const; + u64 local_extent_remaining(u64 pos) const; + u64 local_extent_size(u64 pos) const; + + public: + iso_file(fs::file&& iso_handle, const iso_fs_node& node); + + fs::stat_t get_stat() override; + bool trunc(u64 length) override; + u64 read(void* buffer, u64 size) override; + u64 read_at(u64 offset, void* buffer, u64 size) override; + u64 write(const void* buffer, u64 size) override; + u64 seek(s64 offset, fs::seek_mode whence) override; + u64 size() override; + + void release() override; +}; + +class iso_dir : public fs::dir_base +{ + const iso_fs_node& m_node; + u64 m_pos; + + public: + iso_dir(const iso_fs_node& node) + : m_node(node), m_pos(0) + {} + + bool read(fs::dir_entry&) override; + void rewind() override; +}; + +// represents the .iso file itself. +class iso_archive +{ + std::string m_path; + iso_fs_node m_root; + fs::file m_file; + + public: + iso_archive(const std::string& path); + + iso_fs_node* retrieve(const std::string& path); + bool exists(const std::string& path); + bool is_file(const std::string& path); + + iso_file open(const std::string& path); +}; + +class iso_device : public fs::device_base +{ + iso_archive m_archive; + std::string iso_path; + + public: + inline static std::string virtual_device_name = "/vfsv0_virtual_iso_overlay_fs_dev"; + + iso_device(const std::string& iso_path, const std::string& device_name = virtual_device_name) + : m_archive(iso_path), iso_path(iso_path) + { + fs_prefix = device_name; + } + ~iso_device() override = default; + + const std::string& get_loaded_iso() const { return iso_path; } + + bool stat(const std::string& path, fs::stat_t& info) override; + bool statfs(const std::string& path, fs::device_stat& info) override; + + std::unique_ptr open(const std::string& path, bs_t mode) override; + std::unique_ptr open_dir(const std::string& path) override; +}; diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index 357dfe238d..7f3be85a13 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -540,6 +540,7 @@ + @@ -1021,6 +1022,7 @@ + @@ -1096,4 +1098,4 @@ - \ No newline at end of file + From 990e0027829b9a5a4742fa363bb3a10cac4576b6 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:33:05 +0000 Subject: [PATCH 04/47] ISO: Modify game boot for ISOs This commit modifies code in System.cpp to allow games to boot from ISO images. Game data is loaded by leveraging fs::set_virtual_device, and setting the m_path to the ISO device location. This commit also modifies the resolve_path callback to return the path it was given if it wasn't found in the OS. This is necessary as Emulator::GetFakeCat will return "HG" on disc games, making games unplayable through ISO. --- rpcs3/Emu/System.cpp | 39 +++++++++++++++++++++++++++++++++++++- rpcs3/main_application.cpp | 3 ++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 651c220b60..006c0264c3 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -34,6 +34,7 @@ #include "Loader/PSF.h" #include "Loader/TAR.h" +#include "Loader/ISO.h" #include "Loader/ELF.h" #include "Loader/disc.h" @@ -1078,6 +1079,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, sys_log.notice("Path: %s", m_path); std::string inherited_ps3_game_path; + bool launching_from_disc_archive = false; { Init(); @@ -1421,6 +1423,28 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } const std::string resolved_path = GetCallbacks().resolve_path(m_path); + if (is_file_iso(m_path)) + { + load_iso(m_path); + + std::string path = iso_device::virtual_device_name + "/"; + + vfs::mount("/dev_bdvd/"sv, path); + + // ISOs that are install discs will error if set to EBOOT.BIN + // so this should cover both of them + if (fs::exists(path + "PS3_GAME/USRDIR/EBOOT.BIN")) + { + path = path + "PS3_GAME/USRDIR/EBOOT.BIN"; + } + + m_path = path; + + m_dir = "/dev_bdvd/PS3_GAME/"; + m_cat = "DG"sv; + + launching_from_disc_archive = true; + } const std::string elf_dir = fs::get_parent_dir(m_path); @@ -1595,8 +1619,14 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } } + // ISO PKG INSTALL HACK! + if (!m_path.ends_with("EBOOT.BIN") && launching_from_disc_archive) + { + bdvd_dir = m_path; + } + // Special boot mode (directory scan) - if (fs::is_dir(m_path)) + if (fs::is_dir(m_path) && !launching_from_disc_archive) { m_state = system_state::ready; GetCallbacks().on_ready(); @@ -2076,6 +2106,13 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } } + // ISO has no USRDIR/EBOOT.BIN, and we've examined its PKGDIR and extras. + // time to wrap up + if (!m_path.ends_with("EBOOT.BIN") && launching_from_disc_archive) + { + return game_boot_result::nothing_to_boot; + } + // Check firmware version if (const std::string_view game_fw_version = psf::get_string(_psf, "PS3_SYSTEM_VER", ""); !game_fw_version.empty()) { diff --git a/rpcs3/main_application.cpp b/rpcs3/main_application.cpp index ff3b70f952..aa4d035f27 100644 --- a/rpcs3/main_application.cpp +++ b/rpcs3/main_application.cpp @@ -345,7 +345,8 @@ EmuCallbacks main_application::CreateCallbacks() callbacks.resolve_path = [](std::string_view sv) { // May result in an empty string if path does not exist - return QFileInfo(QString::fromUtf8(sv.data(), static_cast(sv.size()))).canonicalFilePath().toStdString(); + std::string result = QFileInfo(QString::fromUtf8(sv.data(), static_cast(sv.size()))).canonicalFilePath().toStdString(); + return !result.empty() ? result : std::string(sv); }; callbacks.get_font_dirs = []() From d6d7ae35f87dd1b0838851fd3310e609dfb2a8c7 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:41:36 +0000 Subject: [PATCH 05/47] ISO: Add games to games list --- rpcs3/Emu/games_config.cpp | 11 ++++++ rpcs3/rpcs3qt/game_list_frame.cpp | 66 ++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/rpcs3/Emu/games_config.cpp b/rpcs3/Emu/games_config.cpp index cfa43f6e15..ad6daff193 100644 --- a/rpcs3/Emu/games_config.cpp +++ b/rpcs3/Emu/games_config.cpp @@ -4,6 +4,8 @@ #include "util/yaml.hpp" #include "Utilities/File.h" +#include "Loader/ISO.h" + LOG_CHANNEL(cfg_log, "CFG"); games_config::games_config() @@ -44,6 +46,15 @@ std::string games_config::get_path(const std::string& title_id) const games_config::result games_config::add_game(const std::string& key, const std::string& path) { + if (path == iso_device::virtual_device_name + "/") + { + auto device = fs::get_virtual_device(iso_device::virtual_device_name + "/"); + if (!device) return result::failure; + + auto iso_device = dynamic_cast(device.get()); + return add_game(key, iso_device->get_loaded_iso()); + } + std::lock_guard lock(m_mutex); // Access or create node if does not exist diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 9563c46fb5..219b4075b8 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -15,6 +15,7 @@ #include "Emu/vfs_config.h" #include "Emu/system_utils.hpp" #include "Loader/PSF.h" +#include "Loader/ISO.h" #include "util/types.hpp" #include "Utilities/File.h" #include "util/sysinfo.hpp" @@ -522,13 +523,46 @@ void game_list_frame::OnParsingFinished() const auto add_game = [this, localized_title, localized_icon, localized_movie, dev_flash, cat_unknown_localized = localized.category.unknown.toStdString(), cat_unknown = cat::cat_unknown.toStdString(), game_icon_path, _hdd, play_hover_movies = m_play_hover_movies, show_custom_icons = m_show_custom_icons](const std::string& dir_or_elf) { + const auto load_game_psf = [&dir_or_elf](const std::string& sfo_path, const std::unique_ptr& archive) + { + if (!archive) return psf::load_object(sfo_path); + + // HACK: psf does not accept a file_base argument, + // so we are creating a dummy fs:file and replacing the internal file_base handle with an iso_file + // instead. + fs::file psf_file(sfo_path); + psf_file.reset(std::make_unique(fs::file(dir_or_elf), *archive->retrieve(sfo_path))); + + return psf::load_object(psf_file, sfo_path); + }; + + std::unique_ptr archive; + if (is_file_iso(dir_or_elf)) + { + archive = std::make_unique(dir_or_elf); + } + + const auto file_exists = [&archive](const std::string& path) + { + if (!archive) return fs::is_file(path); + return archive->is_file(path); + }; + gui_game_info game{}; game.info.path = dir_or_elf; + if (archive) + { + fs::stat_t iso_stat; + fs::get_stat(game.info.path, iso_stat); + + game.info.size_on_disk = iso_stat.size; + } + const Localized thread_localized; - const std::string sfo_dir = rpcs3::utils::get_sfo_dir_from_game_path(dir_or_elf); - const psf::registry psf = psf::load_object(sfo_dir + "/PARAM.SFO"); + const std::string sfo_dir = !archive ? rpcs3::utils::get_sfo_dir_from_game_path(dir_or_elf) : "PS3_GAME"; + const psf::registry psf = load_game_psf(sfo_dir + "/PARAM.SFO", archive); const std::string_view title_id = psf::get_string(psf, "TITLE_ID", ""); if (title_id.empty()) @@ -596,7 +630,7 @@ void game_list_frame::OnParsingFinished() if (game.info.icon_path.empty()) { - if (std::string icon_path = sfo_dir + "/" + localized_icon; fs::is_file(icon_path)) + if (std::string icon_path = sfo_dir + "/" + localized_icon; file_exists(icon_path)) { game.info.icon_path = std::move(icon_path); } @@ -604,19 +638,35 @@ void game_list_frame::OnParsingFinished() { game.info.icon_path = sfo_dir + "/ICON0.PNG"; } + + if (!game.info.icon_path.empty() && archive) + { + if (!archive->exists(game.info.icon_path)) return; + + auto icon_file = archive->open(game.info.icon_path); + auto icon_size = icon_file.size(); + QByteArray data(icon_size, 0); + icon_file.read(data.data(), icon_size); + QImage iconImage; + if (iconImage.loadFromData(data)) + { + game.icon = QPixmap::fromImage(iconImage); + } + game.info.icon_path.clear(); + } } - if (std::string movie_path = game_icon_path + game.info.serial + "/hover.gif"; fs::is_file(movie_path)) + if (std::string movie_path = game_icon_path + game.info.serial + "/hover.gif"; file_exists(movie_path)) { game.info.movie_path = std::move(movie_path); game.has_hover_gif = true; } - else if (std::string movie_path = sfo_dir + "/" + localized_movie; fs::is_file(movie_path)) + else if (std::string movie_path = sfo_dir + "/" + localized_movie; file_exists(movie_path)) { game.info.movie_path = std::move(movie_path); game.has_hover_pam = true; } - else if (std::string movie_path = sfo_dir + "/ICON1.PAM"; fs::is_file(movie_path)) + else if (std::string movie_path = sfo_dir + "/ICON1.PAM"; file_exists(movie_path)) { game.info.movie_path = std::move(movie_path); game.has_hover_pam = true; @@ -741,6 +791,10 @@ void game_list_frame::OnParsingFinished() add_disc_dir(entry.path, legit_paths); } + else if (is_file_iso(entry.path)) + { + push_path(entry.path, legit_paths); + } else { game_list_log.trace("Invalid game path registered: %s", entry.path); From 1d2f6404c6eb086d1ceb52aca9ce31296b5647f4 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:36:43 +0000 Subject: [PATCH 06/47] ISO: Fix for save states --- rpcs3/Emu/System.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 006c0264c3..f7659af23a 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -783,7 +783,7 @@ bool Emulator::BootRsxCapture(const std::string& path) std::unique_ptr frame = std::make_unique(); utils::serial load; load.set_reading_state(); - + const std::string lower = fmt::to_lower(path); if (lower.ends_with(".gz") || lower.ends_with(".zst")) @@ -1408,6 +1408,10 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, m_path = rpcs3::utils::get_hdd1_dir(); m_path += std::string_view(argv[0]).substr(9); } + else if (is_file_iso(argv[0])) + { + m_path = argv[0]; + } else { sys_log.error("Unknown source for path redirection: %s", argv[0]); @@ -1818,6 +1822,10 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, sys_log.error("Failed to move disc game %s to '%s' (%s)", m_title_id, dst_dir, fs::g_tls_error); return game_boot_result::wrong_disc_location; } + else if (launching_from_disc_archive) + { + bdvd_dir = iso_device::virtual_device_name + "/"; + } } if (bdvd_dir.empty() && disc.empty() && !is_disc_patch) @@ -3405,7 +3413,7 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s { if (spu.first->pc != spu.second || spu.first->unsavable) { - std::string dump; + std::string dump; spu.first->dump_all(dump); sys_log.error("SPU thread continued after being paused. (old_pc=0x%x, pc=0x%x, unsavable=%d)", spu.second, spu.first->pc, spu.first->unsavable); @@ -3583,7 +3591,17 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s ar(std::array{}); // Reserved for future use - if (auto dir = vfs::get("/dev_bdvd/PS3_GAME"); fs::is_dir(dir) && !fs::is_file(fs::get_parent_dir(dir) + "/PS3_DISC.SFB")) + // Game mounted from archive + if (m_path.starts_with(iso_device::virtual_device_name + "/")) + { + auto device = fs::get_virtual_device(iso_device::virtual_device_name + "/"); + ensure(device); + + auto iso_device = dynamic_cast(device.get()); + ar(iso_device->get_loaded_iso()); + ar(m_title_id); + } + else if (auto dir = vfs::get("/dev_bdvd/PS3_GAME"); fs::is_dir(dir) && !fs::is_file(fs::get_parent_dir(dir) + "/PS3_DISC.SFB")) { // Fake /dev_bdvd/PS3_GAME detected, use HDD0 for m_path restoration ensure(vfs::unmount("/dev_bdvd/PS3_GAME")); From ab45b1bf6176f48192b2f7ea4b580921ff8dcd6a Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:38:54 +0000 Subject: [PATCH 07/47] ISO: Fix icons in savestate_manager_dialog --- rpcs3/rpcs3qt/savestate_manager_dialog.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/rpcs3/rpcs3qt/savestate_manager_dialog.cpp b/rpcs3/rpcs3qt/savestate_manager_dialog.cpp index ed664e1b65..7df0e235ca 100644 --- a/rpcs3/rpcs3qt/savestate_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/savestate_manager_dialog.cpp @@ -208,7 +208,7 @@ savestate_manager_dialog::savestate_manager_dialog(std::shared_ptr m_savestate_table->create_header_actions(m_savestate_column_acts, [this](int col) { return m_gui_settings->GetSavestateListColVisibility(static_cast(col)); }, [this](int col, bool visible) { m_gui_settings->SetSavestateListColVisibility(static_cast(col), visible); }); - + m_game_table->create_header_actions(m_game_column_acts, [this](int col) { return m_gui_settings->GetSavestateGamelistColVisibility(static_cast(col)); }, [this](int col, bool visible) { m_gui_settings->SetSavestateGamelistColVisibility(static_cast(col), visible); }); @@ -445,6 +445,18 @@ void savestate_manager_dialog::ResizeGameIcons() } } + if (icon.isNull()) + { + for (const game_info& gameinfo : m_game_info) + { + if (gameinfo && gameinfo->info.serial == m_savestate_db[savestate_index]->title_id) + { + icon = gameinfo->icon; + break; + } + } + } + if (cancel && cancel->load()) { return; From eff25ed0aaf491db6f676dd4cdd842cd28b077f8 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:42:01 +0000 Subject: [PATCH 08/47] ISO: Fix game movie icons --- rpcs3/rpcs3qt/game_list_table.cpp | 8 +++++ rpcs3/rpcs3qt/qt_video_source.cpp | 59 +++++++++++++++++++++++++------ rpcs3/rpcs3qt/qt_video_source.h | 2 ++ 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/rpcs3/rpcs3qt/game_list_table.cpp b/rpcs3/rpcs3qt/game_list_table.cpp index c3069ef67e..a91141eb57 100644 --- a/rpcs3/rpcs3qt/game_list_table.cpp +++ b/rpcs3/rpcs3qt/game_list_table.cpp @@ -11,6 +11,8 @@ #include "Emu/vfs_config.h" #include "Utilities/StrUtil.h" +#include "Loader/ISO.h" + #include #include #include @@ -242,6 +244,12 @@ void game_list_table::populate( custom_table_widget_item* icon_item = new custom_table_widget_item; game->item = icon_item; + if (is_file_iso(game->info.path) && !game->info.movie_path.empty() + && !fs::exists(game->info.movie_path)) + { + icon_item->set_source_path(game->info.path); + } + icon_item->set_image_change_callback([this, icon_item, game](const QVideoFrame& frame) { if (!icon_item || !game) diff --git a/rpcs3/rpcs3qt/qt_video_source.cpp b/rpcs3/rpcs3qt/qt_video_source.cpp index 107e5ef850..81e6d98e55 100644 --- a/rpcs3/rpcs3qt/qt_video_source.cpp +++ b/rpcs3/rpcs3qt/qt_video_source.cpp @@ -2,6 +2,8 @@ #include "Emu/System.h" #include "qt_video_source.h" +#include "Loader/ISO.h" + #include qt_video_source::qt_video_source() @@ -19,6 +21,11 @@ void qt_video_source::set_video_path(const std::string& video_path) m_video_path = QString::fromStdString(video_path); } +void qt_video_source::set_source_path(const std::string& source_path) +{ + m_source_path = QString::fromStdString(source_path); +} + void qt_video_source::set_active(bool active) { if (m_active.exchange(active) == active) return; @@ -55,7 +62,7 @@ void qt_video_source::init_movie() return; } - if (!m_image_change_callback || m_video_path.isEmpty() || !QFile::exists(m_video_path)) + if (!m_image_change_callback || m_video_path.isEmpty() || (!QFile::exists(m_video_path) && m_source_path.isEmpty())) { m_video_path.clear(); return; @@ -65,8 +72,25 @@ void qt_video_source::init_movie() if (lower.endsWith(".gif")) { - m_movie = std::make_unique(m_video_path); - m_video_path.clear(); + if (m_source_path.isEmpty()) + { + m_movie = std::make_unique(m_video_path); + m_video_path.clear(); + } + else + { + iso_archive archive(m_source_path.toStdString()); + auto movie_file = archive.open(m_video_path.toStdString()); + auto movie_size = movie_file.size(); + m_video_data = QByteArray(movie_size, 0); + movie_file.read(m_video_data.data(), movie_size); + + QBuffer buffer(&m_video_data); + buffer.open(QIODevice::ReadOnly); + m_movie = std::make_unique(&buffer); + + m_video_path.clear(); + } if (!m_movie->isValid()) { @@ -85,17 +109,30 @@ void qt_video_source::init_movie() if (lower.endsWith(".pam")) { // We can't set PAM files as source of the video player, so we have to feed them as raw data. - QFile file(m_video_path); - if (!file.open(QFile::OpenModeFlag::ReadOnly)) + if (m_source_path.isEmpty()) { - return; - } + QFile file(m_video_path); + if (!file.open(QFile::OpenModeFlag::ReadOnly)) + { + return; + } - // TODO: Decode the pam properly before pushing it to the player - m_video_data = file.readAll(); - if (m_video_data.isEmpty()) + // TODO: Decode the pam properly before pushing it to the player + m_video_data = file.readAll(); + if (m_video_data.isEmpty()) + { + return; + } + } + else { - return; + auto source_path = m_source_path.toStdString(); + auto video_path = m_video_path.toStdString(); + iso_archive archive(source_path.c_str()); + auto movie_file = archive.open(video_path); + auto movie_size = movie_file.size(); + m_video_data = QByteArray(movie_size, 0); + movie_file.read(m_video_data.data(), movie_size); } m_video_buffer = std::make_unique(&m_video_data); diff --git a/rpcs3/rpcs3qt/qt_video_source.h b/rpcs3/rpcs3qt/qt_video_source.h index a2710eea33..1f195def02 100644 --- a/rpcs3/rpcs3qt/qt_video_source.h +++ b/rpcs3/rpcs3qt/qt_video_source.h @@ -17,6 +17,7 @@ public: qt_video_source(); virtual ~qt_video_source(); + void set_source_path(const std::string& source_path); void set_video_path(const std::string& video_path) override; const QString& video_path() const { return m_video_path; } @@ -43,6 +44,7 @@ protected: atomic_t m_has_new = false; QString m_video_path; + QString m_source_path; // path of the source archive QByteArray m_video_data{}; QImage m_image{}; std::vector m_image_path; From 673cc040faff80ae6607f9e91ec2add128df86b1 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:19:09 +0000 Subject: [PATCH 09/47] ISO: Add UI support Adds ISO file type to load dialog and allows launching ISO games via drag and drop. --- rpcs3/rpcs3qt/main_window.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 485f7e3f22..0456da3502 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -569,6 +569,7 @@ void main_window::BootElf() "SELF files (EBOOT.BIN *.self);;" "BOOT files (*BOOT.BIN);;" "BIN files (*.bin);;" + "ISO files (*.iso);;" "All executable files (*.SAVESTAT.zst *.SAVESTAT.gz *.SAVESTAT *.sprx *.SPRX *.self *.SELF *.bin *.BIN *.prx *.PRX *.elf *.ELF *.o *.O);;" "All files (*.*)"), Q_NULLPTR, QFileDialog::DontResolveSymlinks); @@ -4019,7 +4020,7 @@ main_window::drop_type main_window::IsValidFile(const QMimeData& md, QStringList type = drop_type::drop_rrc; } // The emulator allows to execute ANY filetype, just not from drag-and-drop because it is confusing to users - else if (path.toLower().endsWith(".savestat.gz") || path.toLower().endsWith(".savestat.zst") || suffix_lo == "savestat" || suffix_lo == "sprx" || suffix_lo == "self" || suffix_lo == "bin" || suffix_lo == "prx" || suffix_lo == "elf" || suffix_lo == "o") + else if (path.toLower().endsWith(".savestat.gz") || path.toLower().endsWith(".savestat.zst") || suffix_lo == "savestat" || suffix_lo == "sprx" || suffix_lo == "self" || suffix_lo == "bin" || suffix_lo == "prx" || suffix_lo == "elf" || suffix_lo == "o" || suffix_lo == "iso") { type = drop_type::drop_game; } From acdb0ee72922a82d24a3d568b3bbd23802269980 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Tue, 9 Dec 2025 23:53:57 +0000 Subject: [PATCH 10/47] ISO: Small clean up Cleaning up old comments and fixing small mistakes in ISO.cpp --- rpcs3/Loader/ISO.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index db11f14c84..4ba55fae78 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -7,7 +7,6 @@ #include #include -// TODO: replace with file check for iso! bool is_file_iso(const std::string& path) { if (fs::is_dir(path)) return false; @@ -355,7 +354,7 @@ std::pair iso_file::get_extent_pos(u64 pos) const { auto it = m_meta.extents.begin(); - while(pos >= it->size && it < m_meta.extents.end() - 1) + while(pos >= it->size && it != m_meta.extents.end() - 1) { pos -= it->size; @@ -405,7 +404,7 @@ u64 iso_file::read_at(u64 offset, void* buffer, u64 size) if (size > total_read && (offset + total_read) < total_size) { u64 second_total_read = read_at(offset + total_read, - static_cast(buffer) + total_read, + reinterpret_cast(buffer) + total_read, size - total_read ); @@ -580,6 +579,7 @@ std::unique_ptr iso_device::open_dir(const std::string& path) // pointing to a file instead of a folder, which translates to error::unknown. // doing the same here. fs::g_tls_error = fs::error::unknown; + return nullptr; } return std::make_unique(*node); From 27510f5fa87a60b83dd16ec7cfe57782bf7b3f97 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Sat, 13 Dec 2025 12:05:34 +0000 Subject: [PATCH 11/47] ISO: Move game image loading to game_list_base::IconLoadFunction --- rpcs3/rpcs3qt/game_list_base.cpp | 17 ++++++++++++++++- rpcs3/rpcs3qt/game_list_frame.cpp | 11 +---------- rpcs3/rpcs3qt/gui_game_info.h | 1 + 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/rpcs3/rpcs3qt/game_list_base.cpp b/rpcs3/rpcs3qt/game_list_base.cpp index 51e43c95bf..e4fb012b17 100644 --- a/rpcs3/rpcs3qt/game_list_base.cpp +++ b/rpcs3/rpcs3qt/game_list_base.cpp @@ -1,6 +1,8 @@ #include "stdafx.h" #include "game_list_base.h" +#include "Loader/ISO.h" + #include #include @@ -50,7 +52,20 @@ void game_list_base::IconLoadFunction(game_info game, qreal device_pixel_ratio, if (game->icon.isNull() && (game->info.icon_path.empty() || !game->icon.load(QString::fromStdString(game->info.icon_path)))) { - if (game_list_log.warning) + if (game->icon_in_archive) + { + iso_archive archive(game->info.path); + auto icon_file = archive.open(game->info.icon_path); + auto icon_size = icon_file.size(); + QByteArray data(icon_size, 0); + icon_file.read(data.data(), icon_size); + QImage iconImage; + if (iconImage.loadFromData(data)) + { + game->icon = QPixmap::fromImage(iconImage); + } + } + else if (game_list_log.warning) { bool logged = false; { diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 219b4075b8..6c8dee2a15 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -643,16 +643,7 @@ void game_list_frame::OnParsingFinished() { if (!archive->exists(game.info.icon_path)) return; - auto icon_file = archive->open(game.info.icon_path); - auto icon_size = icon_file.size(); - QByteArray data(icon_size, 0); - icon_file.read(data.data(), icon_size); - QImage iconImage; - if (iconImage.loadFromData(data)) - { - game.icon = QPixmap::fromImage(iconImage); - } - game.info.icon_path.clear(); + game.icon_in_archive = true; } } diff --git a/rpcs3/rpcs3qt/gui_game_info.h b/rpcs3/rpcs3qt/gui_game_info.h index 984f8eb7bf..3b19d27458 100644 --- a/rpcs3/rpcs3qt/gui_game_info.h +++ b/rpcs3/rpcs3qt/gui_game_info.h @@ -20,6 +20,7 @@ struct gui_game_info bool has_custom_icon = false; bool has_hover_gif = false; bool has_hover_pam = false; + bool icon_in_archive = false; movie_item_base* item = nullptr; // Returns the visible version string in the game list From 2533aa02d01269487d09cdbc03a299c200562162 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Sat, 13 Dec 2025 12:51:38 +0000 Subject: [PATCH 12/47] ISO: Don't access m_game_info from icon_load_func --- rpcs3/rpcs3qt/savestate_manager_dialog.cpp | 26 +++++++++++++++------- rpcs3/rpcs3qt/savestate_manager_dialog.h | 1 + 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/rpcs3/rpcs3qt/savestate_manager_dialog.cpp b/rpcs3/rpcs3qt/savestate_manager_dialog.cpp index 7df0e235ca..1af19a197c 100644 --- a/rpcs3/rpcs3qt/savestate_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/savestate_manager_dialog.cpp @@ -7,6 +7,8 @@ #include "gui_settings.h" #include "progress_dialog.h" +#include "Loader/ISO.h" + #include #include #include @@ -417,8 +419,9 @@ void savestate_manager_dialog::ResizeGameIcons() const qreal dpr = devicePixelRatioF(); const int savestate_index = item->data(GameUserRole::GameIndex).toInt(); const std::string icon_path = m_savestate_db[savestate_index]->game_icon_path; + const std::string archive_path = m_savestate_db[savestate_index]->archive_path; - item->set_icon_load_func([this, icon_path, savestate_index, cancel = item->icon_loading_aborted(), dpr](int index) + item->set_icon_load_func([this, icon_path, archive_path, savestate_index, cancel = item->icon_loading_aborted(), dpr](int index) { if (cancel && cancel->load()) { @@ -445,15 +448,17 @@ void savestate_manager_dialog::ResizeGameIcons() } } - if (icon.isNull()) + if (!archive_path.empty()) { - for (const game_info& gameinfo : m_game_info) + iso_archive archive(archive_path); + auto icon_file = archive.open(icon_path); + auto icon_size = icon_file.size(); + QByteArray data(icon_size, 0); + icon_file.read(data.data(), icon_size); + QImage iconImage; + if (iconImage.loadFromData(data)) { - if (gameinfo && gameinfo->info.serial == m_savestate_db[savestate_index]->title_id) - { - icon = gameinfo->icon; - break; - } + icon = QPixmap::fromImage(iconImage); } } @@ -629,6 +634,11 @@ void savestate_manager_dialog::StartSavestateLoadThreads() { game_data_ptr->game_name = gameinfo->info.name; game_data_ptr->game_icon_path = gameinfo->info.icon_path; + if (gameinfo->icon_in_archive) + { + game_data_ptr->archive_path = gameinfo->info.path; + } + break; } } diff --git a/rpcs3/rpcs3qt/savestate_manager_dialog.h b/rpcs3/rpcs3qt/savestate_manager_dialog.h index 28c3eddc8c..7a6a39a700 100644 --- a/rpcs3/rpcs3qt/savestate_manager_dialog.h +++ b/rpcs3/rpcs3qt/savestate_manager_dialog.h @@ -48,6 +48,7 @@ private: std::string game_name; std::string game_icon_path; std::string dir_path; + std::string archive_path; }; bool LoadSavestateFolderToDB(std::unique_ptr&& game_savestates); From 257043529ae264fa2f4815aa3aace83199ebd6a7 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Sat, 13 Dec 2025 13:17:13 +0000 Subject: [PATCH 13/47] ISO: Loader improvements - Don't use floating point when advancing sectors - Remove redudndant seek in iso_archive constructor - Drop error code checks in iso_file::read_at - Don't use lexically_normal for handling path in iso_archive::retrieve. More complete directory search implementation. --- rpcs3/Loader/ISO.cpp | 60 ++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index 4ba55fae78..f97d7fd026 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -6,6 +6,7 @@ #include #include #include +#include bool is_file_iso(const std::string& path) { @@ -166,9 +167,7 @@ void iso_form_hierarchy(fs::file& file, iso_fs_node& node, auto entry = iso_read_directory_entry(file, use_ucs2_decoding); if (!entry) { - float block_size = ISO_BLOCK_SIZE; - float t = std::floor(file.pos() / block_size); - u64 new_sector = t+1; + u64 new_sector = (file.pos() / ISO_BLOCK_SIZE) + 1; file.seek(new_sector * ISO_BLOCK_SIZE); continue; } @@ -253,8 +252,6 @@ iso_archive::iso_archive(const std::string& path) { .metadata = iso_read_directory_entry(m_file, use_ucs2_decoding).value(), }; - - m_file.seek(descriptor_start); } m_file.seek(descriptor_start + ISO_BLOCK_SIZE); @@ -266,15 +263,19 @@ iso_archive::iso_archive(const std::string& path) iso_fs_node* iso_archive::retrieve(const std::string& passed_path) { - std::string path = std::filesystem::path(passed_path).lexically_normal().string(); + std::string path = std::filesystem::path(passed_path).string(); size_t start = 0; size_t end = path.find_first_of(fs::delim); - auto dir_entry = &m_root; + std::stack search_stack; + search_stack.push(&m_root); do { + if (search_stack.empty()) return nullptr; + auto* top_entry = search_stack.top(); + if (end == std::string::npos) { end = path.size(); @@ -283,27 +284,40 @@ iso_fs_node* iso_archive::retrieve(const std::string& passed_path) auto path_component = path.substr(start, end-start); bool found = false; - for (const auto& entry : dir_entry->children) - { - if (entry->metadata.name.compare(path_component) == 0) - { - dir_entry = entry.get(); - start = end + 1; - end = path.find_first_of(fs::delim, start); - found = true; - break; + if (path_component == ".") + { + found = true; + } + else if (path_component == "..") + { + search_stack.pop(); + found = true; + } + else + { + for (const auto& entry : top_entry->children) + { + if (entry->metadata.name.compare(path_component) == 0) + { + search_stack.push(entry.get()); + + found = true; + break; + } } } - if (!found) - { - return nullptr; - } + if (!found) return nullptr; + + start = end + 1; + end = path.find_first_of(fs::delim, start); } while(start < path.size()); - return dir_entry; + if (search_stack.empty()) return nullptr; + + return search_stack.top(); } bool iso_archive::exists(const std::string& path) @@ -393,11 +407,9 @@ u64 iso_file::read(void* buffer, u64 size) u64 iso_file::read_at(u64 offset, void* buffer, u64 size) { - const u64 bad_res = -1; u64 local_remaining = local_extent_remaining(offset); u64 total_read = m_file.read_at(file_offset(offset), buffer, std::min(size, local_remaining)); - if (total_read == bad_res) return -1; auto total_size = this->size(); @@ -408,8 +420,6 @@ u64 iso_file::read_at(u64 offset, void* buffer, u64 size) size - total_read ); - if (second_total_read == bad_res) return -1; - return total_read + second_total_read; } From 48f39acac7e68a260c265224def79e810eac94f3 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Sat, 13 Dec 2025 14:02:09 +0000 Subject: [PATCH 14/47] ISO: Unload the iso_device on shutdown --- rpcs3/Emu/System.cpp | 1 + rpcs3/Loader/ISO.cpp | 9 ++++++--- rpcs3/Loader/ISO.h | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index f7659af23a..0325807e1e 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -3886,6 +3886,7 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s disc.clear(); klic.clear(); hdd1.clear(); + unload_iso(); init_mem_containers = nullptr; m_config_path.clear(); m_config_mode = cfg_mode::custom; diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index f97d7fd026..bee017a59d 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -602,9 +602,12 @@ void iso_dir::rewind() void load_iso(const std::string& path) { - fs::set_virtual_device("iso_overlay_fs_dev", - stx::shared_ptr()); - fs::set_virtual_device("iso_overlay_fs_dev", stx::make_shared(path)); } + +void unload_iso() +{ + fs::set_virtual_device("iso_overlay_fs_dev", + stx::shared_ptr()); +} diff --git a/rpcs3/Loader/ISO.h b/rpcs3/Loader/ISO.h index bc3d1faa2b..f986899db8 100644 --- a/rpcs3/Loader/ISO.h +++ b/rpcs3/Loader/ISO.h @@ -7,6 +7,7 @@ bool is_file_iso(const std::string& path); bool is_file_iso(const fs::file& path); void load_iso(const std::string& path); +void unload_iso(); struct iso_extent_info { From fa11af1cbab32855bfdc034f3f6725dd4a901e6f Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Sun, 14 Dec 2025 13:08:34 +0000 Subject: [PATCH 15/47] ISO: Remove resolve_path hack - Modify Emulator::GetFakeCat to return "DG" for mounted ISO games like expected. - Undo changes to main_application resolve_path callback --- rpcs3/Emu/System.cpp | 2 +- rpcs3/main_application.cpp | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 0325807e1e..8cd25fdf35 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -4387,7 +4387,7 @@ const std::string& Emulator::GetFakeCat() const { const std::string mount_point = vfs::get("/dev_bdvd"); - if (mount_point.empty() || !IsPathInsideDir(m_path, mount_point)) + if (mount_point.empty() || (!IsPathInsideDir(m_path, mount_point) && !m_path.starts_with(iso_device::virtual_device_name))) { static const std::string s_hg = "HG"; return s_hg; diff --git a/rpcs3/main_application.cpp b/rpcs3/main_application.cpp index aa4d035f27..ff3b70f952 100644 --- a/rpcs3/main_application.cpp +++ b/rpcs3/main_application.cpp @@ -345,8 +345,7 @@ EmuCallbacks main_application::CreateCallbacks() callbacks.resolve_path = [](std::string_view sv) { // May result in an empty string if path does not exist - std::string result = QFileInfo(QString::fromUtf8(sv.data(), static_cast(sv.size()))).canonicalFilePath().toStdString(); - return !result.empty() ? result : std::string(sv); + return QFileInfo(QString::fromUtf8(sv.data(), static_cast(sv.size()))).canonicalFilePath().toStdString(); }; callbacks.get_font_dirs = []() From 095f433f8ee51e0cd4e625f27d83836844d223e0 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:18:14 +0000 Subject: [PATCH 16/47] ISO: Fix loading games in continuous mode Some games will load into another game and continuous mode will be set. Currently, if this happens the ISO will get unloaded in the shutdown function. This commit adds an additional check before calling the unload function to fix this. --- rpcs3/Emu/System.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 8cd25fdf35..f0addb4be1 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -3886,7 +3886,6 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s disc.clear(); klic.clear(); hdd1.clear(); - unload_iso(); init_mem_containers = nullptr; m_config_path.clear(); m_config_mode = cfg_mode::custom; @@ -3896,6 +3895,11 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s m_emu_state_close_pending = false; m_precompilation_option = {}; + if (!m_continuous_mode) + { + unload_iso(); + } + initialize_timebased_time(0, true); // Complete the operation From 38daf8d325f606df89edbe478d1c09760485901f Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:36:25 +0000 Subject: [PATCH 17/47] ISO: Add empty path check to iso_archive::retrieve --- rpcs3/Loader/ISO.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index bee017a59d..9804407767 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -263,6 +263,8 @@ iso_archive::iso_archive(const std::string& path) iso_fs_node* iso_archive::retrieve(const std::string& passed_path) { + if (passed_path.empty()) return nullptr; + std::string path = std::filesystem::path(passed_path).string(); size_t start = 0; From 4cd75971f0024199de44d5b055d4a0eec9afb502 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:39:05 +0000 Subject: [PATCH 18/47] ISO: Move code to load psf::registry into iso_archive - Multiple places end up needing to retrieve a psf::registry from an is o archive. - Added a safety check along with this --- rpcs3/Loader/ISO.cpp | 13 +++++++++++++ rpcs3/Loader/ISO.h | 4 ++++ rpcs3/rpcs3qt/game_list_frame.cpp | 17 +++-------------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index 9804407767..3c3a0a8c20 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -340,6 +340,19 @@ iso_file iso_archive::open(const std::string& path) return iso_file(fs::file(m_path), *retrieve(path)); } +psf::registry iso_archive::open_psf(const std::string& path) +{ + auto* archive_file = retrieve(path); + if (!archive_file) return psf::registry(); + + // HACK: psf does not accept a file_base argument, + // instead we are creating a dummy fs::file and replacing the internal file_base handle with an iso_file + fs::file psf_file(path); + psf_file.reset(std::make_unique(fs::file(m_path), *archive_file)); + + return psf::load_object(psf_file, path); +} + iso_file::iso_file(fs::file&& iso_handle, const iso_fs_node& node) : m_file(std::move(iso_handle)), m_meta(node.metadata), m_pos(0) { diff --git a/rpcs3/Loader/ISO.h b/rpcs3/Loader/ISO.h index f986899db8..55c8b99f16 100644 --- a/rpcs3/Loader/ISO.h +++ b/rpcs3/Loader/ISO.h @@ -1,5 +1,7 @@ #pragma once +#include "Loader/PSF.h" + #include "Utilities/File.h" #include "util/types.hpp" @@ -86,6 +88,8 @@ class iso_archive bool is_file(const std::string& path); iso_file open(const std::string& path); + + psf::registry open_psf(const std::string& path); }; class iso_device : public fs::device_base diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 6c8dee2a15..34aa4147f2 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -523,19 +523,6 @@ void game_list_frame::OnParsingFinished() const auto add_game = [this, localized_title, localized_icon, localized_movie, dev_flash, cat_unknown_localized = localized.category.unknown.toStdString(), cat_unknown = cat::cat_unknown.toStdString(), game_icon_path, _hdd, play_hover_movies = m_play_hover_movies, show_custom_icons = m_show_custom_icons](const std::string& dir_or_elf) { - const auto load_game_psf = [&dir_or_elf](const std::string& sfo_path, const std::unique_ptr& archive) - { - if (!archive) return psf::load_object(sfo_path); - - // HACK: psf does not accept a file_base argument, - // so we are creating a dummy fs:file and replacing the internal file_base handle with an iso_file - // instead. - fs::file psf_file(sfo_path); - psf_file.reset(std::make_unique(fs::file(dir_or_elf), *archive->retrieve(sfo_path))); - - return psf::load_object(psf_file, sfo_path); - }; - std::unique_ptr archive; if (is_file_iso(dir_or_elf)) { @@ -562,7 +549,9 @@ void game_list_frame::OnParsingFinished() const Localized thread_localized; const std::string sfo_dir = !archive ? rpcs3::utils::get_sfo_dir_from_game_path(dir_or_elf) : "PS3_GAME"; - const psf::registry psf = load_game_psf(sfo_dir + "/PARAM.SFO", archive); + const std::string sfo_path = sfo_dir + "/PARAM.SFO"; + + const psf::registry psf = !archive ? psf::load_object(sfo_path) : archive->open_psf(sfo_path); const std::string_view title_id = psf::get_string(psf, "TITLE_ID", ""); if (title_id.empty()) From 407cbf9690f55d54ef73b3496891fa8e339b2ea6 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:47:12 +0000 Subject: [PATCH 19/47] ISO: Add ISO games through Emulator::AddGamesFromDir - When dragging and dropping ISO files, they will be treated the same as game directories currently are and added to the game list rather than immediately being opened. --- rpcs3/Emu/System.cpp | 35 ++++++++++++++++++++++++++++++++--- rpcs3/rpcs3qt/main_window.cpp | 5 +++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index f0addb4be1..2608de03e3 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -4099,13 +4099,18 @@ u32 Emulator::AddGamesFromDir(const std::string& path) { auto dir_entry = std::move(*path_it); - if (!dir_entry.is_directory || dir_entry.name == "." || dir_entry.name == "..") + if (dir_entry.name == "." || dir_entry.name == "..") { continue; } const std::string dir_path = path + '/' + dir_entry.name; + if (!dir_entry.is_directory && !is_file_iso(dir_path)) + { + continue; + } + if (const game_boot_result error = AddGame(dir_path); error == game_boot_result::no_errors) { games_added++; @@ -4203,10 +4208,17 @@ game_boot_result Emulator::AddGameToYml(const std::string& path) return error; } + std::unique_ptr archive; + if (is_file_iso(path)) + { + archive = std::make_unique(path); + } + // Load PARAM.SFO const std::string elf_dir = fs::get_parent_dir(path); - std::string sfo_dir = rpcs3::utils::get_sfo_dir_from_game_path(fs::get_parent_dir(elf_dir)); - const psf::registry _psf = psf::load_object(sfo_dir + "/PARAM.SFO"); + std::string sfo_dir = !archive ? rpcs3::utils::get_sfo_dir_from_game_path(fs::get_parent_dir(elf_dir)) : "PS3_GAME"; + const std::string sfo_path = sfo_dir + "/PARAM.SFO"; + const psf::registry _psf = !archive ? psf::load_object(sfo_path) : archive->open_psf(sfo_path); const std::string title_id = std::string(psf::get_string(_psf, "TITLE_ID")); const std::string cat = std::string(psf::get_string(_psf, "CATEGORY")); @@ -4229,6 +4241,23 @@ game_boot_result Emulator::AddGameToYml(const std::string& path) return game_boot_result::invalid_file_or_folder; } + // Add ISO game + if (archive) + { + if (cat == "DG") + { + std::string iso_path = path; + switch (m_games_config.add_external_hdd_game(title_id, iso_path)) + { + case games_config::result::failure: return game_boot_result::generic_error; + case games_config::result::success: return game_boot_result::no_errors; + case games_config::result::exists: return game_boot_result::already_added; + } + + return game_boot_result::generic_error; + } + } + // Set bdvd_dir std::string bdvd_dir; std::string game_dir; diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 0456da3502..b32e2ded34 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -78,6 +78,7 @@ #include "Loader/PUP.h" #include "Loader/TAR.h" #include "Loader/PSF.h" +#include "Loader/ISO.h" #include "Loader/mself.hpp" #include "Utilities/Thread.h" @@ -3959,7 +3960,7 @@ main_window::drop_type main_window::IsValidFile(const QMimeData& md, QStringList const QString suffix_lo = info.suffix().toLower(); // check for directories first, only valid if all other paths led to directories until now. - if (info.isDir()) + if (info.isDir() || is_file_iso(path.toStdString())) { if (type != drop_type::drop_dir && type != drop_type::drop_error) { @@ -4020,7 +4021,7 @@ main_window::drop_type main_window::IsValidFile(const QMimeData& md, QStringList type = drop_type::drop_rrc; } // The emulator allows to execute ANY filetype, just not from drag-and-drop because it is confusing to users - else if (path.toLower().endsWith(".savestat.gz") || path.toLower().endsWith(".savestat.zst") || suffix_lo == "savestat" || suffix_lo == "sprx" || suffix_lo == "self" || suffix_lo == "bin" || suffix_lo == "prx" || suffix_lo == "elf" || suffix_lo == "o" || suffix_lo == "iso") + else if (path.toLower().endsWith(".savestat.gz") || path.toLower().endsWith(".savestat.zst") || suffix_lo == "savestat" || suffix_lo == "sprx" || suffix_lo == "self" || suffix_lo == "bin" || suffix_lo == "prx" || suffix_lo == "elf" || suffix_lo == "o") { type = drop_type::drop_game; } From bc1d0b9e6548c09d1f0d612d894146dc3604bbe1 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:51:01 +0000 Subject: [PATCH 20/47] ISO: Move game size assignment to size_calc_func in game_list_table.cpp --- rpcs3/rpcs3qt/game_list_frame.cpp | 8 -------- rpcs3/rpcs3qt/game_list_table.cpp | 7 +++++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 34aa4147f2..35790c016f 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -538,14 +538,6 @@ void game_list_frame::OnParsingFinished() gui_game_info game{}; game.info.path = dir_or_elf; - if (archive) - { - fs::stat_t iso_stat; - fs::get_stat(game.info.path, iso_stat); - - game.info.size_on_disk = iso_stat.size; - } - const Localized thread_localized; const std::string sfo_dir = !archive ? rpcs3::utils::get_sfo_dir_from_game_path(dir_or_elf) : "PS3_GAME"; diff --git a/rpcs3/rpcs3qt/game_list_table.cpp b/rpcs3/rpcs3qt/game_list_table.cpp index a91141eb57..2ab2013b19 100644 --- a/rpcs3/rpcs3qt/game_list_table.cpp +++ b/rpcs3/rpcs3qt/game_list_table.cpp @@ -285,6 +285,13 @@ void game_list_table::populate( // Do not report size of apps inside /dev_flash (it does not make sense to do so) game->info.size_on_disk = 0; } + else if(is_file_iso(game->info.path)) + { + fs::stat_t iso_stat; + fs::get_stat(game->info.path, iso_stat); + + game->info.size_on_disk = iso_stat.size; + } else { game->info.size_on_disk = fs::get_dir_size(game->info.path, 1, cancel.get()); From 714359b5445935e8fc34deee6a098475ae31b622 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Wed, 7 Jan 2026 19:02:27 +0000 Subject: [PATCH 21/47] ISO: Move icon loading to a common function in qt_utils.h --- rpcs3/rpcs3qt/game_list_base.cpp | 17 ++++----------- rpcs3/rpcs3qt/qt_utils.cpp | 24 ++++++++++++++++++++++ rpcs3/rpcs3qt/qt_utils.h | 3 +++ rpcs3/rpcs3qt/savestate_manager_dialog.cpp | 11 +--------- 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/rpcs3/rpcs3qt/game_list_base.cpp b/rpcs3/rpcs3qt/game_list_base.cpp index e4fb012b17..4105d754b0 100644 --- a/rpcs3/rpcs3qt/game_list_base.cpp +++ b/rpcs3/rpcs3qt/game_list_base.cpp @@ -1,8 +1,7 @@ #include "stdafx.h" +#include "qt_utils.h" #include "game_list_base.h" -#include "Loader/ISO.h" - #include #include @@ -54,18 +53,10 @@ void game_list_base::IconLoadFunction(game_info game, qreal device_pixel_ratio, { if (game->icon_in_archive) { - iso_archive archive(game->info.path); - auto icon_file = archive.open(game->info.icon_path); - auto icon_size = icon_file.size(); - QByteArray data(icon_size, 0); - icon_file.read(data.data(), icon_size); - QImage iconImage; - if (iconImage.loadFromData(data)) - { - game->icon = QPixmap::fromImage(iconImage); - } + game->icon_in_archive = gui::utils::load_icon(game->icon, game->info.icon_path, game->info.path); } - else if (game_list_log.warning) + + if (!game->icon_in_archive && game_list_log.warning) { bool logged = false; { diff --git a/rpcs3/rpcs3qt/qt_utils.cpp b/rpcs3/rpcs3qt/qt_utils.cpp index fc53dc542a..b10799a453 100644 --- a/rpcs3/rpcs3qt/qt_utils.cpp +++ b/rpcs3/rpcs3qt/qt_utils.cpp @@ -12,6 +12,7 @@ #include "Emu/system_utils.hpp" #include "Utilities/File.h" +#include "Loader/ISO.h" #include LOG_CHANNEL(gui_log, "GUI"); @@ -682,6 +683,29 @@ namespace gui return QString("%1 days ago %2").arg(current_date - exctrated_date).arg(date.toString(fmt_relative)); } + bool load_icon(QPixmap& icon, const std::string& icon_path, + const std::string& archive_path) + { + if (!is_file_iso(archive_path)) return false; + + iso_archive archive(archive_path); + if (!archive.exists(icon_path)) return false; + + auto icon_file = archive.open(icon_path); + auto icon_size = icon_file.size(); + + QByteArray data(icon_size, 0); + icon_file.read(data.data(), icon_size); + QImage iconImage; + + if (iconImage.loadFromData(data)) + { + icon = QPixmap::fromImage(iconImage); + } + + return true; + } + QString format_timestamp(s64 time, const QString& fmt) { return format_datetime(datetime(time), fmt); diff --git a/rpcs3/rpcs3qt/qt_utils.h b/rpcs3/rpcs3qt/qt_utils.h index dc15385923..de2473c083 100644 --- a/rpcs3/rpcs3qt/qt_utils.h +++ b/rpcs3/rpcs3qt/qt_utils.h @@ -178,6 +178,9 @@ namespace gui return color_scheme() == Qt::ColorScheme::Dark; } + // Loads an icon from an (ISO) archive file. + bool load_icon(QPixmap& icon, const std::string& icon_path, const std::string& archive_path); + template void stop_future_watcher(QFutureWatcher& watcher, bool cancel, std::shared_ptr> cancel_flag = nullptr) { diff --git a/rpcs3/rpcs3qt/savestate_manager_dialog.cpp b/rpcs3/rpcs3qt/savestate_manager_dialog.cpp index 1af19a197c..9e871800e2 100644 --- a/rpcs3/rpcs3qt/savestate_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/savestate_manager_dialog.cpp @@ -450,16 +450,7 @@ void savestate_manager_dialog::ResizeGameIcons() if (!archive_path.empty()) { - iso_archive archive(archive_path); - auto icon_file = archive.open(icon_path); - auto icon_size = icon_file.size(); - QByteArray data(icon_size, 0); - icon_file.read(data.data(), icon_size); - QImage iconImage; - if (iconImage.loadFromData(data)) - { - icon = QPixmap::fromImage(iconImage); - } + gui::utils::load_icon(icon, icon_path, archive_path); } if (cancel && cancel->load()) From 59f9a7c080ac3b55df5ba61857ecf61ab5933370 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Wed, 7 Jan 2026 19:45:19 +0000 Subject: [PATCH 22/47] ISO: Use clearer code to set game.icon_in_archive - Suggested change from PR review --- rpcs3/rpcs3qt/game_list_frame.cpp | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 35790c016f..4a695c9828 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -619,13 +619,7 @@ void game_list_frame::OnParsingFinished() { game.info.icon_path = sfo_dir + "/ICON0.PNG"; } - - if (!game.info.icon_path.empty() && archive) - { - if (!archive->exists(game.info.icon_path)) return; - - game.icon_in_archive = true; - } + game.icon_in_archive = archive && archive->exists(game.info.icon_path); } if (std::string movie_path = game_icon_path + game.info.serial + "/hover.gif"; file_exists(movie_path)) From a2d76aedaa16edafb033daec4de5c001b27b6a7a Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:07:57 +0000 Subject: [PATCH 23/47] ISO: Fix recent actions created when launching ISO games --- rpcs3/rpcs3qt/main_window.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index b32e2ded34..331e25e2fc 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -543,8 +543,16 @@ void main_window::Boot(const std::string& path, const std::string& title_id, boo } else { + std::string game_path = Emu.GetBoot(); + if (game_path.starts_with(iso_device::virtual_device_name)) + { + auto device = fs::get_virtual_device(iso_device::virtual_device_name + "/"); + auto iso_device = dynamic_cast(device.get()); + game_path = iso_device->get_loaded_iso(); + } + gui_log.success("Boot successful."); - AddRecentAction(gui::Recent_Game(QString::fromStdString(Emu.GetBoot()), QString::fromStdString(Emu.GetTitleAndTitleID())), false); + AddRecentAction(gui::Recent_Game(QString::fromStdString(game_path), QString::fromStdString(Emu.GetTitleAndTitleID())), false); } if (refresh_list) From 59a1f81e86fc1424c8ec2ec3263891422fd2c912 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:58:13 +0000 Subject: [PATCH 24/47] ISO: Game loading fixes - After undoing the changes to do with resolve_path, loading game updates was broken. This required the condition to be changed to check for launching_from_disc_archive. - When attempting to load an update for an ISO game, inherited_ps3_game_path needs to be set otherwise launching the updated game will fail. - An additional check was added to ensure that the inherited vitual game directory isn't saved as the game path in games.yml. - Fixed attempting to load an updated ISO game by launching its updated executable, which can happen when loading a savestate for a game with updates, the emulator would crash before. - Unload ISO file when Load() fails, otherwise ISO loading might be stuck in a broken state. --- rpcs3/Emu/System.cpp | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 2608de03e3..4f6a4eb680 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -932,6 +932,7 @@ game_boot_result Emulator::BootGame(const std::string& path, const std::string& if (result != game_boot_result::no_errors) { + unload_iso(); GetCallbacks().close_gs_frame(); } } @@ -1184,6 +1185,11 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, // Load /dev_bdvd/ from game list if available if (std::string game_path = m_games_config.get_path(m_title_id); !game_path.empty()) { + if (is_file_iso(game_path)) + { + game_path = iso_device::virtual_device_name + "/PS3_GAME/./"; + } + if (game_path.ends_with("/./")) { // Marked as PS3_GAME directory @@ -1840,6 +1846,13 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, // Load /dev_bdvd/ from game list if available if (std::string game_path = m_games_config.get_path(m_title_id); !game_path.empty()) { + if (is_file_iso(game_path)) + { + load_iso(game_path); + launching_from_disc_archive = true; + game_path = iso_device::virtual_device_name + "/PS3_GAME/./"; + } + if (game_path.ends_with("/./")) { // Marked as PS3_GAME directory @@ -1967,21 +1980,28 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } // TODO: Verify timestamps and error codes with sys_fs - vfs::mount("/dev_bdvd", bdvd_dir); - vfs::mount("/dev_bdvd/PS3_GAME", inherited_ps3_game_path.empty() ? hdd0_game + m_path.substr(hdd0_game.size(), 10) : inherited_ps3_game_path); const std::string new_ps3_game = vfs::get("/dev_bdvd/PS3_GAME"); sys_log.notice("Game: %s", new_ps3_game); - // Store /dev_bdvd/PS3_GAME location - if (games_config::result res = m_games_config.add_game(m_title_id, new_ps3_game + "/./"); res == games_config::result::success) + if (!new_ps3_game.starts_with(iso_device::virtual_device_name)) { - sys_log.notice("Registered BDVD/PS3_GAME game directory for title '%s': %s", m_title_id, new_ps3_game); + // Store /dev_bdvd/PS3_GAME location + if (games_config::result res = m_games_config.add_game(m_title_id, new_ps3_game + "/./"); res == games_config::result::success) + { + sys_log.notice("Registered BDVD/PS3_GAME game directory for title '%s': %s", m_title_id, new_ps3_game); + } + else if (res == games_config::result::failure) + { + sys_log.error("Failed to save BDVD/PS3_GAME location of title '%s' (error=%s)", m_title_id, fs::g_tls_error); + } + + vfs::mount("/dev_bdvd", bdvd_dir); } - else if (res == games_config::result::failure) + else { - sys_log.error("Failed to save BDVD/PS3_GAME location of title '%s' (error=%s)", m_title_id, fs::g_tls_error); + vfs::mount("/dev_bdvd", iso_device::virtual_device_name + "/"); } } else if (disc.empty()) @@ -2229,7 +2249,8 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, // Check game updates if (const std::string hdd0_boot = hdd0_game + m_title_id + "/USRDIR/EBOOT.BIN"; !m_ar && recursion_count == 0 && disc.empty() && !bdvd_dir.empty() && !m_title_id.empty() - && resolved_path == GetCallbacks().resolve_path(vfs::get("/dev_bdvd/PS3_GAME/USRDIR/EBOOT.BIN")) + && (resolved_path == GetCallbacks().resolve_path(vfs::get("/dev_bdvd/PS3_GAME/USRDIR/EBOOT.BIN")) + || launching_from_disc_archive) && resolved_path != GetCallbacks().resolve_path(hdd0_boot) && fs::is_file(hdd0_boot) && ppu_exec == elf_error::ok) { From f4dffd985b95c37aff5994110ddfce42fb0d0894 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:51:14 +0000 Subject: [PATCH 25/47] ISO: Fix argv[0] being set to a garbage value - Due to GetCallbacks().resolve_path behaviour being reverted in a prior commit, resolved_path will not have the path to the game executable. This causes issues when setting argv[0] to the game executable path and apparently breaks some games which may rely on this value. --- rpcs3/Emu/System.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 4f6a4eb680..bff86636e7 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -2361,8 +2361,9 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, else if (!bdvd_dir.empty() && fs::is_dir(bdvd_dir)) { // Disc games are on /dev_bdvd/ - const usz pos = resolved_path.rfind(m_game_dir); - argv[0] = "/dev_bdvd/PS3_GAME/" + unescape(resolved_path.substr(pos + m_game_dir.size() + 1)); + std::string disc_path = !launching_from_disc_archive ? resolved_path : m_path; + const usz pos = disc_path.rfind(m_game_dir); + argv[0] = "/dev_bdvd/PS3_GAME/" + unescape(disc_path.substr(pos + m_game_dir.size() + 1)); m_dir = "/dev_bdvd/PS3_GAME/"; } else if (from_dev_flash) From a53f2fc7b96b29348cbd6183a26df3a0a0eec672 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Thu, 8 Jan 2026 01:07:40 +0000 Subject: [PATCH 26/47] ISO: Fix save states in games with multiple executables - Some games will load into a different executable from EBOOT.BIN, and attempting to restore those games from savestate causes the game to crash immediately. - This commit repurposes the disc_info field in a savestate generated from an ISO game to store the running game executable at the time of the save state. --- rpcs3/Emu/System.cpp | 33 ++++++++++++++++++++------------- rpcs3/Loader/ISO.cpp | 3 +++ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index bff86636e7..c8e8630219 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -1172,12 +1172,14 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, std::string disc_info; m_ar->serialize(argv.emplace_back(), disc_info, klic.emplace_back(), m_game_dir, hdd1); + launching_from_disc_archive = is_file_iso(disc_info); + if (!klic[0]) { klic.clear(); } - if (!disc_info.empty() && disc_info[0] != '/') + if (!launching_from_disc_archive && !disc_info.empty() && disc_info[0] != '/') { // Restore disc path for disc games (must exist in games.yml i.e. your game library) m_title_id = disc_info; @@ -1294,6 +1296,13 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, m_path_old = m_path; resolve_path_as_vfs_path = true; + if (launching_from_disc_archive) + { + load_iso(disc_info); + m_path = iso_device::virtual_device_name + "/" + argv[0]; + + resolve_path_as_vfs_path = false; + } } else if (m_path.starts_with(vfs_boot_prefix)) { @@ -1414,10 +1423,6 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, m_path = rpcs3::utils::get_hdd1_dir(); m_path += std::string_view(argv[0]).substr(9); } - else if (is_file_iso(argv[0])) - { - m_path = argv[0]; - } else { sys_log.error("Unknown source for path redirection: %s", argv[0]); @@ -1433,13 +1438,13 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } const std::string resolved_path = GetCallbacks().resolve_path(m_path); - if (is_file_iso(m_path)) + if (!launching_from_disc_archive && is_file_iso(m_path)) { load_iso(m_path); - std::string path = iso_device::virtual_device_name + "/"; + launching_from_disc_archive = true; - vfs::mount("/dev_bdvd/"sv, path); + std::string path = iso_device::virtual_device_name + "/"; // ISOs that are install discs will error if set to EBOOT.BIN // so this should cover both of them @@ -1449,11 +1454,12 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } m_path = path; + } + if (launching_from_disc_archive) + { m_dir = "/dev_bdvd/PS3_GAME/"; m_cat = "DG"sv; - - launching_from_disc_archive = true; } const std::string elf_dir = fs::get_parent_dir(m_path); @@ -1630,7 +1636,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } // ISO PKG INSTALL HACK! - if (!m_path.ends_with("EBOOT.BIN") && launching_from_disc_archive) + if (fs::is_dir(m_path) && launching_from_disc_archive) { bdvd_dir = m_path; } @@ -2136,7 +2142,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, // ISO has no USRDIR/EBOOT.BIN, and we've examined its PKGDIR and extras. // time to wrap up - if (!m_path.ends_with("EBOOT.BIN") && launching_from_disc_archive) + if (fs::is_dir(m_path) && launching_from_disc_archive) { return game_boot_result::nothing_to_boot; } @@ -3620,8 +3626,9 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s ensure(device); auto iso_device = dynamic_cast(device.get()); + + ar(m_path.substr(iso_device::virtual_device_name.size() + 1)); ar(iso_device->get_loaded_iso()); - ar(m_title_id); } else if (auto dir = vfs::get("/dev_bdvd/PS3_GAME"); fs::is_dir(dir) && !fs::is_file(fs::get_parent_dir(dir) + "/PS3_DISC.SFB")) { diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index 3c3a0a8c20..7c3cb2dd07 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "ISO.h" +#include "Emu/VFS.h" #include #include @@ -619,6 +620,8 @@ void load_iso(const std::string& path) { fs::set_virtual_device("iso_overlay_fs_dev", stx::make_shared(path)); + + vfs::mount("/dev_bdvd/"sv, iso_device::virtual_device_name + "/"); } void unload_iso() From 49bcc930727ced0ea935155f8ef8bf4a4ce4e6e1 Mon Sep 17 00:00:00 2001 From: Functionable <40835042+Functionable@users.noreply.github.com> Date: Fri, 9 Jan 2026 01:28:36 +0000 Subject: [PATCH 27/47] ISO: Move load_icon --- rpcs3/rpcs3qt/savestate_manager_dialog.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rpcs3/rpcs3qt/savestate_manager_dialog.cpp b/rpcs3/rpcs3qt/savestate_manager_dialog.cpp index 9e871800e2..7a341ff0af 100644 --- a/rpcs3/rpcs3qt/savestate_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/savestate_manager_dialog.cpp @@ -434,6 +434,11 @@ void savestate_manager_dialog::ResizeGameIcons() { if (!item->data(GameUserRole::GamePixmapLoaded).toBool()) { + if (!archive_path.empty()) + { + gui::utils::load_icon(icon, icon_path, archive_path); + } + // Load game icon if (!icon.load(QString::fromStdString(icon_path))) { @@ -448,11 +453,6 @@ void savestate_manager_dialog::ResizeGameIcons() } } - if (!archive_path.empty()) - { - gui::utils::load_icon(icon, icon_path, archive_path); - } - if (cancel && cancel->load()) { return; From be77083a2b9f734f316312f9a406228b5152951c Mon Sep 17 00:00:00 2001 From: Megamouse Date: Fri, 9 Jan 2026 18:56:27 +0100 Subject: [PATCH 28/47] Qt: some cleanup --- rpcs3/rpcs3qt/syntax_highlighter.cpp | 12 ++++++------ rpcs3/rpcs3qt/syntax_highlighter.h | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/rpcs3/rpcs3qt/syntax_highlighter.cpp b/rpcs3/rpcs3qt/syntax_highlighter.cpp index 412271eb6a..33d49e678d 100644 --- a/rpcs3/rpcs3qt/syntax_highlighter.cpp +++ b/rpcs3/rpcs3qt/syntax_highlighter.cpp @@ -1,11 +1,11 @@ #include "syntax_highlighter.h" #include "qt_utils.h" -Highlighter::Highlighter(QTextDocument *parent) : QSyntaxHighlighter(parent) +Highlighter::Highlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { } -void Highlighter::addRule(const QString &pattern, const QBrush &brush) +void Highlighter::addRule(const QString& pattern, const QBrush& brush) { HighlightingRule rule; rule.pattern = QRegularExpression(pattern); @@ -13,14 +13,14 @@ void Highlighter::addRule(const QString &pattern, const QBrush &brush) highlightingRules.append(rule); } -void Highlighter::highlightBlock(const QString &text) +void Highlighter::highlightBlock(const QString& text) { for (const HighlightingRule& rule : highlightingRules) { QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text); while (matchIterator.hasNext()) { - QRegularExpressionMatch match = matchIterator.next(); + const QRegularExpressionMatch match = matchIterator.next(); setFormat(match.capturedStart(), match.capturedLength(), rule.format); } } @@ -67,7 +67,7 @@ LogHighlighter::LogHighlighter(QTextDocument* parent) : Highlighter(parent) addRule("^·T.*$", gui::utils::get_label_color("log_level_trace", color, color)); } -AsmHighlighter::AsmHighlighter(QTextDocument *parent) : Highlighter(parent) +AsmHighlighter::AsmHighlighter(QTextDocument* parent) : Highlighter(parent) { addRule("^\\b[A-Z0-9]+\\b", Qt::darkBlue); // Instructions addRule("-?R\\d[^,;\\s]*", Qt::darkRed); // -R0.* @@ -78,7 +78,7 @@ AsmHighlighter::AsmHighlighter(QTextDocument *parent) : Highlighter(parent) addRule("#[^\\n]*", Qt::darkGreen); // Single line comment } -GlslHighlighter::GlslHighlighter(QTextDocument *parent) : Highlighter(parent) +GlslHighlighter::GlslHighlighter(QTextDocument* parent) : Highlighter(parent) { const QStringList keywordPatterns = QStringList() // Selection-Iteration-Jump Statements: diff --git a/rpcs3/rpcs3qt/syntax_highlighter.h b/rpcs3/rpcs3qt/syntax_highlighter.h index 3e701ffb0e..8179cd2257 100644 --- a/rpcs3/rpcs3qt/syntax_highlighter.h +++ b/rpcs3/rpcs3qt/syntax_highlighter.h @@ -10,11 +10,11 @@ class Highlighter : public QSyntaxHighlighter Q_OBJECT public: - explicit Highlighter(QTextDocument *parent = nullptr); + explicit Highlighter(QTextDocument* parent = nullptr); protected: - void highlightBlock(const QString &text) override; - void addRule(const QString &pattern, const QBrush &brush); + void highlightBlock(const QString& text) override; + void addRule(const QString& pattern, const QBrush& brush); struct HighlightingRule { @@ -42,7 +42,7 @@ class AsmHighlighter : public Highlighter Q_OBJECT public: - explicit AsmHighlighter(QTextDocument *parent = nullptr); + explicit AsmHighlighter(QTextDocument* parent = nullptr); }; class GlslHighlighter : public Highlighter @@ -50,5 +50,5 @@ class GlslHighlighter : public Highlighter Q_OBJECT public: - explicit GlslHighlighter(QTextDocument *parent = nullptr); + explicit GlslHighlighter(QTextDocument* parent = nullptr); }; From 19129eddd770ac7083630990bd925469f44722cc Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 16 Dec 2025 21:08:08 +0100 Subject: [PATCH 29/47] Qt: ISO clean up Should fix some potential Qt bugs as well --- rpcs3/Emu/System.cpp | 23 ++-- rpcs3/Emu/games_config.cpp | 6 +- rpcs3/Loader/ISO.cpp | 147 ++++++++++----------- rpcs3/Loader/ISO.h | 80 +++++------ rpcs3/rpcs3qt/game_list_base.cpp | 9 +- rpcs3/rpcs3qt/game_list_frame.cpp | 7 +- rpcs3/rpcs3qt/game_list_grid.cpp | 7 + rpcs3/rpcs3qt/game_list_table.cpp | 13 +- rpcs3/rpcs3qt/main_window.cpp | 6 +- rpcs3/rpcs3qt/qt_utils.cpp | 21 +-- rpcs3/rpcs3qt/qt_video_source.cpp | 47 ++++--- rpcs3/rpcs3qt/qt_video_source.h | 4 +- rpcs3/rpcs3qt/savestate_manager_dialog.cpp | 13 +- rpcs3/rpcs3qt/trophy_manager_dialog.cpp | 4 +- 14 files changed, 191 insertions(+), 196 deletions(-) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index c8e8630219..6db05a21ea 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -1296,6 +1296,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, m_path_old = m_path; resolve_path_as_vfs_path = true; + if (launching_from_disc_archive) { load_iso(disc_info); @@ -1636,13 +1637,13 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } // ISO PKG INSTALL HACK! - if (fs::is_dir(m_path) && launching_from_disc_archive) + if (launching_from_disc_archive && fs::is_dir(m_path)) { bdvd_dir = m_path; } // Special boot mode (directory scan) - if (fs::is_dir(m_path) && !launching_from_disc_archive) + if (!launching_from_disc_archive && fs::is_dir(m_path)) { m_state = system_state::ready; GetCallbacks().on_ready(); @@ -2142,7 +2143,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, // ISO has no USRDIR/EBOOT.BIN, and we've examined its PKGDIR and extras. // time to wrap up - if (fs::is_dir(m_path) && launching_from_disc_archive) + if (launching_from_disc_archive && fs::is_dir(m_path)) { return game_boot_result::nothing_to_boot; } @@ -2255,8 +2256,8 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, // Check game updates if (const std::string hdd0_boot = hdd0_game + m_title_id + "/USRDIR/EBOOT.BIN"; !m_ar && recursion_count == 0 && disc.empty() && !bdvd_dir.empty() && !m_title_id.empty() - && (resolved_path == GetCallbacks().resolve_path(vfs::get("/dev_bdvd/PS3_GAME/USRDIR/EBOOT.BIN")) - || launching_from_disc_archive) + && (launching_from_disc_archive || + resolved_path == GetCallbacks().resolve_path(vfs::get("/dev_bdvd/PS3_GAME/USRDIR/EBOOT.BIN"))) && resolved_path != GetCallbacks().resolve_path(hdd0_boot) && fs::is_file(hdd0_boot) && ppu_exec == elf_error::ok) { @@ -2367,7 +2368,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, else if (!bdvd_dir.empty() && fs::is_dir(bdvd_dir)) { // Disc games are on /dev_bdvd/ - std::string disc_path = !launching_from_disc_archive ? resolved_path : m_path; + const std::string& disc_path = !launching_from_disc_archive ? resolved_path : m_path; const usz pos = disc_path.rfind(m_game_dir); argv[0] = "/dev_bdvd/PS3_GAME/" + unescape(disc_path.substr(pos + m_game_dir.size() + 1)); m_dir = "/dev_bdvd/PS3_GAME/"; @@ -3622,13 +3623,13 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s // Game mounted from archive if (m_path.starts_with(iso_device::virtual_device_name + "/")) { - auto device = fs::get_virtual_device(iso_device::virtual_device_name + "/"); + const auto device = fs::get_virtual_device(iso_device::virtual_device_name + "/"); ensure(device); - auto iso_device = dynamic_cast(device.get()); + const auto iso_dev = dynamic_cast(device.get()); ar(m_path.substr(iso_device::virtual_device_name.size() + 1)); - ar(iso_device->get_loaded_iso()); + ar(iso_dev->get_loaded_iso()); } else if (auto dir = vfs::get("/dev_bdvd/PS3_GAME"); fs::is_dir(dir) && !fs::is_file(fs::get_parent_dir(dir) + "/PS3_DISC.SFB")) { @@ -4245,9 +4246,9 @@ game_boot_result Emulator::AddGameToYml(const std::string& path) // Load PARAM.SFO const std::string elf_dir = fs::get_parent_dir(path); - std::string sfo_dir = !archive ? rpcs3::utils::get_sfo_dir_from_game_path(fs::get_parent_dir(elf_dir)) : "PS3_GAME"; + std::string sfo_dir = archive ? "PS3_GAME" : rpcs3::utils::get_sfo_dir_from_game_path(fs::get_parent_dir(elf_dir)); const std::string sfo_path = sfo_dir + "/PARAM.SFO"; - const psf::registry _psf = !archive ? psf::load_object(sfo_path) : archive->open_psf(sfo_path); + const psf::registry _psf = archive ? archive->open_psf(sfo_path) : psf::load_object(sfo_path); const std::string title_id = std::string(psf::get_string(_psf, "TITLE_ID")); const std::string cat = std::string(psf::get_string(_psf, "CATEGORY")); diff --git a/rpcs3/Emu/games_config.cpp b/rpcs3/Emu/games_config.cpp index ad6daff193..ae0fad8084 100644 --- a/rpcs3/Emu/games_config.cpp +++ b/rpcs3/Emu/games_config.cpp @@ -48,11 +48,11 @@ games_config::result games_config::add_game(const std::string& key, const std::s { if (path == iso_device::virtual_device_name + "/") { - auto device = fs::get_virtual_device(iso_device::virtual_device_name + "/"); + const auto device = fs::get_virtual_device(iso_device::virtual_device_name + "/"); if (!device) return result::failure; - auto iso_device = dynamic_cast(device.get()); - return add_game(key, iso_device->get_loaded_iso()); + const auto iso_dev = dynamic_cast(device.get()); + return add_game(key, iso_dev->get_loaded_iso()); } std::lock_guard lock(m_mutex); diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index 7c3cb2dd07..0f9772cd8f 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -11,6 +11,7 @@ bool is_file_iso(const std::string& path) { + if (path.empty()) return false; if (fs::is_dir(path)) return false; return is_file_iso(fs::file(path)); @@ -55,14 +56,14 @@ inline T read_both_endian_int(fs::file& file) // assumed that directory_entry is at file head std::optional iso_read_directory_entry(fs::file& file, bool names_in_ucs2 = false) { - auto start_pos = file.pos(); - u8 entry_length = file.read(); + const auto start_pos = file.pos(); + const u8 entry_length = file.read(); if (entry_length == 0) return std::nullopt; file.seek(1, fs::seek_cur); - u32 start_sector = read_both_endian_int(file); - u32 file_size = read_both_endian_int(file); + const u32 start_sector = read_both_endian_int(file); + const u32 file_size = read_both_endian_int(file); std::tm file_date = {}; file_date.tm_year = file.read(); @@ -71,20 +72,20 @@ std::optional iso_read_directory_entry(fs::file& file, bool nam file_date.tm_hour = file.read(); file_date.tm_min = file.read(); file_date.tm_sec = file.read(); - s16 timezone_value = file.read(); - s16 timezone_offset = (timezone_value - 50) * 15 * 60; + const s16 timezone_value = file.read(); + const s16 timezone_offset = (timezone_value - 50) * 15 * 60; - std::time_t date_time = std::mktime(&file_date) + timezone_offset; + const std::time_t date_time = std::mktime(&file_date) + timezone_offset; - u8 flags = file.read(); + const u8 flags = file.read(); // 2nd flag bit indicates whether a given fs node is a directory - bool is_directory = flags & 0b00000010; - bool has_more_extents = flags & 0b10000000; + const bool is_directory = flags & 0b00000010; + const bool has_more_extents = flags & 0b10000000; file.seek(6, fs::seek_cur); - u8 file_name_length = file.read(); + const u8 file_name_length = file.read(); std::string file_name; file.read(file_name, file_name_length); @@ -99,10 +100,10 @@ std::optional iso_read_directory_entry(fs::file& file, bool nam } else if (names_in_ucs2) // for strings in joliet descriptor { - std::string new_file_name = ""; + std::string new_file_name; int read = 0; const u8* raw_str = reinterpret_cast(file_name.c_str()); - while(read < file_name_length) + while (read < file_name_length) { // characters are stored in big endian format. const u16 upper = raw_str[read]; @@ -134,7 +135,7 @@ std::optional iso_read_directory_entry(fs::file& file, bool nam return iso_fs_metadata { - .name = file_name, + .name = std::move(file_name), .time = date_time, .is_directory = is_directory, .has_multiple_extents = has_more_extents, @@ -149,26 +150,25 @@ std::optional iso_read_directory_entry(fs::file& file, bool nam }; } -void iso_form_hierarchy(fs::file& file, iso_fs_node& node, - bool use_ucs2_decoding = false, std::string parent_path = "") +void iso_form_hierarchy(fs::file& file, iso_fs_node& node, bool use_ucs2_decoding = false, const std::string& parent_path = "") { if (!node.metadata.is_directory) return; - std::vector multi_extent_node_indices; + std::vector multi_extent_node_indices; // assuming the directory spans a single extent const auto& directory_extent = node.metadata.extents[0]; file.seek(directory_extent.start * ISO_BLOCK_SIZE); - u64 end_pos = directory_extent.size + (directory_extent.start * ISO_BLOCK_SIZE); + const u64 end_pos = directory_extent.size + (directory_extent.start * ISO_BLOCK_SIZE); while(file.pos() < end_pos) { auto entry = iso_read_directory_entry(file, use_ucs2_decoding); if (!entry) { - u64 new_sector = (file.pos() / ISO_BLOCK_SIZE) + 1; + const u64 new_sector = (file.pos() / ISO_BLOCK_SIZE) + 1; file.seek(new_sector * ISO_BLOCK_SIZE); continue; } @@ -176,10 +176,10 @@ void iso_form_hierarchy(fs::file& file, iso_fs_node& node, bool extent_added = false; // find previous extent and merge into it, otherwise we push this node's index - for (int index : multi_extent_node_indices) + for (usz index : multi_extent_node_indices) { - auto& selected_node = node.children.at(index); - if (selected_node->metadata.name.compare(entry->name) == 0) + auto& selected_node = ::at32(node.children, index); + if (selected_node->metadata.name == entry->name) { // merge into selected_node selected_node->metadata.extents.push_back(entry->extents[0]); @@ -197,7 +197,7 @@ void iso_form_hierarchy(fs::file& file, iso_fs_node& node, } node.children.push_back(std::make_unique(iso_fs_node{ - .metadata = *entry + .metadata = std::move(*entry) })); } @@ -237,7 +237,7 @@ iso_archive::iso_archive(const std::string& path) do { - auto descriptor_start = m_file.pos(); + const auto descriptor_start = m_file.pos(); descriptor_type = m_file.read(); @@ -257,7 +257,7 @@ iso_archive::iso_archive(const std::string& path) m_file.seek(descriptor_start + ISO_BLOCK_SIZE); } - while(descriptor_type != 255); + while (descriptor_type != 255); iso_form_hierarchy(m_file, m_root, use_ucs2_decoding); } @@ -266,7 +266,7 @@ iso_fs_node* iso_archive::retrieve(const std::string& passed_path) { if (passed_path.empty()) return nullptr; - std::string path = std::filesystem::path(passed_path).string(); + const std::string path = std::filesystem::path(passed_path).string(); size_t start = 0; size_t end = path.find_first_of(fs::delim); @@ -277,14 +277,14 @@ iso_fs_node* iso_archive::retrieve(const std::string& passed_path) do { if (search_stack.empty()) return nullptr; - auto* top_entry = search_stack.top(); + const auto* top_entry = search_stack.top(); - if (end == std::string::npos) + if (end == umax) { end = path.size(); } - auto path_component = path.substr(start, end-start); + const auto path_component = path.substr(start, end-start); bool found = false; @@ -301,7 +301,7 @@ iso_fs_node* iso_archive::retrieve(const std::string& passed_path) { for (const auto& entry : top_entry->children) { - if (entry->metadata.name.compare(path_component) == 0) + if (entry->metadata.name == path_component) { search_stack.push(entry.get()); @@ -316,7 +316,7 @@ iso_fs_node* iso_archive::retrieve(const std::string& passed_path) start = end + 1; end = path.find_first_of(fs::delim, start); } - while(start < path.size()); + while (start < path.size()); if (search_stack.empty()) return nullptr; @@ -330,7 +330,7 @@ bool iso_archive::exists(const std::string& path) bool iso_archive::is_file(const std::string& path) { - auto file_node = retrieve(path); + const auto file_node = retrieve(path); if (!file_node) return false; return !file_node->metadata.is_directory; @@ -338,7 +338,7 @@ bool iso_archive::is_file(const std::string& path) iso_file iso_archive::open(const std::string& path) { - return iso_file(fs::file(m_path), *retrieve(path)); + return iso_file(fs::file(m_path), *ensure(retrieve(path))); } psf::registry iso_archive::open_psf(const std::string& path) @@ -355,7 +355,7 @@ psf::registry iso_archive::open_psf(const std::string& path) } iso_file::iso_file(fs::file&& iso_handle, const iso_fs_node& node) - : m_file(std::move(iso_handle)), m_meta(node.metadata), m_pos(0) + : m_file(std::move(iso_handle)), m_meta(node.metadata) { m_file.seek(ISO_BLOCK_SIZE * node.metadata.extents[0].start); } @@ -374,7 +374,7 @@ fs::stat_t iso_file::get_stat() }; } -bool iso_file::trunc(u64) +bool iso_file::trunc(u64 /*length*/) { fs::g_tls_error = fs::error::readonly; return false; @@ -382,9 +382,11 @@ bool iso_file::trunc(u64) std::pair iso_file::get_extent_pos(u64 pos) const { + ensure(!m_meta.extents.empty()); + auto it = m_meta.extents.begin(); - while(pos >= it->size && it != m_meta.extents.end() - 1) + while (pos >= it->size && it != m_meta.extents.end() - 1) { pos -= it->size; @@ -397,14 +399,14 @@ std::pair iso_file::get_extent_pos(u64 pos) const // assumed valid and in bounds. u64 iso_file::file_offset(u64 pos) const { - auto [local_pos, extent] = get_extent_pos(pos); + const auto [local_pos, extent] = get_extent_pos(pos); return (extent.start * ISO_BLOCK_SIZE) + local_pos; } u64 iso_file::local_extent_remaining(u64 pos) const { - auto [local_pos, extent] = get_extent_pos(pos); + const auto [local_pos, extent] = get_extent_pos(pos); return extent.size - local_pos; } @@ -416,25 +418,22 @@ u64 iso_file::local_extent_size(u64 pos) const u64 iso_file::read(void* buffer, u64 size) { - auto r = read_at(m_pos, buffer, size); + const auto r = read_at(m_pos, buffer, size); m_pos += r; return r; } u64 iso_file::read_at(u64 offset, void* buffer, u64 size) { - u64 local_remaining = local_extent_remaining(offset); + const u64 local_remaining = local_extent_remaining(offset); - u64 total_read = m_file.read_at(file_offset(offset), buffer, std::min(size, local_remaining)); + const u64 total_read = m_file.read_at(file_offset(offset), buffer, std::min(size, local_remaining)); - auto total_size = this->size(); + const auto total_size = this->size(); if (size > total_read && (offset + total_read) < total_size) { - u64 second_total_read = read_at(offset + total_read, - reinterpret_cast(buffer) + total_read, - size - total_read - ); + const u64 second_total_read = read_at(offset + total_read, reinterpret_cast(buffer) + total_read, size - total_read); return total_read + second_total_read; } @@ -442,7 +441,7 @@ u64 iso_file::read_at(u64 offset, void* buffer, u64 size) return total_read; } -u64 iso_file::write(const void*, u64) +u64 iso_file::write(const void* /*buffer*/, u64 /*size*/) { fs::g_tls_error = fs::error::readonly; return 0; @@ -462,10 +461,8 @@ u64 iso_file::seek(s64 offset, fs::seek_mode whence) return -1; } - const u64 bad_res = -1; - - u64 result = m_file.seek(file_offset(m_pos)); - if (result == bad_res) return -1; + const u64 result = m_file.seek(file_offset(m_pos)); + if (result == umax) return umax; m_pos = new_pos; return m_pos; @@ -491,8 +488,7 @@ bool iso_dir::read(fs::dir_entry& entry) { if (m_pos < m_node.children.size()) { - auto& selected = m_node.children[m_pos].get()->metadata; - u64 size = selected.size(); + const auto& selected = m_node.children[m_pos].get()->metadata; entry.name = selected.name; entry.atime = selected.time; @@ -501,7 +497,7 @@ bool iso_dir::read(fs::dir_entry& entry) entry.is_directory = selected.is_directory; entry.is_symlink = false; entry.is_writable = false; - entry.size = size; + entry.size = selected.size(); m_pos++; @@ -513,25 +509,23 @@ bool iso_dir::read(fs::dir_entry& entry) bool iso_device::stat(const std::string& path, fs::stat_t& info) { - auto relative_path = std::filesystem::relative(std::filesystem::path(path), - std::filesystem::path(fs_prefix)).string(); + const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string(); - auto node = m_archive.retrieve(relative_path); + const auto node = m_archive.retrieve(relative_path); if (!node) { fs::g_tls_error = fs::error::noent; return false; } - auto& meta = node->metadata; - u64 size = meta.size(); + const auto& meta = node->metadata; info = fs::stat_t { .is_directory = meta.is_directory, .is_symlink = false, .is_writable = false, - .size = size, + .size = meta.size(), .atime = meta.time, .mtime = meta.time, .ctime = meta.time @@ -542,25 +536,24 @@ bool iso_device::stat(const std::string& path, fs::stat_t& info) bool iso_device::statfs(const std::string& path, fs::device_stat& info) { - auto relative_path = std::filesystem::relative(std::filesystem::path(path), - std::filesystem::path(fs_prefix)).string(); + const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string(); - auto node = m_archive.retrieve(relative_path); + const auto node = m_archive.retrieve(relative_path); if (!node) { fs::g_tls_error = fs::error::noent; return false; } - auto& meta = node->metadata; - u64 size = meta.size(); + const auto& meta = node->metadata; + const u64 size = meta.size(); info = fs::device_stat { - .block_size=size, - .total_size=size, - .total_free=0, - .avail_free=0 + .block_size = size, + .total_size = size, + .total_free = 0, + .avail_free = 0 }; return false; @@ -568,10 +561,9 @@ bool iso_device::statfs(const std::string& path, fs::device_stat& info) std::unique_ptr iso_device::open(const std::string& path, bs_t mode) { - auto relative_path = std::filesystem::relative(std::filesystem::path(path), - std::filesystem::path(fs_prefix)).string(); + const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string(); - auto node = m_archive.retrieve(relative_path); + const auto node = m_archive.retrieve(relative_path); if (!node) { fs::g_tls_error = fs::error::noent; @@ -589,10 +581,9 @@ std::unique_ptr iso_device::open(const std::string& path, bs_t iso_device::open_dir(const std::string& path) { - auto relative_path = std::filesystem::relative(std::filesystem::path(path), - std::filesystem::path(fs_prefix)).string(); + const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string(); - auto node = m_archive.retrieve(relative_path); + const auto node = m_archive.retrieve(relative_path); if (!node) { fs::g_tls_error = fs::error::noent; @@ -618,14 +609,12 @@ void iso_dir::rewind() void load_iso(const std::string& path) { - fs::set_virtual_device("iso_overlay_fs_dev", - stx::make_shared(path)); + fs::set_virtual_device("iso_overlay_fs_dev", stx::make_shared(path)); vfs::mount("/dev_bdvd/"sv, iso_device::virtual_device_name + "/"); } void unload_iso() { - fs::set_virtual_device("iso_overlay_fs_dev", - stx::shared_ptr()); + fs::set_virtual_device("iso_overlay_fs_dev", stx::shared_ptr()); } diff --git a/rpcs3/Loader/ISO.h b/rpcs3/Loader/ISO.h index 55c8b99f16..3af4732aad 100644 --- a/rpcs3/Loader/ISO.h +++ b/rpcs3/Loader/ISO.h @@ -36,82 +36,86 @@ struct iso_fs_node class iso_file : public fs::file_base { +private: fs::file m_file; iso_fs_metadata m_meta; - u64 m_pos; + u64 m_pos = 0; std::pair get_extent_pos(u64 pos) const; u64 file_offset(u64 pos) const; u64 local_extent_remaining(u64 pos) const; u64 local_extent_size(u64 pos) const; - public: - iso_file(fs::file&& iso_handle, const iso_fs_node& node); +public: + iso_file(fs::file&& iso_handle, const iso_fs_node& node); - fs::stat_t get_stat() override; - bool trunc(u64 length) override; - u64 read(void* buffer, u64 size) override; - u64 read_at(u64 offset, void* buffer, u64 size) override; - u64 write(const void* buffer, u64 size) override; - u64 seek(s64 offset, fs::seek_mode whence) override; - u64 size() override; + fs::stat_t get_stat() override; + bool trunc(u64 length) override; + u64 read(void* buffer, u64 size) override; + u64 read_at(u64 offset, void* buffer, u64 size) override; + u64 write(const void* buffer, u64 size) override; + u64 seek(s64 offset, fs::seek_mode whence) override; + u64 size() override; - void release() override; + void release() override; }; class iso_dir : public fs::dir_base { +private: const iso_fs_node& m_node; - u64 m_pos; + u64 m_pos = 0; - public: - iso_dir(const iso_fs_node& node) - : m_node(node), m_pos(0) - {} +public: + iso_dir(const iso_fs_node& node) + : m_node(node) + {} - bool read(fs::dir_entry&) override; - void rewind() override; + bool read(fs::dir_entry&) override; + void rewind() override; }; // represents the .iso file itself. class iso_archive { +private: std::string m_path; iso_fs_node m_root; fs::file m_file; - public: - iso_archive(const std::string& path); +public: + iso_archive(const std::string& path); - iso_fs_node* retrieve(const std::string& path); - bool exists(const std::string& path); - bool is_file(const std::string& path); + iso_fs_node* retrieve(const std::string& path); + bool exists(const std::string& path); + bool is_file(const std::string& path); - iso_file open(const std::string& path); + iso_file open(const std::string& path); - psf::registry open_psf(const std::string& path); + psf::registry open_psf(const std::string& path); }; class iso_device : public fs::device_base { +private: iso_archive m_archive; std::string iso_path; - public: - inline static std::string virtual_device_name = "/vfsv0_virtual_iso_overlay_fs_dev"; +public: + inline static std::string virtual_device_name = "/vfsv0_virtual_iso_overlay_fs_dev"; - iso_device(const std::string& iso_path, const std::string& device_name = virtual_device_name) - : m_archive(iso_path), iso_path(iso_path) - { - fs_prefix = device_name; - } - ~iso_device() override = default; + iso_device(const std::string& iso_path, const std::string& device_name = virtual_device_name) + : m_archive(iso_path), iso_path(iso_path) + { + fs_prefix = device_name; + } + ~iso_device() override = default; - const std::string& get_loaded_iso() const { return iso_path; } + const std::string& get_loaded_iso() const { return iso_path; } - bool stat(const std::string& path, fs::stat_t& info) override; - bool statfs(const std::string& path, fs::device_stat& info) override; + bool stat(const std::string& path, fs::stat_t& info) override; + bool statfs(const std::string& path, fs::device_stat& info) override; - std::unique_ptr open(const std::string& path, bs_t mode) override; - std::unique_ptr open_dir(const std::string& path) override; + std::unique_ptr open(const std::string& path, bs_t mode) override; + std::unique_ptr open_dir(const std::string& path) override; }; diff --git a/rpcs3/rpcs3qt/game_list_base.cpp b/rpcs3/rpcs3qt/game_list_base.cpp index 4105d754b0..edae4e51f6 100644 --- a/rpcs3/rpcs3qt/game_list_base.cpp +++ b/rpcs3/rpcs3qt/game_list_base.cpp @@ -49,14 +49,9 @@ void game_list_base::IconLoadFunction(game_info game, qreal device_pixel_ratio, static std::unordered_set warn_once_list; static shared_mutex s_mtx; - if (game->icon.isNull() && (game->info.icon_path.empty() || !game->icon.load(QString::fromStdString(game->info.icon_path)))) + if (game->icon.isNull() && !gui::utils::load_icon(game->icon, game->info.icon_path, game->icon_in_archive ? game->info.path : "")) { - if (game->icon_in_archive) - { - game->icon_in_archive = gui::utils::load_icon(game->icon, game->info.icon_path, game->info.path); - } - - if (!game->icon_in_archive && game_list_log.warning) + if (game_list_log.warning) { bool logged = false; { diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 4a695c9828..ea7b87ff6d 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -531,8 +531,7 @@ void game_list_frame::OnParsingFinished() const auto file_exists = [&archive](const std::string& path) { - if (!archive) return fs::is_file(path); - return archive->is_file(path); + return archive ? archive->is_file(path) : fs::is_file(path); }; gui_game_info game{}; @@ -540,10 +539,10 @@ void game_list_frame::OnParsingFinished() const Localized thread_localized; - const std::string sfo_dir = !archive ? rpcs3::utils::get_sfo_dir_from_game_path(dir_or_elf) : "PS3_GAME"; + const std::string sfo_dir = archive ? "PS3_GAME" : rpcs3::utils::get_sfo_dir_from_game_path(dir_or_elf); const std::string sfo_path = sfo_dir + "/PARAM.SFO"; - const psf::registry psf = !archive ? psf::load_object(sfo_path) : archive->open_psf(sfo_path); + const psf::registry psf = archive ? archive->open_psf(sfo_path) : psf::load_object(sfo_path); const std::string_view title_id = psf::get_string(psf, "TITLE_ID", ""); if (title_id.empty()) diff --git a/rpcs3/rpcs3qt/game_list_grid.cpp b/rpcs3/rpcs3qt/game_list_grid.cpp index a20e67da73..a33755ff54 100644 --- a/rpcs3/rpcs3qt/game_list_grid.cpp +++ b/rpcs3/rpcs3qt/game_list_grid.cpp @@ -4,6 +4,8 @@ #include "gui_settings.h" #include "qt_utils.h" +#include "Loader/ISO.h" + #include game_list_grid::game_list_grid() @@ -110,6 +112,11 @@ void game_list_grid::populate( if (play_hover_movies && (game->has_hover_gif || game->has_hover_pam)) { item->set_video_path(game->info.movie_path); + + if (!fs::exists(game->info.movie_path) && is_file_iso(game->info.path)) + { + item->set_iso_path(game->info.path); + } } if (selected_item_ids.contains(game->info.path + game->info.icon_path)) diff --git a/rpcs3/rpcs3qt/game_list_table.cpp b/rpcs3/rpcs3qt/game_list_table.cpp index 2ab2013b19..98b9ef344d 100644 --- a/rpcs3/rpcs3qt/game_list_table.cpp +++ b/rpcs3/rpcs3qt/game_list_table.cpp @@ -244,12 +244,6 @@ void game_list_table::populate( custom_table_widget_item* icon_item = new custom_table_widget_item; game->item = icon_item; - if (is_file_iso(game->info.path) && !game->info.movie_path.empty() - && !fs::exists(game->info.movie_path)) - { - icon_item->set_source_path(game->info.path); - } - icon_item->set_image_change_callback([this, icon_item, game](const QVideoFrame& frame) { if (!icon_item || !game) @@ -285,7 +279,7 @@ void game_list_table::populate( // Do not report size of apps inside /dev_flash (it does not make sense to do so) game->info.size_on_disk = 0; } - else if(is_file_iso(game->info.path)) + else if (is_file_iso(game->info.path)) { fs::stat_t iso_stat; fs::get_stat(game->info.path, iso_stat); @@ -308,6 +302,11 @@ void game_list_table::populate( if (play_hover_movies && (game->has_hover_gif || game->has_hover_pam)) { icon_item->set_video_path(game->info.movie_path); + + if (!fs::exists(game->info.movie_path) && is_file_iso(game->info.path)) + { + icon_item->set_iso_path(game->info.path); + } } icon_item->setData(Qt::UserRole, index, true); diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 331e25e2fc..98d3ceefb3 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -546,9 +546,9 @@ void main_window::Boot(const std::string& path, const std::string& title_id, boo std::string game_path = Emu.GetBoot(); if (game_path.starts_with(iso_device::virtual_device_name)) { - auto device = fs::get_virtual_device(iso_device::virtual_device_name + "/"); - auto iso_device = dynamic_cast(device.get()); - game_path = iso_device->get_loaded_iso(); + const auto device = fs::get_virtual_device(iso_device::virtual_device_name + "/"); + const auto iso_dev = ensure(dynamic_cast(device.get())); + game_path = iso_dev->get_loaded_iso(); } gui_log.success("Boot successful."); diff --git a/rpcs3/rpcs3qt/qt_utils.cpp b/rpcs3/rpcs3qt/qt_utils.cpp index b10799a453..0f53ee0f86 100644 --- a/rpcs3/rpcs3qt/qt_utils.cpp +++ b/rpcs3/rpcs3qt/qt_utils.cpp @@ -683,27 +683,28 @@ namespace gui return QString("%1 days ago %2").arg(current_date - exctrated_date).arg(date.toString(fmt_relative)); } - bool load_icon(QPixmap& icon, const std::string& icon_path, - const std::string& archive_path) + bool load_icon(QPixmap& icon, const std::string& icon_path, const std::string& archive_path) { + if (icon_path.empty()) return false; + + if (archive_path.empty()) + { + return icon.load(QString::fromStdString(icon_path)); + } + if (!is_file_iso(archive_path)) return false; iso_archive archive(archive_path); if (!archive.exists(icon_path)) return false; auto icon_file = archive.open(icon_path); - auto icon_size = icon_file.size(); + const auto icon_size = icon_file.size(); + if (icon_size == 0) return false; QByteArray data(icon_size, 0); icon_file.read(data.data(), icon_size); - QImage iconImage; - if (iconImage.loadFromData(data)) - { - icon = QPixmap::fromImage(iconImage); - } - - return true; + return icon.loadFromData(data); } QString format_timestamp(s64 time, const QString& fmt) diff --git a/rpcs3/rpcs3qt/qt_video_source.cpp b/rpcs3/rpcs3qt/qt_video_source.cpp index 81e6d98e55..d74d395c58 100644 --- a/rpcs3/rpcs3qt/qt_video_source.cpp +++ b/rpcs3/rpcs3qt/qt_video_source.cpp @@ -21,9 +21,9 @@ void qt_video_source::set_video_path(const std::string& video_path) m_video_path = QString::fromStdString(video_path); } -void qt_video_source::set_source_path(const std::string& source_path) +void qt_video_source::set_iso_path(const std::string& iso_path) { - m_source_path = QString::fromStdString(source_path); + m_iso_path = iso_path; } void qt_video_source::set_active(bool active) @@ -62,7 +62,7 @@ void qt_video_source::init_movie() return; } - if (!m_image_change_callback || m_video_path.isEmpty() || (!QFile::exists(m_video_path) && m_source_path.isEmpty())) + if (!m_image_change_callback || m_video_path.isEmpty() || (m_iso_path.empty() && !QFile::exists(m_video_path))) { m_video_path.clear(); return; @@ -72,24 +72,24 @@ void qt_video_source::init_movie() if (lower.endsWith(".gif")) { - if (m_source_path.isEmpty()) + if (m_iso_path.empty()) { m_movie = std::make_unique(m_video_path); - m_video_path.clear(); } else { - iso_archive archive(m_source_path.toStdString()); + iso_archive archive(m_iso_path); auto movie_file = archive.open(m_video_path.toStdString()); - auto movie_size = movie_file.size(); + const auto movie_size = movie_file.size(); + if (movie_size == 0) return; + m_video_data = QByteArray(movie_size, 0); movie_file.read(m_video_data.data(), movie_size); - QBuffer buffer(&m_video_data); - buffer.open(QIODevice::ReadOnly); - m_movie = std::make_unique(&buffer); + m_video_buffer = std::make_unique(&m_video_data); + m_video_buffer->open(QIODevice::ReadOnly); + m_movie = std::make_unique(m_video_buffer.get()); - m_video_path.clear(); } if (!m_movie->isValid()) @@ -109,7 +109,7 @@ void qt_video_source::init_movie() if (lower.endsWith(".pam")) { // We can't set PAM files as source of the video player, so we have to feed them as raw data. - if (m_source_path.isEmpty()) + if (m_iso_path.empty()) { QFile file(m_video_path); if (!file.open(QFile::OpenModeFlag::ReadOnly)) @@ -119,22 +119,26 @@ void qt_video_source::init_movie() // TODO: Decode the pam properly before pushing it to the player m_video_data = file.readAll(); - if (m_video_data.isEmpty()) - { - return; - } } else { - auto source_path = m_source_path.toStdString(); - auto video_path = m_video_path.toStdString(); - iso_archive archive(source_path.c_str()); - auto movie_file = archive.open(video_path); - auto movie_size = movie_file.size(); + iso_archive archive(m_iso_path); + auto movie_file = archive.open(m_video_path.toStdString()); + const auto movie_size = movie_file.size(); + if (movie_size == 0) + { + return; + } + m_video_data = QByteArray(movie_size, 0); movie_file.read(m_video_data.data(), movie_size); } + if (m_video_data.isEmpty()) + { + return; + } + m_video_buffer = std::make_unique(&m_video_data); m_video_buffer->open(QIODevice::ReadOnly); } @@ -185,6 +189,7 @@ void qt_video_source::stop_movie() if (m_movie) { m_movie->stop(); + m_movie.reset(); } m_video_sink.reset(); diff --git a/rpcs3/rpcs3qt/qt_video_source.h b/rpcs3/rpcs3qt/qt_video_source.h index 1f195def02..ce43d593d7 100644 --- a/rpcs3/rpcs3qt/qt_video_source.h +++ b/rpcs3/rpcs3qt/qt_video_source.h @@ -17,7 +17,7 @@ public: qt_video_source(); virtual ~qt_video_source(); - void set_source_path(const std::string& source_path); + void set_iso_path(const std::string& iso_path); void set_video_path(const std::string& video_path) override; const QString& video_path() const { return m_video_path; } @@ -44,7 +44,7 @@ protected: atomic_t m_has_new = false; QString m_video_path; - QString m_source_path; // path of the source archive + std::string m_iso_path; // path of the source archive QByteArray m_video_data{}; QImage m_image{}; std::vector m_image_path; diff --git a/rpcs3/rpcs3qt/savestate_manager_dialog.cpp b/rpcs3/rpcs3qt/savestate_manager_dialog.cpp index 7a341ff0af..2898c9b170 100644 --- a/rpcs3/rpcs3qt/savestate_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/savestate_manager_dialog.cpp @@ -418,10 +418,10 @@ void savestate_manager_dialog::ResizeGameIcons() { const qreal dpr = devicePixelRatioF(); const int savestate_index = item->data(GameUserRole::GameIndex).toInt(); - const std::string icon_path = m_savestate_db[savestate_index]->game_icon_path; - const std::string archive_path = m_savestate_db[savestate_index]->archive_path; + std::string game_icon_path = m_savestate_db[savestate_index]->game_icon_path; + std::string game_archive_path = m_savestate_db[savestate_index]->archive_path; - item->set_icon_load_func([this, icon_path, archive_path, savestate_index, cancel = item->icon_loading_aborted(), dpr](int index) + item->set_icon_load_func([this, icon_path = std::move(game_icon_path), archive_path = std::move(game_archive_path), savestate_index, cancel = item->icon_loading_aborted(), dpr](int index) { if (cancel && cancel->load()) { @@ -434,13 +434,8 @@ void savestate_manager_dialog::ResizeGameIcons() { if (!item->data(GameUserRole::GamePixmapLoaded).toBool()) { - if (!archive_path.empty()) - { - gui::utils::load_icon(icon, icon_path, archive_path); - } - // Load game icon - if (!icon.load(QString::fromStdString(icon_path))) + if (!gui::utils::load_icon(icon, icon_path, archive_path)) { gui_log.warning("Could not load savestate game icon from path %s", icon_path); } diff --git a/rpcs3/rpcs3qt/trophy_manager_dialog.cpp b/rpcs3/rpcs3qt/trophy_manager_dialog.cpp index d784f16f6e..51ef2d2e9f 100644 --- a/rpcs3/rpcs3qt/trophy_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/trophy_manager_dialog.cpp @@ -649,9 +649,9 @@ void trophy_manager_dialog::ResizeGameIcons() { const qreal dpr = devicePixelRatioF(); const int trophy_index = item->data(GameUserRole::GameIndex).toInt(); - const QString icon_path = QString::fromStdString(m_trophies_db[trophy_index]->path); + QString trophy_icon_path = QString::fromStdString(m_trophies_db[trophy_index]->path); - item->set_icon_load_func([this, icon_path, localized_icon, trophy_index, cancel = item->icon_loading_aborted(), dpr](int index) + item->set_icon_load_func([this, icon_path = std::move(trophy_icon_path), localized_icon, trophy_index, cancel = item->icon_loading_aborted(), dpr](int index) { if (cancel && cancel->load()) { From f2afdee40c4507c2df40ca37f9ca314c12b9f314 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 10 Jan 2026 06:52:49 +0100 Subject: [PATCH 30/47] Qt: fix missing mice menu --- rpcs3/rpcs3qt/main_window.ui | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index bf75029d38..5876bfeead 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -242,17 +242,17 @@ Configuration - - - Mice - - - - Devices + + + Mice + + + + @@ -296,9 +296,6 @@ Manage - - - Network Services @@ -307,7 +304,6 @@ - Portals and Gates @@ -317,6 +313,10 @@ + + + + @@ -1046,7 +1046,7 @@ - + :/Icons/rpcn.png:/Icons/rpcn.png From 41a5c69db4c9ff4b9943b35701f427618a0b37e3 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 10 Jan 2026 06:58:59 +0100 Subject: [PATCH 31/47] Qt: Allow to add iso files from menu --- rpcs3/rpcs3qt/main_window.cpp | 16 ++++++++++++++++ rpcs3/rpcs3qt/main_window.ui | 6 ++++++ 2 files changed, 22 insertions(+) diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 98d3ceefb3..bd910e0698 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -2531,6 +2531,22 @@ void main_window::CreateConnects() AddGamesFromDirs(std::move(paths)); }); + connect(ui->addIsoGamesAct, &QAction::triggered, this, [this]() + { + if (!m_gui_settings->GetBootConfirmation(this)) + { + return; + } + + QStringList paths = QFileDialog::getOpenFileNames(this, tr("Select ISO files to add"), QString::fromStdString(fs::get_config_dir()), tr("ISO files (*.iso);;All files (*.*)")); + if (paths.isEmpty()) + { + return; + } + + AddGamesFromDirs(std::move(paths)); + }); + connect(ui->bootRecentMenu, &QMenu::aboutToShow, this, [this]() { // Enable/Disable Recent Games List diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index 5876bfeead..9b9853d315 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -218,6 +218,7 @@ + @@ -1513,6 +1514,11 @@ Download Update + + + Add ISO Games + + From 6bd4f52173cf3bbb98f3fa139868d75071f6c2b2 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 10 Jan 2026 07:06:09 +0100 Subject: [PATCH 32/47] Qt: Allow to boot iso files from menu --- rpcs3/rpcs3qt/main_window.cpp | 29 +++++++++++++++++++++++++++++ rpcs3/rpcs3qt/main_window.h | 1 + rpcs3/rpcs3qt/main_window.ui | 7 ++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index bd910e0698..56e125ab11 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -700,6 +700,34 @@ void main_window::BootGame() Boot(dir_path.toStdString(), "", false, true); } +void main_window::BootISO() +{ + bool stopped = false; + + if (Emu.IsRunning()) + { + Emu.Pause(); + stopped = true; + } + + const QString path_last_game = m_gui_settings->GetValue(gui::fd_boot_game).toString(); + const QString path = QFileDialog::getOpenFileName(this, tr("Select ISO"), path_last_game, tr("ISO files (*.iso);;All files (*.*)")); + + if (path.isEmpty()) + { + if (stopped) + { + Emu.Resume(); + } + return; + } + + m_gui_settings->SetValue(gui::fd_boot_game, QFileInfo(path).dir().path()); + + gui_log.notice("Booting from BootISO..."); + Boot(path.toStdString(), "", true, true); +} + void main_window::BootVSH() { gui_log.notice("Booting from BootVSH..."); @@ -2479,6 +2507,7 @@ void main_window::CreateConnects() connect(ui->bootElfAct, &QAction::triggered, this, &main_window::BootElf); connect(ui->bootTestAct, &QAction::triggered, this, &main_window::BootTest); connect(ui->bootGameAct, &QAction::triggered, this, &main_window::BootGame); + connect(ui->bootIsoAct, &QAction::triggered, this, &main_window::BootISO); connect(ui->bootVSHAct, &QAction::triggered, this, &main_window::BootVSH); connect(ui->actionopen_rsx_capture, &QAction::triggered, this, [this](){ BootRsxCapture(); }); connect(ui->actionCreate_RSX_Capture, &QAction::triggered, this, []() diff --git a/rpcs3/rpcs3qt/main_window.h b/rpcs3/rpcs3qt/main_window.h index ada8f01cad..588c6e6918 100644 --- a/rpcs3/rpcs3qt/main_window.h +++ b/rpcs3/rpcs3qt/main_window.h @@ -104,6 +104,7 @@ private Q_SLOTS: void BootElf(); void BootTest(); void BootGame(); + void BootISO(); void BootVSH(); void BootSavestate(); void BootRsxCapture(std::string path = ""); diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index 9b9853d315..1f6a75c6f7 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -62,7 +62,6 @@ 10 - 50 false @@ -211,6 +210,7 @@ + @@ -1519,6 +1519,11 @@ Add ISO Games + + + Boot ISO + + From 98e64d7821472a61fc11cf40aa72f1481e86ddc8 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 10 Jan 2026 07:40:35 +0100 Subject: [PATCH 33/47] Qt: fix appicon for ISO --- rpcs3/rpcs3qt/qt_utils.cpp | 67 ++++++++++++++++++++++++++------------ rpcs3/rpcs3qt/qt_utils.h | 3 ++ 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/rpcs3/rpcs3qt/qt_utils.cpp b/rpcs3/rpcs3qt/qt_utils.cpp index 0f53ee0f86..a957dba11c 100644 --- a/rpcs3/rpcs3qt/qt_utils.cpp +++ b/rpcs3/rpcs3qt/qt_utils.cpp @@ -393,27 +393,46 @@ namespace gui std::string icon_path = fs::get_config_dir() + "/Icons/game_icons/" + title_id + "/ICON0.PNG"; bool found_file = fs::is_file(icon_path); + std::unique_ptr pixmap; + if (!found_file) { // Get Icon for the gs_frame from path. this handles presumably all possible use cases - const QString qpath = QString::fromStdString(path); - const std::string path_list[] = { path, qpath.section("/", 0, -2, QString::SectionIncludeTrailingSep).toStdString(), - qpath.section("/", 0, -3, QString::SectionIncludeTrailingSep).toStdString() }; + std::vector path_list; - for (const std::string& pth : path_list) + const bool is_archive = is_file_iso(path); + if (is_archive) { - if (!fs::is_dir(pth)) + icon_path = "PS3_GAME/ICON0.PNG"; + + QPixmap px; + if (load_iso_icon(px, icon_path, path)) { - continue; + found_file = true; + pixmap = std::make_unique(std::move(px)); } + } + else + { + const QString qpath = QString::fromStdString(path); + path_list = { path, qpath.section("/", 0, -2, QString::SectionIncludeTrailingSep).toStdString(), + qpath.section("/", 0, -3, QString::SectionIncludeTrailingSep).toStdString() }; - const std::string sfo_dir = rpcs3::utils::get_sfo_dir_from_game_path(pth, title_id); - icon_path = sfo_dir + "/ICON0.PNG"; - found_file = fs::is_file(icon_path); - - if (found_file) + for (const std::string& pth : path_list) { - break; + if (!fs::is_dir(pth)) + { + continue; + } + + const std::string sfo_dir = rpcs3::utils::get_sfo_dir_from_game_path(pth, title_id); + icon_path = sfo_dir + "/ICON0.PNG"; + found_file = fs::is_file(icon_path); + + if (found_file) + { + break; + } } } } @@ -421,7 +440,7 @@ namespace gui if (found_file) { // load the image from path. It will most likely be a rectangle - const QImage source = QImage(QString::fromStdString(icon_path)); + const QImage source = pixmap ? pixmap->toImage() : QImage(QString::fromStdString(icon_path)); const int edge_max = std::max(source.width(), source.height()); // create a new transparent image with square size and same format as source (maybe handle other formats than RGB32 as well?) @@ -683,15 +702,9 @@ namespace gui return QString("%1 days ago %2").arg(current_date - exctrated_date).arg(date.toString(fmt_relative)); } - bool load_icon(QPixmap& icon, const std::string& icon_path, const std::string& archive_path) + bool load_iso_icon(QPixmap& icon, const std::string& icon_path, const std::string& archive_path) { - if (icon_path.empty()) return false; - - if (archive_path.empty()) - { - return icon.load(QString::fromStdString(icon_path)); - } - + if (icon_path.empty() || archive_path.empty()) return false; if (!is_file_iso(archive_path)) return false; iso_archive archive(archive_path); @@ -707,6 +720,18 @@ namespace gui return icon.loadFromData(data); } + bool load_icon(QPixmap& icon, const std::string& icon_path, const std::string& archive_path) + { + if (icon_path.empty()) return false; + + if (archive_path.empty()) + { + return icon.load(QString::fromStdString(icon_path)); + } + + return load_iso_icon(icon, icon_path, archive_path); + } + QString format_timestamp(s64 time, const QString& fmt) { return format_datetime(datetime(time), fmt); diff --git a/rpcs3/rpcs3qt/qt_utils.h b/rpcs3/rpcs3qt/qt_utils.h index de2473c083..c08c5665a7 100644 --- a/rpcs3/rpcs3qt/qt_utils.h +++ b/rpcs3/rpcs3qt/qt_utils.h @@ -179,6 +179,9 @@ namespace gui } // Loads an icon from an (ISO) archive file. + bool load_iso_icon(QPixmap& icon, const std::string& icon_path, const std::string& archive_path); + + // Loads an icon (optionally from an (ISO) archive file). bool load_icon(QPixmap& icon, const std::string& icon_path, const std::string& archive_path); template From 4dc994833d3ae766392135f475ab2bc04242a1ce Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 10 Jan 2026 07:53:17 +0100 Subject: [PATCH 34/47] Qt: fix shortcut creation for ISO --- rpcs3/rpcs3qt/game_list_actions.cpp | 2 +- rpcs3/rpcs3qt/shortcut_utils.cpp | 20 +++++++++++++++----- rpcs3/rpcs3qt/shortcut_utils.h | 1 + rpcs3/rpcs3qt/welcome_dialog.cpp | 4 ++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/rpcs3/rpcs3qt/game_list_actions.cpp b/rpcs3/rpcs3qt/game_list_actions.cpp index 8e53a94df1..8cc4f767ef 100644 --- a/rpcs3/rpcs3qt/game_list_actions.cpp +++ b/rpcs3/rpcs3qt/game_list_actions.cpp @@ -1534,7 +1534,7 @@ void game_list_actions::CreateShortcuts(const std::vector& games, con #endif } - if (!gameid_token_value.empty() && gui::utils::create_shortcut(gameinfo->info.name, gameinfo->info.serial, target_cli_args, gameinfo->info.name, gameinfo->info.icon_path, target_icon_dir, location)) + if (!gameid_token_value.empty() && gui::utils::create_shortcut(gameinfo->info.name, gameinfo->icon_in_archive ? gameinfo->info.path : "", gameinfo->info.serial, target_cli_args, gameinfo->info.name, gameinfo->info.icon_path, target_icon_dir, location)) { game_list_log.success("Created %s shortcut for %s", destination, QString::fromStdString(gameinfo->info.name).simplified()); } diff --git a/rpcs3/rpcs3qt/shortcut_utils.cpp b/rpcs3/rpcs3qt/shortcut_utils.cpp index 98f72d2872..00bec65f13 100644 --- a/rpcs3/rpcs3qt/shortcut_utils.cpp +++ b/rpcs3/rpcs3qt/shortcut_utils.cpp @@ -4,6 +4,7 @@ #include "Emu/VFS.h" #include "Utilities/File.h" #include "Utilities/StrUtil.h" +#include "Loader/ISO.h" #ifdef _WIN32 #include @@ -31,7 +32,7 @@ LOG_CHANNEL(sys_log, "SYS"); namespace gui::utils { - bool create_square_shortcut_icon_file(const std::string& src_icon_path, const std::string& target_icon_dir, std::string& target_icon_path, const std::string& extension, int size) + bool create_square_shortcut_icon_file(const std::string& path, const std::string& src_icon_path, const std::string& target_icon_dir, std::string& target_icon_path, const std::string& extension, int size) { if (src_icon_path.empty() || target_icon_dir.empty() || extension.empty()) { @@ -39,7 +40,15 @@ namespace gui::utils return false; } - QPixmap icon(QString::fromStdString(src_icon_path)); + const bool is_archive = is_file_iso(path); + + QPixmap icon; + if (!load_icon(icon, src_icon_path, is_archive ? path : "")) + { + sys_log.error("Failed to create shortcut. Failed to load %sicon: '%s'", is_archive ? "iso " : "", src_icon_path); + return false; + } + if (!gui::utils::create_square_pixmap(icon, size)) { sys_log.error("Failed to create shortcut. Icon empty."); @@ -67,6 +76,7 @@ namespace gui::utils } bool create_shortcut(const std::string& name, + const std::string& path, [[maybe_unused]] const std::string& serial, [[maybe_unused]] const std::string& target_cli_args, [[maybe_unused]] const std::string& description, @@ -189,7 +199,7 @@ namespace gui::utils if (!src_icon_path.empty() && !target_icon_dir.empty()) { std::string target_icon_path; - if (!create_square_shortcut_icon_file(src_icon_path, target_icon_dir, target_icon_path, "ico", 512)) + if (!create_square_shortcut_icon_file(path, src_icon_path, target_icon_dir, target_icon_path, "ico", 512)) return cleanup(false, ".ico creation failed"); const std::wstring w_icon_path = utf8_to_wchar(target_icon_path); @@ -301,7 +311,7 @@ namespace gui::utils if (!src_icon_path.empty()) { std::string target_icon_path = resources_dir; - if (!create_square_shortcut_icon_file(src_icon_path, resources_dir, target_icon_path, "icns", 512)) + if (!create_square_shortcut_icon_file(path, src_icon_path, resources_dir, target_icon_path, "icns", 512)) { // Error is logged in create_square_shortcut_icon_file return false; @@ -339,7 +349,7 @@ namespace gui::utils if (!src_icon_path.empty() && !target_icon_dir.empty()) { std::string target_icon_path; - if (!create_square_shortcut_icon_file(src_icon_path, target_icon_dir, target_icon_path, "png", 512)) + if (!create_square_shortcut_icon_file(path, src_icon_path, target_icon_dir, target_icon_path, "png", 512)) { // Error is logged in create_square_shortcut_icon_file return false; diff --git a/rpcs3/rpcs3qt/shortcut_utils.h b/rpcs3/rpcs3qt/shortcut_utils.h index 1b47182cbb..11208a2bf0 100644 --- a/rpcs3/rpcs3qt/shortcut_utils.h +++ b/rpcs3/rpcs3qt/shortcut_utils.h @@ -12,6 +12,7 @@ namespace gui::utils }; bool create_shortcut(const std::string& name, + const std::string& path, const std::string& serial, const std::string& target_cli_args, const std::string& description, diff --git a/rpcs3/rpcs3qt/welcome_dialog.cpp b/rpcs3/rpcs3qt/welcome_dialog.cpp index cbe2433bfd..e20415594d 100644 --- a/rpcs3/rpcs3qt/welcome_dialog.cpp +++ b/rpcs3/rpcs3qt/welcome_dialog.cpp @@ -83,12 +83,12 @@ welcome_dialog::welcome_dialog(std::shared_ptr gui_settings, bool { if (ui->create_desktop_shortcut->isChecked()) { - gui::utils::create_shortcut("RPCS3", "", "", "RPCS3", ":/rpcs3.svg", fs::get_temp_dir(), gui::utils::shortcut_location::desktop); + gui::utils::create_shortcut("RPCS3", "", "", "", "RPCS3", ":/rpcs3.svg", fs::get_temp_dir(), gui::utils::shortcut_location::desktop); } if (ui->create_applications_menu_shortcut->isChecked()) { - gui::utils::create_shortcut("RPCS3", "", "", "RPCS3", ":/rpcs3.svg", fs::get_temp_dir(), gui::utils::shortcut_location::applications); + gui::utils::create_shortcut("RPCS3", "", "", "", "RPCS3", ":/rpcs3.svg", fs::get_temp_dir(), gui::utils::shortcut_location::applications); } if (ui->use_dark_theme->isChecked() && ui->use_dark_theme->isEnabled()) // if checked and also on initial welcome dialog From a8624682a770738ffb7d6b0c1ff48dc9c3e6df5b Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 10 Jan 2026 08:20:55 +0100 Subject: [PATCH 35/47] ISO: add some logging --- rpcs3/Emu/System.cpp | 12 +++++++++++- rpcs3/Loader/ISO.cpp | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 6db05a21ea..7c0fa19c93 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -1174,6 +1174,8 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, launching_from_disc_archive = is_file_iso(disc_info); + sys_log.notice("Savestate: is iso archive = %d ('%s')", launching_from_disc_archive, disc_info); + if (!klic[0]) { klic.clear(); @@ -1299,6 +1301,8 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, if (launching_from_disc_archive) { + sys_log.notice("Savestate: Loading iso archive"); + load_iso(disc_info); m_path = iso_device::virtual_device_name + "/" + argv[0]; @@ -1441,6 +1445,8 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, const std::string resolved_path = GetCallbacks().resolve_path(m_path); if (!launching_from_disc_archive && is_file_iso(m_path)) { + sys_log.notice("Loading iso archive '%s'", m_path); + load_iso(m_path); launching_from_disc_archive = true; @@ -1457,6 +1463,8 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, m_path = path; } + sys_log.notice("Load: is iso archive = %d (m_path='%s')", launching_from_disc_archive, m_path); + if (launching_from_disc_archive) { m_dir = "/dev_bdvd/PS3_GAME/"; @@ -1855,6 +1863,8 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, { if (is_file_iso(game_path)) { + sys_log.notice("Loading iso archive for patch ('%s')", game_path); + load_iso(game_path); launching_from_disc_archive = true; game_path = iso_device::virtual_device_name + "/PS3_GAME/./"; @@ -2368,7 +2378,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, else if (!bdvd_dir.empty() && fs::is_dir(bdvd_dir)) { // Disc games are on /dev_bdvd/ - const std::string& disc_path = !launching_from_disc_archive ? resolved_path : m_path; + const std::string& disc_path = launching_from_disc_archive ? m_path : resolved_path; const usz pos = disc_path.rfind(m_game_dir); argv[0] = "/dev_bdvd/PS3_GAME/" + unescape(disc_path.substr(pos + m_game_dir.size() + 1)); m_dir = "/dev_bdvd/PS3_GAME/"; diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index 0f9772cd8f..064bc48e61 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -9,6 +9,8 @@ #include #include +LOG_CHANNEL(sys_log, "SYS"); + bool is_file_iso(const std::string& path) { if (path.empty()) return false; @@ -609,6 +611,8 @@ void iso_dir::rewind() void load_iso(const std::string& path) { + sys_log.notice("Loading iso '%s'", path); + fs::set_virtual_device("iso_overlay_fs_dev", stx::make_shared(path)); vfs::mount("/dev_bdvd/"sv, iso_device::virtual_device_name + "/"); @@ -616,5 +620,7 @@ void load_iso(const std::string& path) void unload_iso() { + sys_log.notice("Unoading iso"); + fs::set_virtual_device("iso_overlay_fs_dev", stx::shared_ptr()); } From f712de0c917658c206649a0b6c2a71978e6038f2 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 10 Jan 2026 12:35:19 +0100 Subject: [PATCH 36/47] ISO: Remove psf hack --- Utilities/File.h | 2 ++ rpcs3/Loader/ISO.cpp | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Utilities/File.h b/Utilities/File.h index f7765507e8..7e6356da7b 100644 --- a/Utilities/File.h +++ b/Utilities/File.h @@ -257,6 +257,8 @@ namespace fs // Open file with specified mode explicit file(const std::string& path, bs_t mode = ::fs::read); + file(std::unique_ptr&& ptr) : m_file(std::move(ptr)) {} + static file from_native_handle(native_handle handle); // Open memory for read diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index 064bc48e61..7e3351cb0e 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -348,10 +348,7 @@ psf::registry iso_archive::open_psf(const std::string& path) auto* archive_file = retrieve(path); if (!archive_file) return psf::registry(); - // HACK: psf does not accept a file_base argument, - // instead we are creating a dummy fs::file and replacing the internal file_base handle with an iso_file - fs::file psf_file(path); - psf_file.reset(std::make_unique(fs::file(m_path), *archive_file)); + const fs::file psf_file(std::make_unique(fs::file(m_path), *archive_file)); return psf::load_object(psf_file, path); } From 36bce336844614001ccbd35a5a6153c3cb7f9483 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 10 Jan 2026 12:35:52 +0100 Subject: [PATCH 37/47] ISO: use string_view in retrieve --- rpcs3/Loader/ISO.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index 7e3351cb0e..b3f9ccb8cf 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -269,9 +269,10 @@ iso_fs_node* iso_archive::retrieve(const std::string& passed_path) if (passed_path.empty()) return nullptr; const std::string path = std::filesystem::path(passed_path).string(); + const std::string_view path_sv = path; size_t start = 0; - size_t end = path.find_first_of(fs::delim); + size_t end = path_sv.find_first_of(fs::delim); std::stack search_stack; search_stack.push(&m_root); @@ -286,7 +287,7 @@ iso_fs_node* iso_archive::retrieve(const std::string& passed_path) end = path.size(); } - const auto path_component = path.substr(start, end-start); + const std::string_view path_component = path_sv.substr(start, end-start); bool found = false; @@ -316,7 +317,7 @@ iso_fs_node* iso_archive::retrieve(const std::string& passed_path) if (!found) return nullptr; start = end + 1; - end = path.find_first_of(fs::delim, start); + end = path_sv.find_first_of(fs::delim, start); } while (start < path.size()); From 9faf9a6145367043cce0123fb41e624a992f58d4 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 10 Jan 2026 12:36:33 +0100 Subject: [PATCH 38/47] ISO: optimize byteswap and utf16 string conversion --- rpcs3/Loader/ISO.cpp | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index b3f9ccb8cf..e46a1a6b82 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -102,24 +102,17 @@ std::optional iso_read_directory_entry(fs::file& file, bool nam } else if (names_in_ucs2) // for strings in joliet descriptor { - std::string new_file_name; - int read = 0; - const u8* raw_str = reinterpret_cast(file_name.c_str()); - while (read < file_name_length) + // characters are stored in big endian format. + std::u16string utf16; + utf16.resize(file_name_length / 2); + + const u16* raw = reinterpret_cast(file_name.data()); + for (size_t i = 0; i < utf16.size(); ++i, raw++) { - // characters are stored in big endian format. - const u16 upper = raw_str[read]; - const u8 lower = raw_str[read + 1]; - - const u16 code_point = (upper << 8) + lower; - - std::wstring_convert, char16_t> convert; - new_file_name += convert.to_bytes(code_point); - - read += 2; + utf16[i] = *reinterpret_cast*>(raw); } - file_name = new_file_name; + file_name = utf16_to_utf8(utf16); } if (file_name.ends_with(";1")) From b2768bbd61f70bdb4ca57815e64809c21a82670d Mon Sep 17 00:00:00 2001 From: zeph Date: Fri, 9 Jan 2026 02:02:51 +0100 Subject: [PATCH 39/47] UI: colored ANSI logs --- rpcs3/rpcs3qt/log_frame.cpp | 14 +++- rpcs3/rpcs3qt/log_frame.h | 2 + rpcs3/rpcs3qt/log_viewer.h | 2 + rpcs3/rpcs3qt/syntax_highlighter.cpp | 101 +++++++++++++++++++++++++++ rpcs3/rpcs3qt/syntax_highlighter.h | 11 +++ 5 files changed, 128 insertions(+), 2 deletions(-) diff --git a/rpcs3/rpcs3qt/log_frame.cpp b/rpcs3/rpcs3qt/log_frame.cpp index bbc6fe6145..7689aa8f9c 100644 --- a/rpcs3/rpcs3qt/log_frame.cpp +++ b/rpcs3/rpcs3qt/log_frame.cpp @@ -3,6 +3,7 @@ #include "gui_settings.h" #include "hex_validator.h" #include "memory_viewer_panel.h" +#include "syntax_highlighter.h" #include "Utilities/lockless.h" #include "util/asm.hpp" @@ -131,6 +132,8 @@ log_frame::log_frame(std::shared_ptr _gui_settings, QWidget* paren m_tty->setContextMenuPolicy(Qt::CustomContextMenu); m_tty->document()->setMaximumBlockCount(max_block_count_tty); m_tty->installEventFilter(this); + + m_tty_ansi_highlighter = new AnsiHighlighter(m_tty->document()); m_tty_input = new QLineEdit(); if (m_tty_channel >= 0) @@ -599,8 +602,15 @@ void log_frame::UpdateUI() buf_line.assign(std::string_view(m_tty_buf).substr(str_index, m_tty_buf.find_first_of('\n', str_index) - str_index)); str_index += buf_line.size() + 1; - // Ignore control characters and greater/equal to 0x80 - buf_line.erase(std::remove_if(buf_line.begin(), buf_line.end(), [](s8 c) { return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F); }), buf_line.end()); + // Ignore control characters and greater/equal to 0x80, but preserve ESC (0x1B) if ANSI mode is enabled + buf_line.erase(std::remove_if(buf_line.begin(), buf_line.end(), [this](s8 c) { + if (m_ansi_tty) + { + // Keep ESC (0x1B) so ANSI sequences remain intact + return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F && c != 0x1B); + } + return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F); + }), buf_line.end()); // save old scroll bar state QScrollBar* sb = m_tty->verticalScrollBar(); diff --git a/rpcs3/rpcs3qt/log_frame.h b/rpcs3/rpcs3qt/log_frame.h index 0de081863c..f8dd57605f 100644 --- a/rpcs3/rpcs3qt/log_frame.h +++ b/rpcs3/rpcs3qt/log_frame.h @@ -5,6 +5,7 @@ #include "custom_dock_widget.h" #include "find_dialog.h" +#include "syntax_highlighter.h" #include @@ -69,6 +70,7 @@ private: QPlainTextEdit* m_tty = nullptr; QLineEdit* m_tty_input = nullptr; int m_tty_channel = -1; + AnsiHighlighter* m_tty_ansi_highlighter = nullptr; QAction* m_clear_act = nullptr; QAction* m_clear_tty_act = nullptr; diff --git a/rpcs3/rpcs3qt/log_viewer.h b/rpcs3/rpcs3qt/log_viewer.h index 85ece2688b..8f9e14f833 100644 --- a/rpcs3/rpcs3qt/log_viewer.h +++ b/rpcs3/rpcs3qt/log_viewer.h @@ -9,6 +9,7 @@ #include class LogHighlighter; +class AnsiHighlighter; class gui_settings; class log_viewer : public QWidget @@ -33,6 +34,7 @@ private: QString m_full_log; QPlainTextEdit* m_log_text; LogHighlighter* m_log_highlighter; + AnsiHighlighter* m_ansi_highlighter; std::unique_ptr m_find_dialog; std::bitset<32> m_log_levels = std::bitset<32>(0b11111111u); bool m_show_timestamps = true; diff --git a/rpcs3/rpcs3qt/syntax_highlighter.cpp b/rpcs3/rpcs3qt/syntax_highlighter.cpp index 33d49e678d..0cc16bc30e 100644 --- a/rpcs3/rpcs3qt/syntax_highlighter.cpp +++ b/rpcs3/rpcs3qt/syntax_highlighter.cpp @@ -183,3 +183,104 @@ GlslHighlighter::GlslHighlighter(QTextDocument* parent) : Highlighter(parent) commentStartExpression = QRegularExpression("/\\*"); commentEndExpression = QRegularExpression("\\*/"); } + +AnsiHighlighter::AnsiHighlighter(QTextDocument* parent) : Highlighter(parent) +{ +} + +void AnsiHighlighter::highlightBlock(const QString &text) +{ + // Match ANSI SGR sequences, e.g. "\x1b[31m" or "\x1b[1;32m" + const QRegularExpression ansi_re("\x1b\\[[0-9;]*m"); + const QRegularExpression param_re("\x1b\\[([0-9;]*)m"); + + QTextCharFormat escapeFormat; + escapeFormat.setForeground(Qt::darkGray); + escapeFormat.setFontItalic(true); + + QTextCharFormat currentFormat; + currentFormat.setForeground(gui::utils::get_foreground_color()); + + int pos = 0; + auto it = ansi_re.globalMatch(text); + while (it.hasNext()) + { + auto match = it.next(); + int start = match.capturedStart(); + int length = match.capturedLength(); + + // Apply current format to the chunk before this escape sequence + if (start > pos) + { + setFormat(pos, start - pos, currentFormat); + } + + // Highlight the escape sequence itself + setFormat(start, length, escapeFormat); + + // Parse SGR parameters and update currentFormat + QRegularExpressionMatch pm = param_re.match(match.captured()); + if (pm.hasMatch()) + { + QString params = pm.captured(1); + if (params.isEmpty()) + { + // empty or just \x1b[m = reset + currentFormat = QTextCharFormat(); + currentFormat.setForeground(gui::utils::get_foreground_color()); + } + else + { + const QStringList codes = params.split(';', Qt::SkipEmptyParts); + for (const QString& c : codes) + { + bool ok = false; + const int code = c.toInt(&ok); + if (!ok) continue; + switch (code) + { + case 0: + currentFormat = QTextCharFormat(); + currentFormat.setForeground(gui::utils::get_foreground_color()); + break; + case 1: + currentFormat.setFontWeight(QFont::Bold); + break; + case 3: + currentFormat.setFontItalic(true); + break; + case 4: + currentFormat.setFontUnderline(true); + break; + case 30: currentFormat.setForeground(Qt::black); break; + case 31: currentFormat.setForeground(Qt::red); break; + case 32: currentFormat.setForeground(Qt::darkGreen); break; + case 33: currentFormat.setForeground(Qt::darkYellow); break; + case 34: currentFormat.setForeground(Qt::darkBlue); break; + case 35: currentFormat.setForeground(Qt::darkMagenta); break; + case 36: currentFormat.setForeground(Qt::darkCyan); break; + case 37: currentFormat.setForeground(Qt::lightGray); break; + case 39: currentFormat.setForeground(gui::utils::get_foreground_color()); break; + case 90: currentFormat.setForeground(Qt::darkGray); break; + case 91: currentFormat.setForeground(Qt::red); break; + case 92: currentFormat.setForeground(Qt::green); break; + case 93: currentFormat.setForeground(Qt::yellow); break; + case 94: currentFormat.setForeground(Qt::blue); break; + case 95: currentFormat.setForeground(Qt::magenta); break; + case 96: currentFormat.setForeground(Qt::cyan); break; + case 97: currentFormat.setForeground(Qt::white); break; + // Background and extended colors not yet handled + default: + break; + } + } + } + } + + pos = start + length; + } + + // Apply remaining format + if (pos < text.length()) + setFormat(pos, text.length() - pos, currentFormat); +} diff --git a/rpcs3/rpcs3qt/syntax_highlighter.h b/rpcs3/rpcs3qt/syntax_highlighter.h index 8179cd2257..d5ba800e3b 100644 --- a/rpcs3/rpcs3qt/syntax_highlighter.h +++ b/rpcs3/rpcs3qt/syntax_highlighter.h @@ -52,3 +52,14 @@ class GlslHighlighter : public Highlighter public: explicit GlslHighlighter(QTextDocument* parent = nullptr); }; + +class AnsiHighlighter : public Highlighter +{ + Q_OBJECT + +public: + explicit AnsiHighlighter(QTextDocument* parent = nullptr); + +protected: + void highlightBlock(const QString &text) override; +}; From 484d97caf172c5ab2133d177f067ddbb3a0150c3 Mon Sep 17 00:00:00 2001 From: zeph Date: Fri, 9 Jan 2026 18:33:16 +0100 Subject: [PATCH 40/47] UI: colored ANSI logs fix-ups --- rpcs3/rpcs3qt/log_frame.cpp | 25 +++++++-- rpcs3/rpcs3qt/log_frame.h | 4 +- rpcs3/rpcs3qt/log_viewer.h | 1 - rpcs3/rpcs3qt/syntax_highlighter.cpp | 81 ++++++++++++++-------------- rpcs3/rpcs3qt/syntax_highlighter.h | 5 +- 5 files changed, 68 insertions(+), 48 deletions(-) diff --git a/rpcs3/rpcs3qt/log_frame.cpp b/rpcs3/rpcs3qt/log_frame.cpp index 7689aa8f9c..e77a847c78 100644 --- a/rpcs3/rpcs3qt/log_frame.cpp +++ b/rpcs3/rpcs3qt/log_frame.cpp @@ -132,8 +132,6 @@ log_frame::log_frame(std::shared_ptr _gui_settings, QWidget* paren m_tty->setContextMenuPolicy(Qt::CustomContextMenu); m_tty->document()->setMaximumBlockCount(max_block_count_tty); m_tty->installEventFilter(this); - - m_tty_ansi_highlighter = new AnsiHighlighter(m_tty->document()); m_tty_input = new QLineEdit(); if (m_tty_channel >= 0) @@ -164,6 +162,11 @@ log_frame::log_frame(std::shared_ptr _gui_settings, QWidget* paren CreateAndConnectActions(); LoadSettings(); + if (m_ansi_tty) + { + m_tty_ansi_highlighter = new AnsiHighlighter(m_tty->document()); + } + m_timer = new QTimer(this); connect(m_timer, &QTimer::timeout, this, &log_frame::UpdateUI); } @@ -291,6 +294,16 @@ void log_frame::CreateAndConnectActions() { m_gui_settings->SetValue(gui::l_ansi_code, checked); m_ansi_tty = checked; + + if (!m_tty_ansi_highlighter) + { + m_tty_ansi_highlighter = new AnsiHighlighter(m_tty->document()); + } + else + { + delete m_tty_ansi_highlighter; + m_tty_ansi_highlighter = nullptr; + } }); m_tty_channel_acts = new QActionGroup(this); @@ -602,13 +615,15 @@ void log_frame::UpdateUI() buf_line.assign(std::string_view(m_tty_buf).substr(str_index, m_tty_buf.find_first_of('\n', str_index) - str_index)); str_index += buf_line.size() + 1; - // Ignore control characters and greater/equal to 0x80, but preserve ESC (0x1B) if ANSI mode is enabled - buf_line.erase(std::remove_if(buf_line.begin(), buf_line.end(), [this](s8 c) { + buf_line.erase(std::remove_if(buf_line.begin(), buf_line.end(), [this](s8 c) + { + // If ANSI TTY is enabled, preserve ESC (0x1B) for ANSI sequences if (m_ansi_tty) { - // Keep ESC (0x1B) so ANSI sequences remain intact return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F && c != 0x1B); } + + // Remove all control characters so output is clean return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F); }), buf_line.end()); diff --git a/rpcs3/rpcs3qt/log_frame.h b/rpcs3/rpcs3qt/log_frame.h index f8dd57605f..35ee672ffc 100644 --- a/rpcs3/rpcs3qt/log_frame.h +++ b/rpcs3/rpcs3qt/log_frame.h @@ -1,11 +1,11 @@ #pragma once #include "Utilities/File.h" +#include "rpcs3qt/syntax_highlighter.h" #include "util/logs.hpp" #include "custom_dock_widget.h" #include "find_dialog.h" -#include "syntax_highlighter.h" #include @@ -13,6 +13,8 @@ #include #include +class AnsiHighlighter; + class gui_settings; class log_frame : public custom_dock_widget diff --git a/rpcs3/rpcs3qt/log_viewer.h b/rpcs3/rpcs3qt/log_viewer.h index 8f9e14f833..a2e8242f89 100644 --- a/rpcs3/rpcs3qt/log_viewer.h +++ b/rpcs3/rpcs3qt/log_viewer.h @@ -34,7 +34,6 @@ private: QString m_full_log; QPlainTextEdit* m_log_text; LogHighlighter* m_log_highlighter; - AnsiHighlighter* m_ansi_highlighter; std::unique_ptr m_find_dialog; std::bitset<32> m_log_levels = std::bitset<32>(0b11111111u); bool m_show_timestamps = true; diff --git a/rpcs3/rpcs3qt/syntax_highlighter.cpp b/rpcs3/rpcs3qt/syntax_highlighter.cpp index 0cc16bc30e..4cc44b6d8b 100644 --- a/rpcs3/rpcs3qt/syntax_highlighter.cpp +++ b/rpcs3/rpcs3qt/syntax_highlighter.cpp @@ -186,48 +186,49 @@ GlslHighlighter::GlslHighlighter(QTextDocument* parent) : Highlighter(parent) AnsiHighlighter::AnsiHighlighter(QTextDocument* parent) : Highlighter(parent) { + m_foreground_color = gui::utils::get_foreground_color(); } -void AnsiHighlighter::highlightBlock(const QString &text) +void AnsiHighlighter::highlightBlock(const QString& text) { // Match ANSI SGR sequences, e.g. "\x1b[31m" or "\x1b[1;32m" - const QRegularExpression ansi_re("\x1b\\[[0-9;]*m"); - const QRegularExpression param_re("\x1b\\[([0-9;]*)m"); + static const QRegularExpression ansi_re("\x1b\\[[0-9;]*m"); + static const QRegularExpression param_re("\x1b\\[([0-9;]*)m"); - QTextCharFormat escapeFormat; - escapeFormat.setForeground(Qt::darkGray); - escapeFormat.setFontItalic(true); + static QTextCharFormat escape_format; + escape_format.setForeground(Qt::darkGray); + escape_format.setFontItalic(true); - QTextCharFormat currentFormat; - currentFormat.setForeground(gui::utils::get_foreground_color()); + static QTextCharFormat current_format; + current_format.setForeground(m_foreground_color); int pos = 0; auto it = ansi_re.globalMatch(text); while (it.hasNext()) { - auto match = it.next(); - int start = match.capturedStart(); - int length = match.capturedLength(); + const auto match = it.next(); + const int start = match.capturedStart(); + const int length = match.capturedLength(); // Apply current format to the chunk before this escape sequence if (start > pos) { - setFormat(pos, start - pos, currentFormat); + setFormat(pos, start - pos, current_format); } // Highlight the escape sequence itself - setFormat(start, length, escapeFormat); + setFormat(start, length, escape_format); // Parse SGR parameters and update currentFormat - QRegularExpressionMatch pm = param_re.match(match.captured()); + const QRegularExpressionMatch pm = param_re.match(match.captured()); if (pm.hasMatch()) { - QString params = pm.captured(1); + const QString params = pm.captured(1); if (params.isEmpty()) { // empty or just \x1b[m = reset - currentFormat = QTextCharFormat(); - currentFormat.setForeground(gui::utils::get_foreground_color()); + current_format = QTextCharFormat(); + current_format.setForeground(m_foreground_color); } else { @@ -240,35 +241,35 @@ void AnsiHighlighter::highlightBlock(const QString &text) switch (code) { case 0: - currentFormat = QTextCharFormat(); - currentFormat.setForeground(gui::utils::get_foreground_color()); + current_format = QTextCharFormat(); + current_format.setForeground(m_foreground_color); break; case 1: - currentFormat.setFontWeight(QFont::Bold); + current_format.setFontWeight(QFont::Bold); break; case 3: - currentFormat.setFontItalic(true); + current_format.setFontItalic(true); break; case 4: - currentFormat.setFontUnderline(true); + current_format.setFontUnderline(true); break; - case 30: currentFormat.setForeground(Qt::black); break; - case 31: currentFormat.setForeground(Qt::red); break; - case 32: currentFormat.setForeground(Qt::darkGreen); break; - case 33: currentFormat.setForeground(Qt::darkYellow); break; - case 34: currentFormat.setForeground(Qt::darkBlue); break; - case 35: currentFormat.setForeground(Qt::darkMagenta); break; - case 36: currentFormat.setForeground(Qt::darkCyan); break; - case 37: currentFormat.setForeground(Qt::lightGray); break; - case 39: currentFormat.setForeground(gui::utils::get_foreground_color()); break; - case 90: currentFormat.setForeground(Qt::darkGray); break; - case 91: currentFormat.setForeground(Qt::red); break; - case 92: currentFormat.setForeground(Qt::green); break; - case 93: currentFormat.setForeground(Qt::yellow); break; - case 94: currentFormat.setForeground(Qt::blue); break; - case 95: currentFormat.setForeground(Qt::magenta); break; - case 96: currentFormat.setForeground(Qt::cyan); break; - case 97: currentFormat.setForeground(Qt::white); break; + case 30: current_format.setForeground(Qt::black); break; + case 31: current_format.setForeground(Qt::red); break; + case 32: current_format.setForeground(Qt::darkGreen); break; + case 33: current_format.setForeground(Qt::darkYellow); break; + case 34: current_format.setForeground(Qt::darkBlue); break; + case 35: current_format.setForeground(Qt::darkMagenta); break; + case 36: current_format.setForeground(Qt::darkCyan); break; + case 37: current_format.setForeground(Qt::lightGray); break; + case 39: current_format.setForeground(m_foreground_color); break; + case 90: current_format.setForeground(Qt::darkGray); break; + case 91: current_format.setForeground(Qt::red); break; + case 92: current_format.setForeground(Qt::green); break; + case 93: current_format.setForeground(Qt::yellow); break; + case 94: current_format.setForeground(Qt::blue); break; + case 95: current_format.setForeground(Qt::magenta); break; + case 96: current_format.setForeground(Qt::cyan); break; + case 97: current_format.setForeground(Qt::white); break; // Background and extended colors not yet handled default: break; @@ -282,5 +283,5 @@ void AnsiHighlighter::highlightBlock(const QString &text) // Apply remaining format if (pos < text.length()) - setFormat(pos, text.length() - pos, currentFormat); + setFormat(pos, text.length() - pos, current_format); } diff --git a/rpcs3/rpcs3qt/syntax_highlighter.h b/rpcs3/rpcs3qt/syntax_highlighter.h index d5ba800e3b..f8577333dd 100644 --- a/rpcs3/rpcs3qt/syntax_highlighter.h +++ b/rpcs3/rpcs3qt/syntax_highlighter.h @@ -2,6 +2,7 @@ #include #include +#include // Inspired by https://doc.qt.io/qt-5/qtwidgets-richtext-syntaxhighlighter-example.html @@ -61,5 +62,7 @@ public: explicit AnsiHighlighter(QTextDocument* parent = nullptr); protected: - void highlightBlock(const QString &text) override; + QColor m_foreground_color; + + void highlightBlock(const QString& text) override; }; From 6eeca69c56e7ee27d5a0489968751006fc2d8ce3 Mon Sep 17 00:00:00 2001 From: zeph Date: Fri, 9 Jan 2026 18:56:15 +0100 Subject: [PATCH 41/47] UI: ANSI colors, further fix-ups --- rpcs3/rpcs3qt/log_frame.cpp | 4 ++-- rpcs3/rpcs3qt/log_frame.h | 1 - rpcs3/rpcs3qt/log_viewer.h | 1 - rpcs3/rpcs3qt/syntax_highlighter.cpp | 15 +++++---------- rpcs3/rpcs3qt/syntax_highlighter.h | 4 ++++ 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/rpcs3/rpcs3qt/log_frame.cpp b/rpcs3/rpcs3qt/log_frame.cpp index e77a847c78..a97e2b6231 100644 --- a/rpcs3/rpcs3qt/log_frame.cpp +++ b/rpcs3/rpcs3qt/log_frame.cpp @@ -295,13 +295,13 @@ void log_frame::CreateAndConnectActions() m_gui_settings->SetValue(gui::l_ansi_code, checked); m_ansi_tty = checked; - if (!m_tty_ansi_highlighter) + if (m_ansi_tty) { m_tty_ansi_highlighter = new AnsiHighlighter(m_tty->document()); } else { - delete m_tty_ansi_highlighter; + m_tty_ansi_highlighter->deleteLater(); m_tty_ansi_highlighter = nullptr; } }); diff --git a/rpcs3/rpcs3qt/log_frame.h b/rpcs3/rpcs3qt/log_frame.h index 35ee672ffc..35bd3a7ab1 100644 --- a/rpcs3/rpcs3qt/log_frame.h +++ b/rpcs3/rpcs3qt/log_frame.h @@ -1,7 +1,6 @@ #pragma once #include "Utilities/File.h" -#include "rpcs3qt/syntax_highlighter.h" #include "util/logs.hpp" #include "custom_dock_widget.h" diff --git a/rpcs3/rpcs3qt/log_viewer.h b/rpcs3/rpcs3qt/log_viewer.h index a2e8242f89..85ece2688b 100644 --- a/rpcs3/rpcs3qt/log_viewer.h +++ b/rpcs3/rpcs3qt/log_viewer.h @@ -9,7 +9,6 @@ #include class LogHighlighter; -class AnsiHighlighter; class gui_settings; class log_viewer : public QWidget diff --git a/rpcs3/rpcs3qt/syntax_highlighter.cpp b/rpcs3/rpcs3qt/syntax_highlighter.cpp index 4cc44b6d8b..2fd43033d5 100644 --- a/rpcs3/rpcs3qt/syntax_highlighter.cpp +++ b/rpcs3/rpcs3qt/syntax_highlighter.cpp @@ -186,20 +186,15 @@ GlslHighlighter::GlslHighlighter(QTextDocument* parent) : Highlighter(parent) AnsiHighlighter::AnsiHighlighter(QTextDocument* parent) : Highlighter(parent) { + m_escape_format.setForeground(Qt::darkGray); + m_escape_format.setFontItalic(true); + m_foreground_color = gui::utils::get_foreground_color(); } void AnsiHighlighter::highlightBlock(const QString& text) { - // Match ANSI SGR sequences, e.g. "\x1b[31m" or "\x1b[1;32m" - static const QRegularExpression ansi_re("\x1b\\[[0-9;]*m"); - static const QRegularExpression param_re("\x1b\\[([0-9;]*)m"); - - static QTextCharFormat escape_format; - escape_format.setForeground(Qt::darkGray); - escape_format.setFontItalic(true); - - static QTextCharFormat current_format; + QTextCharFormat current_format; current_format.setForeground(m_foreground_color); int pos = 0; @@ -217,7 +212,7 @@ void AnsiHighlighter::highlightBlock(const QString& text) } // Highlight the escape sequence itself - setFormat(start, length, escape_format); + setFormat(start, length, m_escape_format); // Parse SGR parameters and update currentFormat const QRegularExpressionMatch pm = param_re.match(match.captured()); diff --git a/rpcs3/rpcs3qt/syntax_highlighter.h b/rpcs3/rpcs3qt/syntax_highlighter.h index f8577333dd..82745ed36f 100644 --- a/rpcs3/rpcs3qt/syntax_highlighter.h +++ b/rpcs3/rpcs3qt/syntax_highlighter.h @@ -62,6 +62,10 @@ public: explicit AnsiHighlighter(QTextDocument* parent = nullptr); protected: + const QRegularExpression ansi_re = QRegularExpression("\x1b\\[[0-9;]*m"); + const QRegularExpression param_re = QRegularExpression("\x1b\\[([0-9;]*)m"); + + QTextCharFormat m_escape_format; QColor m_foreground_color; void highlightBlock(const QString& text) override; From 84ddcea45aaae8f98887a23d74e220ad341e8f17 Mon Sep 17 00:00:00 2001 From: zeph Date: Fri, 9 Jan 2026 18:58:57 +0100 Subject: [PATCH 42/47] UI: ANSI colors, more explicit `erase` check --- rpcs3/rpcs3qt/log_frame.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/rpcs3/rpcs3qt/log_frame.cpp b/rpcs3/rpcs3qt/log_frame.cpp index a97e2b6231..6e6c216ebf 100644 --- a/rpcs3/rpcs3qt/log_frame.cpp +++ b/rpcs3/rpcs3qt/log_frame.cpp @@ -615,17 +615,22 @@ void log_frame::UpdateUI() buf_line.assign(std::string_view(m_tty_buf).substr(str_index, m_tty_buf.find_first_of('\n', str_index) - str_index)); str_index += buf_line.size() + 1; - buf_line.erase(std::remove_if(buf_line.begin(), buf_line.end(), [this](s8 c) + // If ANSI TTY is enabled, remove all control characters except for ESC (0x1B) for ANSI sequences + if (m_ansi_tty) { - // If ANSI TTY is enabled, preserve ESC (0x1B) for ANSI sequences - if (m_ansi_tty) + buf_line.erase(std::remove_if(buf_line.begin(), buf_line.end(), [](s8 c) { return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F && c != 0x1B); - } - - // Remove all control characters so output is clean - return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F); - }), buf_line.end()); + }), buf_line.end()); + } + // Otherwise, remove all control characters to keep the output clean + else + { + buf_line.erase(std::remove_if(buf_line.begin(), buf_line.end(), [](s8 c) + { + return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F); + }), buf_line.end()); + } // save old scroll bar state QScrollBar* sb = m_tty->verticalScrollBar(); From 4431f6b3d9822c9fedc6bf8cff7fddd0077a4ee6 Mon Sep 17 00:00:00 2001 From: zeph Date: Fri, 9 Jan 2026 19:21:31 +0100 Subject: [PATCH 43/47] UI: ANSI TTY colors, fix git submodule versions and correct include --- rpcs3/rpcs3qt/syntax_highlighter.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcs3/rpcs3qt/syntax_highlighter.h b/rpcs3/rpcs3qt/syntax_highlighter.h index 82745ed36f..3854059dd7 100644 --- a/rpcs3/rpcs3qt/syntax_highlighter.h +++ b/rpcs3/rpcs3qt/syntax_highlighter.h @@ -2,7 +2,7 @@ #include #include -#include +#include // Inspired by https://doc.qt.io/qt-5/qtwidgets-richtext-syntaxhighlighter-example.html From 50fcee6fe04fee4539ff2aaa52de0098248556f9 Mon Sep 17 00:00:00 2001 From: zeph <35661622+ZephyrCodesStuff@users.noreply.github.com> Date: Fri, 9 Jan 2026 20:38:36 +0100 Subject: [PATCH 44/47] UI: ANSI colors, code fix suggestion Co-authored-by: Megamouse --- rpcs3/rpcs3qt/log_frame.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rpcs3/rpcs3qt/log_frame.cpp b/rpcs3/rpcs3qt/log_frame.cpp index 6e6c216ebf..2080cdabcf 100644 --- a/rpcs3/rpcs3qt/log_frame.cpp +++ b/rpcs3/rpcs3qt/log_frame.cpp @@ -295,11 +295,11 @@ void log_frame::CreateAndConnectActions() m_gui_settings->SetValue(gui::l_ansi_code, checked); m_ansi_tty = checked; - if (m_ansi_tty) + if (m_ansi_tty && !m_tty_ansi_highlighter) { m_tty_ansi_highlighter = new AnsiHighlighter(m_tty->document()); } - else + else if (!m_ansi_tty && m_tty_ansi_highlighter) { m_tty_ansi_highlighter->deleteLater(); m_tty_ansi_highlighter = nullptr; From ad65878c0c637cacb51bfcede8857826caa69c19 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Mon, 1 Sep 2025 19:57:38 +0200 Subject: [PATCH 45/47] gl: fix some cppcheck issues --- rpcs3/Emu/RSX/Common/texture_cache_utils.h | 6 +++--- rpcs3/Emu/RSX/GL/GLRenderTargets.cpp | 4 +++- rpcs3/Emu/RSX/GL/GLTexture.cpp | 2 ++ rpcs3/Emu/RSX/GL/glutils/program.h | 2 +- rpcs3/Emu/RSX/GL/glutils/state_tracker.hpp | 7 ++----- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/rpcs3/Emu/RSX/Common/texture_cache_utils.h b/rpcs3/Emu/RSX/Common/texture_cache_utils.h index c34b73f7ba..a180a1a8d1 100644 --- a/rpcs3/Emu/RSX/Common/texture_cache_utils.h +++ b/rpcs3/Emu/RSX/Common/texture_cache_utils.h @@ -79,7 +79,7 @@ namespace rsx private: // Members - block_list *block; + block_list* block = nullptr; list_iterator list_it = {}; size_type idx = u32{umax}; size_type array_idx = 0; @@ -705,9 +705,9 @@ namespace rsx private: // Members address_range32 range; - section_bounds bounds; + section_bounds bounds {}; - block_type *block = nullptr; + block_type* block = nullptr; bool needs_overlap_check = true; bool unowned_remaining = false; unowned_iterator unowned_it = {}; diff --git a/rpcs3/Emu/RSX/GL/GLRenderTargets.cpp b/rpcs3/Emu/RSX/GL/GLRenderTargets.cpp index 4e5e61dcba..18058842aa 100644 --- a/rpcs3/Emu/RSX/GL/GLRenderTargets.cpp +++ b/rpcs3/Emu/RSX/GL/GLRenderTargets.cpp @@ -218,7 +218,7 @@ void GLGSRender::init_buffers(rsx::framebuffer_creation_context context, bool /* static_cast(m_draw_fbo)->release(); } - for (auto &fbo : m_framebuffer_cache) + for (auto& fbo : m_framebuffer_cache) { if (fbo.matches(color_targets, depth_stencil_target)) { @@ -264,6 +264,8 @@ void GLGSRender::init_buffers(rsx::framebuffer_creation_context context, bool /* } } + ensure(m_draw_fbo); + switch (rsx::method_registers.surface_color_target()) { case rsx::surface_target::none: break; diff --git a/rpcs3/Emu/RSX/GL/GLTexture.cpp b/rpcs3/Emu/RSX/GL/GLTexture.cpp index 7b43cfc0a7..9a439177f4 100644 --- a/rpcs3/Emu/RSX/GL/GLTexture.cpp +++ b/rpcs3/Emu/RSX/GL/GLTexture.cpp @@ -289,6 +289,8 @@ namespace gl void* copy_image_to_buffer(gl::command_context& cmd, const pixel_buffer_layout& pack_info, const gl::texture* src, gl::buffer* dst, u32 dst_offset, const int src_level, const coord3u& src_region, image_memory_requirements* mem_info) { + ensure(src && dst); + auto initialize_scratch_mem = [&]() -> bool // skip_transform { const u64 max_mem = (mem_info->memory_required) ? mem_info->memory_required : mem_info->image_size_in_bytes; diff --git a/rpcs3/Emu/RSX/GL/glutils/program.h b/rpcs3/Emu/RSX/GL/glutils/program.h index 72daef2523..5caca0ed98 100644 --- a/rpcs3/Emu/RSX/GL/glutils/program.h +++ b/rpcs3/Emu/RSX/GL/glutils/program.h @@ -14,7 +14,7 @@ namespace gl class shader { std::string source; - ::glsl::program_domain type; + ::glsl::program_domain type {}; GLuint m_id = GL_NONE; fence m_compiled_fence; diff --git a/rpcs3/Emu/RSX/GL/glutils/state_tracker.hpp b/rpcs3/Emu/RSX/GL/glutils/state_tracker.hpp index 9835a5891f..694ed28cac 100644 --- a/rpcs3/Emu/RSX/GL/glutils/state_tracker.hpp +++ b/rpcs3/Emu/RSX/GL/glutils/state_tracker.hpp @@ -376,15 +376,12 @@ namespace gl GLuint get_bound_texture(GLuint layer, GLenum target) { - ensure(layer < 48); - return bound_textures[layer][target]; + return ::at32(bound_textures, layer)[target]; } void bind_texture(GLuint layer, GLenum target, GLuint name, GLboolean force = GL_FALSE) { - ensure(layer < 48); - - auto& bound = bound_textures[layer][target]; + auto& bound = ::at32(bound_textures, layer)[target]; if (bound != name || force) { glActiveTexture(GL_TEXTURE0 + layer); From ae6bdf84f17db0e52fccb38c60d52f06d628a4d4 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 8 Apr 2025 08:58:35 +0200 Subject: [PATCH 46/47] Update ffmpeg to 7.1.2 --- 3rdparty/ffmpeg | 2 +- rpcs3/util/media_utils.cpp | 65 +++++++++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/3rdparty/ffmpeg b/3rdparty/ffmpeg index ec6367d3ba..ce81114ed9 160000 --- a/3rdparty/ffmpeg +++ b/3rdparty/ffmpeg @@ -1 +1 @@ -Subproject commit ec6367d3ba9d0d57b9d22d4b87da8144acaf428f +Subproject commit ce81114ed99e5510f6cd983f59a1eac9f33bb73c diff --git a/rpcs3/util/media_utils.cpp b/rpcs3/util/media_utils.cpp index 60a337e750..571eaa74fd 100644 --- a/rpcs3/util/media_utils.cpp +++ b/rpcs3/util/media_utils.cpp @@ -347,9 +347,22 @@ namespace utils { if (!codec) return false; - for (const AVSampleFormat* p = codec->sample_fmts; p && *p != AV_SAMPLE_FMT_NONE; p++) + const void* sample_formats = nullptr; + int num = 0; + + if (const int err = avcodec_get_supported_config(nullptr, codec, AVCodecConfig::AV_CODEC_CONFIG_SAMPLE_FORMAT, 0, &sample_formats, &num)) { - if (*p == sample_fmt) + media_log.error("check_sample_fmt: avcodec_get_supported_config error: %d='%s'", err, av_error_to_string(err)); + return false; + } + + if (!sample_formats) + return true; // All supported + + int i = 0; + for (const AVSampleFormat* fmt = static_cast(sample_formats); fmt && *fmt != AV_SAMPLE_FMT_NONE && i < num; fmt++, i++) + { + if (*fmt == sample_fmt) { return true; } @@ -360,18 +373,33 @@ namespace utils // just pick the highest supported samplerate static int select_sample_rate(const AVCodec* codec) { - if (!codec || !codec->supported_samplerates) - return 48000; + constexpr int default_sample_rate = 48000; - int best_samplerate = 0; - for (const int* samplerate = codec->supported_samplerates; samplerate && *samplerate != 0; samplerate++) + if (!codec) + return default_sample_rate; + + const void* sample_rates = nullptr; + int num = 0; + + if (const int err = avcodec_get_supported_config(nullptr, codec, AVCodecConfig::AV_CODEC_CONFIG_SAMPLE_RATE, 0, &sample_rates, &num)) { - if (!best_samplerate || abs(48000 - *samplerate) < abs(48000 - best_samplerate)) + media_log.error("select_sample_rate: avcodec_get_supported_config error: %d='%s'", err, av_error_to_string(err)); + return default_sample_rate; + } + + if (!sample_rates) + return default_sample_rate; + + int i = 0; + int best_sample_rate = 0; + for (const int* sample_rate = static_cast(sample_rates); sample_rate && *sample_rate != 0 && i < num; sample_rate++, i++) + { + if (!best_sample_rate || abs(default_sample_rate - *sample_rate) < abs(default_sample_rate - best_sample_rate)) { - best_samplerate = *samplerate; + best_sample_rate = *sample_rate; } } - return best_samplerate; + return best_sample_rate; } AVChannelLayout get_preferred_channel_layout(int channels) @@ -397,12 +425,25 @@ namespace utils { if (!codec) return nullptr; + const void* ch_layouts = nullptr; + int num = 0; + + if (const int err = avcodec_get_supported_config(nullptr, codec, AVCodecConfig::AV_CODEC_CONFIG_CHANNEL_LAYOUT, 0, &ch_layouts, &num)) + { + media_log.error("select_channel_layout: avcodec_get_supported_config error: %d='%s'", err, av_error_to_string(err)); + return nullptr; + } + + if (!ch_layouts) + return nullptr; + const AVChannelLayout preferred_ch_layout = get_preferred_channel_layout(channels); const AVChannelLayout* found_ch_layout = nullptr; - for (const AVChannelLayout* ch_layout = codec->ch_layouts; - ch_layout && memcmp(ch_layout, &empty_ch_layout, sizeof(AVChannelLayout)) != 0; - ch_layout++) + int i = 0; + for (const AVChannelLayout* ch_layout = static_cast(ch_layouts); + i < num && ch_layout && memcmp(ch_layout, &empty_ch_layout, sizeof(AVChannelLayout)) != 0; + ch_layout++, i++) { media_log.notice("select_channel_layout: listing channel layout '%s' with %d channels", channel_layout_name(*ch_layout), ch_layout->nb_channels); From cd6dfc82a27844860d3596f5e031214ef354d849 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 10 Jan 2026 01:43:24 +0100 Subject: [PATCH 47/47] cellVdec: also use ctx->framerate to calculate the timestamps ffmpeg 7 doesn't seem to guess the time_base parameter anymore --- rpcs3/Emu/Cell/Modules/cellVdec.cpp | 92 ++++++++++++++--------------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/cellVdec.cpp b/rpcs3/Emu/Cell/Modules/cellVdec.cpp index 84bb74ad28..c8c902540c 100644 --- a/rpcs3/Emu/Cell/Modules/cellVdec.cpp +++ b/rpcs3/Emu/Cell/Modules/cellVdec.cpp @@ -211,6 +211,7 @@ struct vdec_context final lf_queue in_cmd; AVRational log_time_base{}; // Used to reduce log spam + AVRational log_framerate{}; // Used to reduce log spam vdec_context(s32 type, u32 /*profile*/, u32 addr, u32 size, vm::ptr func, u32 arg) : type(type) @@ -292,6 +293,19 @@ struct vdec_context final sws_freeContext(sws); } + static u32 freq_to_framerate_code(f64 freq) + { + if (std::abs(freq - 23.976) < 0.002) return CELL_VDEC_FRC_24000DIV1001; + if (std::abs(freq - 24.000) < 0.001) return CELL_VDEC_FRC_24; + if (std::abs(freq - 25.000) < 0.001) return CELL_VDEC_FRC_25; + if (std::abs(freq - 29.970) < 0.002) return CELL_VDEC_FRC_30000DIV1001; + if (std::abs(freq - 30.000) < 0.001) return CELL_VDEC_FRC_30; + if (std::abs(freq - 50.000) < 0.001) return CELL_VDEC_FRC_50; + if (std::abs(freq - 59.940) < 0.002) return CELL_VDEC_FRC_60000DIV1001; + if (std::abs(freq - 60.000) < 0.001) return CELL_VDEC_FRC_60; + return 0; + } + void exec(ppu_thread& ppu, u32 vid) { perf_meter<"VDEC"_u32> perf0; @@ -341,6 +355,7 @@ struct vdec_context final out_queue.clear(); // Flush image queue log_time_base = {}; + log_framerate = {}; frc_set = 0; // TODO: ??? next_pts = 0; @@ -471,10 +486,10 @@ struct vdec_context final frame.userdata = au_usrd; frame.attr = attr; + u64 amend = 0; + if (frc_set) { - u64 amend = 0; - switch (frc_set) { case CELL_VDEC_FRC_24000DIV1001: amend = 1001 * 90000 / 24000; break; @@ -491,62 +506,45 @@ struct vdec_context final } } - next_pts += amend; - next_dts += amend; frame.frc = frc_set; } - else if (ctx->time_base.num == 0) + else if (ctx->time_base.den && ctx->time_base.num) { - if (log_time_base.den != ctx->time_base.den || log_time_base.num != ctx->time_base.num) + const auto freq = 1. * ctx->time_base.den / ctx->time_base.num / ticks_per_frame; + + frame.frc = freq_to_framerate_code(freq); + if (frame.frc) { - cellVdec.error("time_base.num is 0 (handle=0x%x, seq_id=%d, cmd_id=%d, %d/%d, tpf=%d framerate=%d/%d)", handle, cmd->seq_id, cmd->id, ctx->time_base.num, ctx->time_base.den, ticks_per_frame, ctx->framerate.num, ctx->framerate.den); + amend = u64{90000} * ctx->time_base.num * ticks_per_frame / ctx->time_base.den; + } + } + else if (ctx->framerate.den && ctx->framerate.num) + { + const auto freq = ctx->framerate.num / static_cast(ctx->framerate.den); + + frame.frc = freq_to_framerate_code(freq); + if (frame.frc) + { + amend = u64{90000} * ctx->framerate.den / ctx->framerate.num; + } + } + + if (amend == 0 || frame.frc == 0) + { + if (log_time_base.den != ctx->time_base.den || log_time_base.num != ctx->time_base.num || log_framerate.den != ctx->framerate.den || log_framerate.num != ctx->framerate.num) + { + cellVdec.error("Invalid frequency (handle=0x%x, seq_id=%d, cmd_id=%d, timebase=%d/%d, tpf=%d framerate=%d/%d)", handle, cmd->seq_id, cmd->id, ctx->time_base.num, ctx->time_base.den, ticks_per_frame, ctx->framerate.num, ctx->framerate.den); log_time_base = ctx->time_base; + log_framerate = ctx->framerate; } // Hack - const u64 amend = u64{90000} / 30; + amend = u64{90000} / 30; frame.frc = CELL_VDEC_FRC_30; - next_pts += amend; - next_dts += amend; } - else - { - u64 amend = u64{90000} * ctx->time_base.num * ticks_per_frame / ctx->time_base.den; - const auto freq = 1. * ctx->time_base.den / ctx->time_base.num / ticks_per_frame; - if (std::abs(freq - 23.976) < 0.002) - frame.frc = CELL_VDEC_FRC_24000DIV1001; - else if (std::abs(freq - 24.000) < 0.001) - frame.frc = CELL_VDEC_FRC_24; - else if (std::abs(freq - 25.000) < 0.001) - frame.frc = CELL_VDEC_FRC_25; - else if (std::abs(freq - 29.970) < 0.002) - frame.frc = CELL_VDEC_FRC_30000DIV1001; - else if (std::abs(freq - 30.000) < 0.001) - frame.frc = CELL_VDEC_FRC_30; - else if (std::abs(freq - 50.000) < 0.001) - frame.frc = CELL_VDEC_FRC_50; - else if (std::abs(freq - 59.940) < 0.002) - frame.frc = CELL_VDEC_FRC_60000DIV1001; - else if (std::abs(freq - 60.000) < 0.001) - frame.frc = CELL_VDEC_FRC_60; - else - { - if (log_time_base.den != ctx->time_base.den || log_time_base.num != ctx->time_base.num) - { - // 1/1000 usually means that the time stamps are written in 1ms units and that the frame rate may vary. - cellVdec.error("Unsupported time_base (handle=0x%x, seq_id=%d, cmd_id=%d, %d/%d, tpf=%d framerate=%d/%d)", handle, cmd->seq_id, cmd->id, ctx->time_base.num, ctx->time_base.den, ticks_per_frame, ctx->framerate.num, ctx->framerate.den); - log_time_base = ctx->time_base; - } - - // Hack - amend = u64{90000} / 30; - frame.frc = CELL_VDEC_FRC_30; - } - - next_pts += amend; - next_dts += amend; - } + next_pts += amend; + next_dts += amend; cellVdec.trace("Got picture (handle=0x%x, seq_id=%d, cmd_id=%d, pts=0x%llx[0x%llx], dts=0x%llx[0x%llx])", handle, cmd->seq_id, cmd->id, frame.pts, frame->pts, frame.dts, frame->pkt_dts);