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/Utilities/File.h b/Utilities/File.h index 90453f16f0..7e6356da7b 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(); @@ -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/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/Emu/Cell/Modules/cellVdec.cpp b/rpcs3/Emu/Cell/Modules/cellVdec.cpp index 1159dfa41f..b4668be9df 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); 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/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/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); } 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); diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 651c220b60..7c0fa19c93 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" @@ -782,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")) @@ -931,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(); } } @@ -1078,6 +1080,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(); @@ -1169,12 +1172,16 @@ 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); + + sys_log.notice("Savestate: is iso archive = %d ('%s')", launching_from_disc_archive, 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; @@ -1182,6 +1189,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 @@ -1286,6 +1298,16 @@ 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) + { + sys_log.notice("Savestate: Loading iso 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)) { @@ -1421,6 +1443,33 @@ 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; + + 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 + if (fs::exists(path + "PS3_GAME/USRDIR/EBOOT.BIN")) + { + path = path + "PS3_GAME/USRDIR/EBOOT.BIN"; + } + + 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/"; + m_cat = "DG"sv; + } const std::string elf_dir = fs::get_parent_dir(m_path); @@ -1595,8 +1644,14 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } } + // ISO PKG INSTALL HACK! + 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)) + if (!launching_from_disc_archive && fs::is_dir(m_path)) { m_state = system_state::ready; GetCallbacks().on_ready(); @@ -1788,6 +1843,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) @@ -1802,6 +1861,15 @@ 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)) + { + 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/./"; + } + if (game_path.ends_with("/./")) { // Marked as PS3_GAME directory @@ -1929,21 +1997,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()) @@ -2076,6 +2151,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 (launching_from_disc_archive && fs::is_dir(m_path)) + { + 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()) { @@ -2184,7 +2266,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 || + 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) { @@ -2295,8 +2378,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)); + 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/"; } else if (from_dev_flash) @@ -3368,7 +3452,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); @@ -3546,7 +3630,18 @@ 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 + "/")) + { + const auto device = fs::get_virtual_device(iso_device::virtual_device_name + "/"); + ensure(device); + + const auto iso_dev = dynamic_cast(device.get()); + + ar(m_path.substr(iso_device::virtual_device_name.size() + 1)); + 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")) { // Fake /dev_bdvd/PS3_GAME detected, use HDD0 for m_path restoration ensure(vfs::unmount("/dev_bdvd/PS3_GAME")); @@ -3840,6 +3935,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 @@ -4039,13 +4139,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++; @@ -4143,10 +4248,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 ? "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 ? 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")); @@ -4169,6 +4281,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; @@ -4331,7 +4460,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/Emu/games_config.cpp b/rpcs3/Emu/games_config.cpp index cfa43f6e15..ae0fad8084 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 + "/") + { + const auto device = fs::get_virtual_device(iso_device::virtual_device_name + "/"); + if (!device) return result::failure; + + const auto iso_dev = dynamic_cast(device.get()); + return add_game(key, iso_dev->get_loaded_iso()); + } + std::lock_guard lock(m_mutex); // Access or create node if does not exist 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/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp new file mode 100644 index 0000000000..e46a1a6b82 --- /dev/null +++ b/rpcs3/Loader/ISO.cpp @@ -0,0 +1,617 @@ +#include "stdafx.h" + +#include "ISO.h" +#include "Emu/VFS.h" + +#include +#include +#include +#include +#include + +LOG_CHANNEL(sys_log, "SYS"); + +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)); +} + +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) +{ + 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); + 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(); + 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(); + const s16 timezone_value = file.read(); + const s16 timezone_offset = (timezone_value - 50) * 15 * 60; + + const std::time_t date_time = std::mktime(&file_date) + timezone_offset; + + const u8 flags = file.read(); + + // 2nd flag bit indicates whether a given fs node is a directory + const bool is_directory = flags & 0b00000010; + const bool has_more_extents = flags & 0b10000000; + + file.seek(6, fs::seek_cur); + + const 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 + { + // 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++) + { + utf16[i] = *reinterpret_cast*>(raw); + } + + file_name = utf16_to_utf8(utf16); + } + + 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 = std::move(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, const 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); + + 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) + { + const u64 new_sector = (file.pos() / ISO_BLOCK_SIZE) + 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 (usz index : multi_extent_node_indices) + { + 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]); + + 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 = std::move(*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 + { + const 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 + 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) +{ + 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_sv.find_first_of(fs::delim); + + std::stack search_stack; + search_stack.push(&m_root); + + do + { + if (search_stack.empty()) return nullptr; + const auto* top_entry = search_stack.top(); + + if (end == umax) + { + end = path.size(); + } + + const std::string_view path_component = path_sv.substr(start, end-start); + + bool found = false; + + 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 == path_component) + { + search_stack.push(entry.get()); + + found = true; + break; + } + } + } + + if (!found) return nullptr; + + start = end + 1; + end = path_sv.find_first_of(fs::delim, start); + } + while (start < path.size()); + + if (search_stack.empty()) return nullptr; + + return search_stack.top(); +} + +bool iso_archive::exists(const std::string& path) +{ + return retrieve(path) != nullptr; +} + +bool iso_archive::is_file(const std::string& path) +{ + const 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), *ensure(retrieve(path))); +} + +psf::registry iso_archive::open_psf(const std::string& path) +{ + auto* archive_file = retrieve(path); + if (!archive_file) return psf::registry(); + + const fs::file psf_file(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_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 /*length*/) +{ + fs::g_tls_error = fs::error::readonly; + return false; +} + +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) + { + pos -= it->size; + + it++; + } + + return {pos, *it}; +} + +// assumed valid and in bounds. +u64 iso_file::file_offset(u64 pos) const +{ + 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 +{ + 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) +{ + 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) +{ + const u64 local_remaining = local_extent_remaining(offset); + + const u64 total_read = m_file.read_at(file_offset(offset), buffer, std::min(size, local_remaining)); + + const auto total_size = this->size(); + + if (size > total_read && (offset + total_read) < total_size) + { + const u64 second_total_read = read_at(offset + total_read, reinterpret_cast(buffer) + total_read, size - total_read); + + return total_read + second_total_read; + } + + return total_read; +} + +u64 iso_file::write(const void* /*buffer*/, u64 /*size*/) +{ + 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 result = m_file.seek(file_offset(m_pos)); + if (result == umax) return umax; + + 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()) + { + const auto& selected = m_node.children[m_pos].get()->metadata; + + 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 = selected.size(); + + m_pos++; + + return true; + } + + return false; +} + +bool iso_device::stat(const std::string& path, fs::stat_t& info) +{ + const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string(); + + const auto node = m_archive.retrieve(relative_path); + if (!node) + { + fs::g_tls_error = fs::error::noent; + return false; + } + + const auto& meta = node->metadata; + + info = fs::stat_t + { + .is_directory = meta.is_directory, + .is_symlink = false, + .is_writable = false, + .size = meta.size(), + .atime = meta.time, + .mtime = meta.time, + .ctime = meta.time + }; + + return true; +} + +bool iso_device::statfs(const std::string& path, fs::device_stat& info) +{ + const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string(); + + const auto node = m_archive.retrieve(relative_path); + if (!node) + { + fs::g_tls_error = fs::error::noent; + return false; + } + + 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 + }; + + return false; +} + +std::unique_ptr iso_device::open(const std::string& path, bs_t mode) +{ + const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string(); + + const 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) +{ + const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string(); + + const 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 nullptr; + } + + return std::make_unique(*node); +} + +void iso_dir::rewind() +{ + m_pos = 0; +} + +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 + "/"); +} + +void unload_iso() +{ + sys_log.notice("Unoading iso"); + + fs::set_virtual_device("iso_overlay_fs_dev", stx::shared_ptr()); +} diff --git a/rpcs3/Loader/ISO.h b/rpcs3/Loader/ISO.h new file mode 100644 index 0000000000..3af4732aad --- /dev/null +++ b/rpcs3/Loader/ISO.h @@ -0,0 +1,121 @@ +#pragma once + +#include "Loader/PSF.h" + +#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); +void unload_iso(); + +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 +{ +private: + fs::file m_file; + iso_fs_metadata m_meta; + 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); + + 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 +{ +private: + const iso_fs_node& m_node; + u64 m_pos = 0; + +public: + iso_dir(const iso_fs_node& node) + : m_node(node) + {} + + 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); + + 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); + + 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"; + + 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 + 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/game_list_base.cpp b/rpcs3/rpcs3qt/game_list_base.cpp index 51e43c95bf..edae4e51f6 100644 --- a/rpcs3/rpcs3qt/game_list_base.cpp +++ b/rpcs3/rpcs3qt/game_list_base.cpp @@ -1,4 +1,5 @@ #include "stdafx.h" +#include "qt_utils.h" #include "game_list_base.h" #include @@ -48,7 +49,7 @@ 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_list_log.warning) { diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 9563c46fb5..ea7b87ff6d 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,26 @@ 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) { + 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) + { + return archive ? archive->is_file(path) : fs::is_file(path); + }; + gui_game_info game{}; game.info.path = dir_or_elf; 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 ? "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 ? 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()) @@ -596,7 +610,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 +618,20 @@ void game_list_frame::OnParsingFinished() { game.info.icon_path = sfo_dir + "/ICON0.PNG"; } + game.icon_in_archive = archive && archive->exists(game.info.icon_path); } - 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 +756,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); 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 c3069ef67e..98b9ef344d 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 @@ -277,6 +279,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()); @@ -293,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/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 diff --git a/rpcs3/rpcs3qt/log_frame.cpp b/rpcs3/rpcs3qt/log_frame.cpp index bbc6fe6145..2080cdabcf 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" @@ -161,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); } @@ -288,6 +294,16 @@ void log_frame::CreateAndConnectActions() { m_gui_settings->SetValue(gui::l_ansi_code, checked); m_ansi_tty = checked; + + if (m_ansi_tty && !m_tty_ansi_highlighter) + { + m_tty_ansi_highlighter = new AnsiHighlighter(m_tty->document()); + } + else if (!m_ansi_tty && m_tty_ansi_highlighter) + { + m_tty_ansi_highlighter->deleteLater(); + m_tty_ansi_highlighter = nullptr; + } }); m_tty_channel_acts = new QActionGroup(this); @@ -599,8 +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; - // 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()); + // If ANSI TTY is enabled, remove all control characters except for 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); + }), 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(); diff --git a/rpcs3/rpcs3qt/log_frame.h b/rpcs3/rpcs3qt/log_frame.h index 0de081863c..35bd3a7ab1 100644 --- a/rpcs3/rpcs3qt/log_frame.h +++ b/rpcs3/rpcs3qt/log_frame.h @@ -12,6 +12,8 @@ #include #include +class AnsiHighlighter; + class gui_settings; class log_frame : public custom_dock_widget @@ -69,6 +71,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/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 485f7e3f22..56e125ab11 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" @@ -542,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)) + { + 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."); - 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) @@ -569,6 +578,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); @@ -690,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..."); @@ -2469,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, []() @@ -2521,6 +2560,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 @@ -3958,7 +4013,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) { 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 bf75029d38..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 @@ + @@ -218,6 +218,7 @@ + @@ -242,17 +243,17 @@ Configuration - - - Mice - - - - Devices + + + Mice + + + + @@ -296,9 +297,6 @@ Manage - - - Network Services @@ -307,7 +305,6 @@ - Portals and Gates @@ -317,6 +314,10 @@ + + + + @@ -1046,7 +1047,7 @@ - + :/Icons/rpcn.png:/Icons/rpcn.png @@ -1513,6 +1514,16 @@ Download Update + + + Add ISO Games + + + + + Boot ISO + + 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; diff --git a/rpcs3/rpcs3qt/qt_utils.cpp b/rpcs3/rpcs3qt/qt_utils.cpp index fc53dc542a..a957dba11c 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"); @@ -392,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; + } } } } @@ -420,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?) @@ -682,6 +702,36 @@ namespace gui return QString("%1 days ago %2").arg(current_date - exctrated_date).arg(date.toString(fmt_relative)); } + bool load_iso_icon(QPixmap& icon, const std::string& icon_path, const std::string& archive_path) + { + if (icon_path.empty() || archive_path.empty()) return false; + 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); + 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); + + 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 dc15385923..c08c5665a7 100644 --- a/rpcs3/rpcs3qt/qt_utils.h +++ b/rpcs3/rpcs3qt/qt_utils.h @@ -178,6 +178,12 @@ namespace gui return color_scheme() == Qt::ColorScheme::Dark; } + // 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 void stop_future_watcher(QFutureWatcher& watcher, bool cancel, std::shared_ptr> cancel_flag = nullptr) { diff --git a/rpcs3/rpcs3qt/qt_video_source.cpp b/rpcs3/rpcs3qt/qt_video_source.cpp index 107e5ef850..d74d395c58 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_iso_path(const std::string& iso_path) +{ + m_iso_path = iso_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() || (m_iso_path.empty() && !QFile::exists(m_video_path))) { 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_iso_path.empty()) + { + m_movie = std::make_unique(m_video_path); + } + else + { + 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); + + m_video_buffer = std::make_unique(&m_video_data); + m_video_buffer->open(QIODevice::ReadOnly); + m_movie = std::make_unique(m_video_buffer.get()); + + } if (!m_movie->isValid()) { @@ -85,14 +109,31 @@ 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_iso_path.empty()) { - 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(); + } + else + { + 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); } - // TODO: Decode the pam properly before pushing it to the player - m_video_data = file.readAll(); if (m_video_data.isEmpty()) { return; @@ -148,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 a2710eea33..ce43d593d7 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_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; } @@ -43,6 +44,7 @@ protected: atomic_t m_has_new = false; QString m_video_path; + 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 ed664e1b65..2898c9b170 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 @@ -208,7 +210,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); }); @@ -416,9 +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; + 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, 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()) { @@ -432,7 +435,7 @@ void savestate_manager_dialog::ResizeGameIcons() if (!item->data(GameUserRole::GamePixmapLoaded).toBool()) { // 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); } @@ -617,6 +620,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); 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/syntax_highlighter.cpp b/rpcs3/rpcs3qt/syntax_highlighter.cpp index 412271eb6a..2fd43033d5 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: @@ -183,3 +183,100 @@ GlslHighlighter::GlslHighlighter(QTextDocument *parent) : Highlighter(parent) commentStartExpression = QRegularExpression("/\\*"); commentEndExpression = QRegularExpression("\\*/"); } + +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) +{ + QTextCharFormat current_format; + current_format.setForeground(m_foreground_color); + + int pos = 0; + auto it = ansi_re.globalMatch(text); + while (it.hasNext()) + { + 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, current_format); + } + + // Highlight the escape sequence itself + setFormat(start, length, m_escape_format); + + // Parse SGR parameters and update currentFormat + const QRegularExpressionMatch pm = param_re.match(match.captured()); + if (pm.hasMatch()) + { + const QString params = pm.captured(1); + if (params.isEmpty()) + { + // empty or just \x1b[m = reset + current_format = QTextCharFormat(); + current_format.setForeground(m_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: + current_format = QTextCharFormat(); + current_format.setForeground(m_foreground_color); + break; + case 1: + current_format.setFontWeight(QFont::Bold); + break; + case 3: + current_format.setFontItalic(true); + break; + case 4: + current_format.setFontUnderline(true); + 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; + } + } + } + } + + pos = start + length; + } + + // Apply remaining format + if (pos < text.length()) + setFormat(pos, text.length() - pos, current_format); +} diff --git a/rpcs3/rpcs3qt/syntax_highlighter.h b/rpcs3/rpcs3qt/syntax_highlighter.h index 3e701ffb0e..3854059dd7 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 @@ -10,11 +11,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 +43,7 @@ class AsmHighlighter : public Highlighter Q_OBJECT public: - explicit AsmHighlighter(QTextDocument *parent = nullptr); + explicit AsmHighlighter(QTextDocument* parent = nullptr); }; class GlslHighlighter : public Highlighter @@ -50,5 +51,22 @@ class GlslHighlighter : public Highlighter Q_OBJECT public: - explicit GlslHighlighter(QTextDocument *parent = nullptr); + explicit GlslHighlighter(QTextDocument* parent = nullptr); +}; + +class AnsiHighlighter : public Highlighter +{ + Q_OBJECT + +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; }; 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()) { 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 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);