Merge branch 'master' into screenshot

This commit is contained in:
Ani 2026-01-10 19:39:46 +01:00 committed by GitHub
commit 81be51f01a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1469 additions and 170 deletions

2
3rdparty/ffmpeg vendored

@ -1 +1 @@
Subproject commit ec6367d3ba9d0d57b9d22d4b87da8144acaf428f
Subproject commit ce81114ed99e5510f6cd983f59a1eac9f33bb73c

View file

@ -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<open_mode> mode = ::fs::read);
file(std::unique_ptr<file_base>&& ptr) : m_file(std::move(ptr)) {}
static file from_native_handle(native_handle handle);
// Open memory for read

View file

@ -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
)

View file

@ -211,6 +211,7 @@ struct vdec_context final
lf_queue<vdec_cmd> 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<CellVdecCbMsg> 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<f64>(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);

View file

@ -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
};
}

View file

@ -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<int, 6>;

View file

@ -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 = {};

View file

@ -218,7 +218,7 @@ void GLGSRender::init_buffers(rsx::framebuffer_creation_context context, bool /*
static_cast<gl::framebuffer_holder*>(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;

View file

@ -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;

View file

@ -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);
}

View file

@ -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;

View file

@ -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);

View file

@ -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<rsx::frame_capture_data> frame = std::make_unique<rsx::frame_capture_data>();
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<u8, 32>{}); // 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<const iso_device*>(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<iso_archive> archive;
if (is_file_iso(path))
{
archive = std::make_unique<iso_archive>(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;

View file

@ -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<const iso_device*>(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

View file

@ -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;
}

617
rpcs3/Loader/ISO.cpp Normal file
View file

@ -0,0 +1,617 @@
#include "stdafx.h"
#include "ISO.h"
#include "Emu/VFS.h"
#include <codecvt>
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <stack>
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<typename T>
inline T read_both_endian_int(fs::file& file)
{
T out;
if (std::endian::little == std::endian::native)
{
out = file.read<T>();
file.seek(sizeof(T), fs::seek_cur);
}
else
{
file.seek(sizeof(T), fs::seek_cur);
out = file.read<T>();
}
return out;
}
// assumed that directory_entry is at file head
std::optional<iso_fs_metadata> iso_read_directory_entry(fs::file& file, bool names_in_ucs2 = false)
{
const auto start_pos = file.pos();
const u8 entry_length = file.read<u8>();
if (entry_length == 0) return std::nullopt;
file.seek(1, fs::seek_cur);
const u32 start_sector = read_both_endian_int<u32>(file);
const u32 file_size = read_both_endian_int<u32>(file);
std::tm file_date = {};
file_date.tm_year = file.read<u8>();
file_date.tm_mon = file.read<u8>() - 1;
file_date.tm_mday = file.read<u8>();
file_date.tm_hour = file.read<u8>();
file_date.tm_min = file.read<u8>();
file_date.tm_sec = file.read<u8>();
const s16 timezone_value = file.read<u8>();
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<u8>();
// 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<u8>();
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<const u16*>(file_name.data());
for (size_t i = 0; i < utf16.size(); ++i, raw++)
{
utf16[i] = *reinterpret_cast<const be_t<u16>*>(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<usz> 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>(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<u8>();
// 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<iso_fs_node*> 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<iso_file>(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<u64, iso_extent_info> 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<u8*>(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<fs::file_base> iso_device::open(const std::string& path, bs_t<fs::open_mode> 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<iso_file>(fs::file(iso_path), *node);
}
std::unique_ptr<fs::dir_base> 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<iso_dir>(*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<iso_device>(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<iso_device>());
}

121
rpcs3/Loader/ISO.h Normal file
View file

@ -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<iso_extent_info> extents;
u64 size() const;
};
struct iso_fs_node
{
iso_fs_metadata metadata;
std::vector<std::unique_ptr<iso_fs_node>> children;
};
class iso_file : public fs::file_base
{
private:
fs::file m_file;
iso_fs_metadata m_meta;
u64 m_pos = 0;
std::pair<u64, iso_extent_info> 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<fs::file_base> open(const std::string& path, bs_t<fs::open_mode> mode) override;
std::unique_ptr<fs::dir_base> open_dir(const std::string& path) override;
};

View file

@ -540,6 +540,7 @@
<ClCompile Include="Loader\PSF.cpp" />
<ClCompile Include="Loader\PUP.cpp" />
<ClCompile Include="Loader\TAR.cpp" />
<ClCompile Include="Loader\ISO.cpp" />
<ClCompile Include="Loader\mself.cpp" />
<ClCompile Include="Loader\TROPUSR.cpp" />
<ClCompile Include="Loader\TRP.cpp" />
@ -1021,6 +1022,7 @@
<ClInclude Include="Loader\PSF.h" />
<ClInclude Include="Loader\PUP.h" />
<ClInclude Include="Loader\TAR.h" />
<ClInclude Include="Loader\ISO.h" />
<ClInclude Include="Loader\TROPUSR.h" />
<ClInclude Include="Loader\TRP.h" />
<ClInclude Include="rpcs3_version.h" />
@ -1096,4 +1098,4 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
</Project>

View file

@ -1534,7 +1534,7 @@ void game_list_actions::CreateShortcuts(const std::vector<game_info>& 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());
}

View file

@ -1,4 +1,5 @@
#include "stdafx.h"
#include "qt_utils.h"
#include "game_list_base.h"
#include <QDir>
@ -48,7 +49,7 @@ void game_list_base::IconLoadFunction(game_info game, qreal device_pixel_ratio,
static std::unordered_set<std::string> 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)
{

View file

@ -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<iso_archive> archive;
if (is_file_iso(dir_or_elf))
{
archive = std::make_unique<iso_archive>(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);

View file

@ -4,6 +4,8 @@
#include "gui_settings.h"
#include "qt_utils.h"
#include "Loader/ISO.h"
#include <QApplication>
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))

View file

@ -11,6 +11,8 @@
#include "Emu/vfs_config.h"
#include "Utilities/StrUtil.h"
#include "Loader/ISO.h"
#include <QHeaderView>
#include <QScrollBar>
#include <QStringBuilder>
@ -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);

View file

@ -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

View file

@ -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> _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();

View file

@ -12,6 +12,8 @@
#include <QPlainTextEdit>
#include <QActionGroup>
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;

View file

@ -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<const iso_device*>(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)
{

View file

@ -104,6 +104,7 @@ private Q_SLOTS:
void BootElf();
void BootTest();
void BootGame();
void BootISO();
void BootVSH();
void BootSavestate();
void BootRsxCapture(std::string path = "");

View file

@ -62,7 +62,6 @@
<property name="font">
<font>
<pointsize>10</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
@ -211,6 +210,7 @@
<addaction name="separator"/>
</widget>
<addaction name="bootGameAct"/>
<addaction name="bootIsoAct"/>
<addaction name="bootVSHAct"/>
<addaction name="bootElfMenu"/>
<addaction name="bootSavestateAct"/>
@ -218,6 +218,7 @@
<addaction name="bootRecentMenu"/>
<addaction name="separator"/>
<addaction name="addGamesAct"/>
<addaction name="addIsoGamesAct"/>
<addaction name="bootInstallPkgAct"/>
<addaction name="bootInstallPupAct"/>
<addaction name="separator"/>
@ -242,17 +243,17 @@
<property name="title">
<string>Configuration</string>
</property>
<widget class="QMenu" name="menuMice">
<property name="title">
<string>Mice</string>
</property>
<addaction name="actionBasic_Mouse"/>
<addaction name="actionRaw_Mouse"/>
</widget>
<widget class="QMenu" name="menuDevices">
<property name="title">
<string>Devices</string>
</property>
<widget class="QMenu" name="menuMice">
<property name="title">
<string>Mice</string>
</property>
<addaction name="actionBasic_Mouse"/>
<addaction name="actionRaw_Mouse"/>
</widget>
<addaction name="confPadsAct"/>
<addaction name="menuMice"/>
<addaction name="confCamerasAct"/>
@ -296,9 +297,6 @@
<property name="title">
<string>Manage</string>
</property>
<addaction name="confVFSDialogAct"/>
<addaction name="separator"/>
<addaction name="actionManage_Users"/>
<widget class="QMenu" name="menuNetworkServices">
<property name="title">
<string>Network Services</string>
@ -307,7 +305,6 @@
<addaction name="confClansAct"/>
<addaction name="confIPCAct"/>
</widget>
<addaction name="menuNetworkServices"/>
<widget class="QMenu" name="menuPortals">
<property name="title">
<string>Portals and Gates</string>
@ -317,6 +314,10 @@
<addaction name="actionManage_Dimensions_ToyPad"/>
<addaction name="actionManage_KamenRider_RideGate"/>
</widget>
<addaction name="confVFSDialogAct"/>
<addaction name="separator"/>
<addaction name="actionManage_Users"/>
<addaction name="menuNetworkServices"/>
<addaction name="menuPortals"/>
<addaction name="separator"/>
<addaction name="actionManage_RAP_Licenses"/>
@ -1046,7 +1047,7 @@
</action>
<action name="toolbar_rpcn">
<property name="icon">
<iconset>
<iconset resource="../resources.qrc">
<normaloff>:/Icons/rpcn.png</normaloff>:/Icons/rpcn.png</iconset>
</property>
<property name="text">
@ -1513,6 +1514,16 @@
<string>Download Update</string>
</property>
</action>
<action name="addIsoGamesAct">
<property name="text">
<string>Add ISO Games</string>
</property>
</action>
<action name="bootIsoAct">
<property name="text">
<string>Boot ISO</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources>

View file

@ -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;

View file

@ -12,6 +12,7 @@
#include "Emu/system_utils.hpp"
#include "Utilities/File.h"
#include "Loader/ISO.h"
#include <cmath>
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<QPixmap> 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<std::string> 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<QPixmap>(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);

View file

@ -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 <typename T>
void stop_future_watcher(QFutureWatcher<T>& watcher, bool cancel, std::shared_ptr<atomic_t<bool>> cancel_flag = nullptr)
{

View file

@ -2,6 +2,8 @@
#include "Emu/System.h"
#include "qt_video_source.h"
#include "Loader/ISO.h"
#include <QFile>
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<QMovie>(m_video_path);
m_video_path.clear();
if (m_iso_path.empty())
{
m_movie = std::make_unique<QMovie>(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<QBuffer>(&m_video_data);
m_video_buffer->open(QIODevice::ReadOnly);
m_movie = std::make_unique<QMovie>(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();

View file

@ -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<bool> 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<u8> m_image_path;

View file

@ -7,6 +7,8 @@
#include "gui_settings.h"
#include "progress_dialog.h"
#include "Loader/ISO.h"
#include <QApplication>
#include <QClipboard>
#include <QScrollBar>
@ -208,7 +210,7 @@ savestate_manager_dialog::savestate_manager_dialog(std::shared_ptr<gui_settings>
m_savestate_table->create_header_actions(m_savestate_column_acts,
[this](int col) { return m_gui_settings->GetSavestateListColVisibility(static_cast<gui::savestate_list_columns>(col)); },
[this](int col, bool visible) { m_gui_settings->SetSavestateListColVisibility(static_cast<gui::savestate_list_columns>(col), visible); });
m_game_table->create_header_actions(m_game_column_acts,
[this](int col) { return m_gui_settings->GetSavestateGamelistColVisibility(static_cast<gui::savestate_game_list_columns>(col)); },
[this](int col, bool visible) { m_gui_settings->SetSavestateGamelistColVisibility(static_cast<gui::savestate_game_list_columns>(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;
}
}

View file

@ -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_data>&& game_savestates);

View file

@ -4,6 +4,7 @@
#include "Emu/VFS.h"
#include "Utilities/File.h"
#include "Utilities/StrUtil.h"
#include "Loader/ISO.h"
#ifdef _WIN32
#include <Windows.h>
@ -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;

View file

@ -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,

View file

@ -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);
}

View file

@ -2,6 +2,7 @@
#include <QSyntaxHighlighter>
#include <QRegularExpression>
#include <QBrush>
// 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;
};

View file

@ -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())
{

View file

@ -83,12 +83,12 @@ welcome_dialog::welcome_dialog(std::shared_ptr<gui_settings> 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

View file

@ -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<const AVSampleFormat*>(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<const int*>(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<const AVChannelLayout*>(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);