early-access version 3860

This commit is contained in:
pineappleEA 2023-09-08 17:22:41 +02:00
parent 3c1016ab09
commit f8ca381d80
22 changed files with 306 additions and 80 deletions

View File

@ -1,7 +1,7 @@
yuzu emulator early access yuzu emulator early access
============= =============
This is the source code for early-access 3859. This is the source code for early-access 3860.
## Legal Notice ## Legal Notice

View File

@ -88,8 +88,13 @@ MailboxMessage AudioRenderer::Receive(Direction dir, bool block) {
return mailbox.Receive(dir, block); return mailbox.Receive(dir, block);
} }
void AudioRenderer::SetCommandBuffer(s32 session_id, CommandBuffer& buffer) noexcept { void AudioRenderer::SetCommandBuffer(s32 session_id, CpuAddr buffer, u64 size, u64 time_limit,
command_buffers[session_id] = buffer; u64 applet_resource_user_id, bool reset) noexcept {
command_buffers[session_id].buffer = buffer;
command_buffers[session_id].size = size;
command_buffers[session_id].time_limit = time_limit;
command_buffers[session_id].applet_resource_user_id = applet_resource_user_id;
command_buffers[session_id].reset_buffer = reset;
} }
u32 AudioRenderer::GetRemainCommandCount(s32 session_id) const noexcept { u32 AudioRenderer::GetRemainCommandCount(s32 session_id) const noexcept {

View File

@ -75,7 +75,8 @@ public:
void Send(Direction dir, MailboxMessage message); void Send(Direction dir, MailboxMessage message);
MailboxMessage Receive(Direction dir, bool block = true); MailboxMessage Receive(Direction dir, bool block = true);
void SetCommandBuffer(s32 session_id, CommandBuffer& buffer) noexcept; void SetCommandBuffer(s32 session_id, CpuAddr buffer, u64 size, u64 time_limit,
u64 applet_resource_user_id, bool reset) noexcept;
u32 GetRemainCommandCount(s32 session_id) const noexcept; u32 GetRemainCommandCount(s32 session_id) const noexcept;
void ClearRemainCommandCount(s32 session_id) noexcept; void ClearRemainCommandCount(s32 session_id) noexcept;
u64 GetRenderingStartTick(s32 session_id) const noexcept; u64 GetRenderingStartTick(s32 session_id) const noexcept;

View File

@ -37,11 +37,6 @@ u32 CommandListProcessor::GetRemainingCommandCount() const {
return command_count - processed_command_count; return command_count - processed_command_count;
} }
void CommandListProcessor::SetBuffer(const CpuAddr buffer, const u64 size) {
commands = reinterpret_cast<u8*>(buffer + sizeof(Renderer::CommandListHeader));
commands_buffer_size = size;
}
Sink::SinkStream* CommandListProcessor::GetOutputSinkStream() const { Sink::SinkStream* CommandListProcessor::GetOutputSinkStream() const {
return stream; return stream;
} }

View File

@ -56,14 +56,6 @@ public:
*/ */
u32 GetRemainingCommandCount() const; u32 GetRemainingCommandCount() const;
/**
* Set the command buffer.
*
* @param buffer - The buffer to use.
* @param size - The size of the buffer.
*/
void SetBuffer(CpuAddr buffer, u64 size);
/** /**
* Get the stream for this command list. * Get the stream for this command list.
* *

View File

@ -20,6 +20,12 @@ void AdpcmDataSourceVersion1Command::Process(const AudioRenderer::CommandListPro
auto out_buffer{processor.mix_buffers.subspan(output_index * processor.sample_count, auto out_buffer{processor.mix_buffers.subspan(output_index * processor.sample_count,
processor.sample_count)}; processor.sample_count)};
for (auto& wave_buffer : wave_buffers) {
wave_buffer.loop_start_offset = wave_buffer.start_offset;
wave_buffer.loop_end_offset = wave_buffer.end_offset;
wave_buffer.loop_count = -(wave_buffer.loop & 1);
}
DecodeFromWaveBuffersArgs args{ DecodeFromWaveBuffersArgs args{
.sample_format{SampleFormat::Adpcm}, .sample_format{SampleFormat::Adpcm},
.output{out_buffer}, .output{out_buffer},

View File

@ -123,11 +123,13 @@ static u32 DecodeAdpcm(Core::Memory::Memory& memory, std::span<s16> out_buffer,
return 0; return 0;
} }
auto samples_to_process{ auto start_pos{req.start_offset + req.offset};
std::min(req.end_offset - req.start_offset - req.offset, req.samples_to_read)}; auto samples_to_process{std::min(req.end_offset - start_pos, req.samples_to_read)};
if (samples_to_process == 0) {
return 0;
}
auto samples_to_read{samples_to_process}; auto samples_to_read{samples_to_process};
auto start_pos{req.start_offset + req.offset};
auto samples_remaining_in_frame{start_pos % SamplesPerFrame}; auto samples_remaining_in_frame{start_pos % SamplesPerFrame};
auto position_in_frame{(start_pos / SamplesPerFrame) * NibblesPerFrame + auto position_in_frame{(start_pos / SamplesPerFrame) * NibblesPerFrame +
samples_remaining_in_frame}; samples_remaining_in_frame};
@ -225,13 +227,24 @@ static u32 DecodeAdpcm(Core::Memory::Memory& memory, std::span<s16> out_buffer,
* @param args - The wavebuffer data, and information for how to decode it. * @param args - The wavebuffer data, and information for how to decode it.
*/ */
void DecodeFromWaveBuffers(Core::Memory::Memory& memory, const DecodeFromWaveBuffersArgs& args) { void DecodeFromWaveBuffers(Core::Memory::Memory& memory, const DecodeFromWaveBuffersArgs& args) {
static constexpr auto EndWaveBuffer = [](auto& voice_state, auto& wavebuffer, auto& index,
auto& played_samples, auto& consumed) -> void {
voice_state.wave_buffer_valid[index] = false;
voice_state.loop_count = 0;
if (wavebuffer.stream_ended) {
played_samples = 0;
}
index = (index + 1) % MaxWaveBuffers;
consumed++;
};
auto& voice_state{*args.voice_state}; auto& voice_state{*args.voice_state};
auto remaining_sample_count{args.sample_count}; auto remaining_sample_count{args.sample_count};
auto fraction{voice_state.fraction}; auto fraction{voice_state.fraction};
const auto sample_rate_ratio{ const auto sample_rate_ratio{Common::FixedPoint<49, 15>(
(Common::FixedPoint<49, 15>(args.source_sample_rate) / args.target_sample_rate) * (f32)args.source_sample_rate / (f32)args.target_sample_rate * (f32)args.pitch)};
args.pitch};
const auto size_required{fraction + remaining_sample_count * sample_rate_ratio}; const auto size_required{fraction + remaining_sample_count * sample_rate_ratio};
if (size_required < 0) { if (size_required < 0) {
@ -298,22 +311,23 @@ void DecodeFromWaveBuffers(Core::Memory::Memory& memory, const DecodeFromWaveBuf
auto end_offset{wavebuffer.end_offset}; auto end_offset{wavebuffer.end_offset};
if (wavebuffer.loop && voice_state.loop_count > 0 && if (wavebuffer.loop && voice_state.loop_count > 0 &&
wavebuffer.loop_start_offset != 0 && wavebuffer.loop_end_offset != 0 &&
wavebuffer.loop_start_offset <= wavebuffer.loop_end_offset) { wavebuffer.loop_start_offset <= wavebuffer.loop_end_offset) {
start_offset = wavebuffer.loop_start_offset; start_offset = wavebuffer.loop_start_offset;
end_offset = wavebuffer.loop_end_offset; end_offset = wavebuffer.loop_end_offset;
} }
DecodeArg decode_arg{.buffer{wavebuffer.buffer}, DecodeArg decode_arg{
.buffer_size{wavebuffer.buffer_size}, .buffer{wavebuffer.buffer},
.start_offset{start_offset}, .buffer_size{wavebuffer.buffer_size},
.end_offset{end_offset}, .start_offset{start_offset},
.channel_count{args.channel_count}, .end_offset{end_offset},
.coefficients{}, .channel_count{args.channel_count},
.adpcm_context{nullptr}, .coefficients{},
.target_channel{args.channel}, .adpcm_context{nullptr},
.offset{offset}, .target_channel{args.channel},
.samples_to_read{samples_to_read - samples_read}}; .offset{offset},
.samples_to_read{samples_to_read - samples_read},
};
s32 samples_decoded{0}; s32 samples_decoded{0};
@ -350,42 +364,30 @@ void DecodeFromWaveBuffers(Core::Memory::Memory& memory, const DecodeFromWaveBuf
temp_buffer_pos += samples_decoded; temp_buffer_pos += samples_decoded;
offset += samples_decoded; offset += samples_decoded;
if (samples_decoded == 0 || offset >= end_offset - start_offset) { if (samples_decoded && offset < end_offset - start_offset) {
offset = 0; continue;
if (!wavebuffer.loop) { }
voice_state.wave_buffer_valid[wavebuffer_index] = false;
voice_state.loop_count = 0;
if (wavebuffer.stream_ended) { offset = 0;
played_sample_count = 0; if (wavebuffer.loop) {
} voice_state.loop_count++;
if (wavebuffer.loop_count >= 0 &&
wavebuffer_index = (wavebuffer_index + 1) % MaxWaveBuffers; (voice_state.loop_count > wavebuffer.loop_count || samples_decoded == 0)) {
wavebuffers_consumed++; EndWaveBuffer(voice_state, wavebuffer, wavebuffer_index, played_sample_count,
} else { wavebuffers_consumed);
voice_state.loop_count++;
if (wavebuffer.loop_count >= 0 &&
(voice_state.loop_count > wavebuffer.loop_count || samples_decoded == 0)) {
voice_state.wave_buffer_valid[wavebuffer_index] = false;
voice_state.loop_count = 0;
if (wavebuffer.stream_ended) {
played_sample_count = 0;
}
wavebuffer_index = (wavebuffer_index + 1) % MaxWaveBuffers;
wavebuffers_consumed++;
}
if (samples_decoded == 0) {
is_buffer_starved = true;
break;
}
if (args.IsVoicePlayedSampleCountResetAtLoopPointSupported) {
played_sample_count = 0;
}
} }
if (samples_decoded == 0) {
is_buffer_starved = true;
break;
}
if (args.IsVoicePlayedSampleCountResetAtLoopPointSupported) {
played_sample_count = 0;
}
} else {
EndWaveBuffer(voice_state, wavebuffer, wavebuffer_index, played_sample_count,
wavebuffers_consumed);
} }
} }

View File

@ -21,6 +21,12 @@ void PcmFloatDataSourceVersion1Command::Process(
auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count, auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count,
processor.sample_count); processor.sample_count);
for (auto& wave_buffer : wave_buffers) {
wave_buffer.loop_start_offset = wave_buffer.start_offset;
wave_buffer.loop_end_offset = wave_buffer.end_offset;
wave_buffer.loop_count = -(wave_buffer.loop & 1);
}
DecodeFromWaveBuffersArgs args{ DecodeFromWaveBuffersArgs args{
.sample_format{SampleFormat::PcmFloat}, .sample_format{SampleFormat::PcmFloat},
.output{out_buffer}, .output{out_buffer},

View File

@ -23,6 +23,12 @@ void PcmInt16DataSourceVersion1Command::Process(
auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count, auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count,
processor.sample_count); processor.sample_count);
for (auto& wave_buffer : wave_buffers) {
wave_buffer.loop_start_offset = wave_buffer.start_offset;
wave_buffer.loop_end_offset = wave_buffer.end_offset;
wave_buffer.loop_count = -(wave_buffer.loop & 1);
}
DecodeFromWaveBuffersArgs args{ DecodeFromWaveBuffersArgs args{
.sample_format{SampleFormat::PcmInt16}, .sample_format{SampleFormat::PcmInt16},
.output{out_buffer}, .output{out_buffer},

View File

@ -609,17 +609,11 @@ void System::SendCommandToDsp() {
time_limit_percent = 70.0f; time_limit_percent = 70.0f;
} }
AudioRenderer::CommandBuffer command_buffer{ auto time_limit{
.buffer{translated_addr}, static_cast<u64>((time_limit_percent / 100) * 2'880'000.0 *
.size{command_size}, (static_cast<f32>(render_time_limit_percent) / 100.0f))};
.time_limit{ audio_renderer.SetCommandBuffer(session_id, translated_addr, command_size, time_limit,
static_cast<u64>((time_limit_percent / 100) * 2'880'000.0 * applet_resource_user_id, reset_command_buffers);
(static_cast<f32>(render_time_limit_percent) / 100.0f))},
.applet_resource_user_id{applet_resource_user_id},
.reset_buffer{reset_command_buffers},
};
audio_renderer.SetCommandBuffer(session_id, command_buffer);
reset_command_buffers = false; reset_command_buffers = false;
command_buffer_size = command_size; command_buffer_size = command_size;
if (remaining_command_count == 0) { if (remaining_command_count == 0) {

View File

@ -108,7 +108,7 @@ std::string GetFileTypeString(FileType type) {
return "unknown"; return "unknown";
} }
constexpr std::array<const char*, 66> RESULT_MESSAGES{ constexpr std::array<const char*, 68> RESULT_MESSAGES{
"The operation completed successfully.", "The operation completed successfully.",
"The loader requested to load is already loaded.", "The loader requested to load is already loaded.",
"The operation is not implemented.", "The operation is not implemented.",
@ -175,6 +175,8 @@ constexpr std::array<const char*, 66> RESULT_MESSAGES{
"The KIP BLZ decompression of the section failed unexpectedly.", "The KIP BLZ decompression of the section failed unexpectedly.",
"The INI file has a bad header.", "The INI file has a bad header.",
"The INI file contains more than the maximum allowable number of KIP files.", "The INI file contains more than the maximum allowable number of KIP files.",
"Integrity verification could not be performed for this file.",
"Integrity verification failed.",
}; };
std::string GetResultStatusString(ResultStatus status) { std::string GetResultStatusString(ResultStatus status) {

View File

@ -3,6 +3,7 @@
#pragma once #pragma once
#include <functional>
#include <iosfwd> #include <iosfwd>
#include <memory> #include <memory>
#include <optional> #include <optional>
@ -132,6 +133,8 @@ enum class ResultStatus : u16 {
ErrorBLZDecompressionFailed, ErrorBLZDecompressionFailed,
ErrorBadINIHeader, ErrorBadINIHeader,
ErrorINITooManyKIPs, ErrorINITooManyKIPs,
ErrorIntegrityVerificationNotImplemented,
ErrorIntegrityVerificationFailed,
}; };
std::string GetResultStatusString(ResultStatus status); std::string GetResultStatusString(ResultStatus status);
@ -169,6 +172,13 @@ public:
*/ */
virtual LoadResult Load(Kernel::KProcess& process, Core::System& system) = 0; virtual LoadResult Load(Kernel::KProcess& process, Core::System& system) = 0;
/**
* Try to verify the integrity of the file.
*/
virtual ResultStatus VerifyIntegrity(std::function<bool(size_t, size_t)> progress_callback) {
return ResultStatus::ErrorIntegrityVerificationNotImplemented;
}
/** /**
* Get the code (typically .code section) of the application * Get the code (typically .code section) of the application
* *

View File

@ -3,6 +3,8 @@
#include <utility> #include <utility>
#include "common/hex_util.h"
#include "common/scope_exit.h"
#include "core/core.h" #include "core/core.h"
#include "core/file_sys/content_archive.h" #include "core/file_sys/content_archive.h"
#include "core/file_sys/nca_metadata.h" #include "core/file_sys/nca_metadata.h"
@ -12,6 +14,7 @@
#include "core/hle/service/filesystem/filesystem.h" #include "core/hle/service/filesystem/filesystem.h"
#include "core/loader/deconstructed_rom_directory.h" #include "core/loader/deconstructed_rom_directory.h"
#include "core/loader/nca.h" #include "core/loader/nca.h"
#include "mbedtls/sha256.h"
namespace Loader { namespace Loader {
@ -80,6 +83,79 @@ AppLoader_NCA::LoadResult AppLoader_NCA::Load(Kernel::KProcess& process, Core::S
return load_result; return load_result;
} }
ResultStatus AppLoader_NCA::VerifyIntegrity(std::function<bool(size_t, size_t)> progress_callback) {
using namespace Common::Literals;
constexpr size_t NcaFileNameWithHashLength = 36;
constexpr size_t NcaFileNameHashLength = 32;
constexpr size_t NcaSha256HashLength = 32;
constexpr size_t NcaSha256HalfHashLength = NcaSha256HashLength / 2;
// Get the file name.
const auto name = file->GetName();
// We won't try to verify meta NCAs.
if (name.ends_with(".cnmt.nca")) {
return ResultStatus::Success;
}
// Check if we can verify this file. NCAs should be named after their hashes.
if (!name.ends_with(".nca") || name.size() != NcaFileNameWithHashLength) {
LOG_WARNING(Loader, "Unable to validate NCA with name {}", name);
return ResultStatus::ErrorIntegrityVerificationNotImplemented;
}
// Get the expected truncated hash of the NCA.
const auto input_hash =
Common::HexStringToVector(file->GetName().substr(0, NcaFileNameHashLength), false);
// Declare buffer to read into.
std::vector<u8> buffer(4_MiB);
// Initialize sha256 verification context.
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts_ret(&ctx, 0);
// Ensure we maintain a clean state on exit.
SCOPE_EXIT({ mbedtls_sha256_free(&ctx); });
// Declare counters.
const size_t total_size = file->GetSize();
size_t processed_size = 0;
// Begin iterating the file.
while (processed_size < total_size) {
// Refill the buffer.
const size_t intended_read_size = std::min(buffer.size(), total_size - processed_size);
const size_t read_size = file->Read(buffer.data(), intended_read_size, processed_size);
// Update the hash function with the buffer contents.
mbedtls_sha256_update_ret(&ctx, buffer.data(), read_size);
// Update counters.
processed_size += read_size;
// Call the progress function.
if (!progress_callback(processed_size, total_size)) {
return ResultStatus::ErrorIntegrityVerificationFailed;
}
}
// Finalize context and compute the output hash.
std::array<u8, NcaSha256HashLength> output_hash;
mbedtls_sha256_finish_ret(&ctx, output_hash.data());
// Compare to expected.
if (std::memcmp(input_hash.data(), output_hash.data(), NcaSha256HalfHashLength) != 0) {
LOG_ERROR(Loader, "NCA hash mismatch detected for file {}", name);
return ResultStatus::ErrorIntegrityVerificationFailed;
}
// File verified.
return ResultStatus::Success;
}
ResultStatus AppLoader_NCA::ReadRomFS(FileSys::VirtualFile& dir) { ResultStatus AppLoader_NCA::ReadRomFS(FileSys::VirtualFile& dir) {
if (nca == nullptr) { if (nca == nullptr) {
return ResultStatus::ErrorNotInitialized; return ResultStatus::ErrorNotInitialized;

View File

@ -39,6 +39,8 @@ public:
LoadResult Load(Kernel::KProcess& process, Core::System& system) override; LoadResult Load(Kernel::KProcess& process, Core::System& system) override;
ResultStatus VerifyIntegrity(std::function<bool(size_t, size_t)> progress_callback) override;
ResultStatus ReadRomFS(FileSys::VirtualFile& dir) override; ResultStatus ReadRomFS(FileSys::VirtualFile& dir) override;
ResultStatus ReadProgramId(u64& out_program_id) override; ResultStatus ReadProgramId(u64& out_program_id) override;

View File

@ -117,6 +117,42 @@ AppLoader_NSP::LoadResult AppLoader_NSP::Load(Kernel::KProcess& process, Core::S
return result; return result;
} }
ResultStatus AppLoader_NSP::VerifyIntegrity(std::function<bool(size_t, size_t)> progress_callback) {
// Extracted-type NSPs can't be verified.
if (nsp->IsExtractedType()) {
return ResultStatus::ErrorIntegrityVerificationNotImplemented;
}
// Get list of all NCAs.
const auto ncas = nsp->GetNCAsCollapsed();
size_t total_size = 0;
size_t processed_size = 0;
// Loop over NCAs, collecting the total size to verify.
for (const auto& nca : ncas) {
total_size += nca->GetBaseFile()->GetSize();
}
// Loop over NCAs again, verifying each.
for (const auto& nca : ncas) {
AppLoader_NCA loader_nca(nca->GetBaseFile());
const auto NcaProgressCallback = [&](size_t nca_processed_size, size_t nca_total_size) {
return progress_callback(processed_size + nca_processed_size, total_size);
};
const auto verification_result = loader_nca.VerifyIntegrity(NcaProgressCallback);
if (verification_result != ResultStatus::Success) {
return verification_result;
}
processed_size += nca->GetBaseFile()->GetSize();
}
return ResultStatus::Success;
}
ResultStatus AppLoader_NSP::ReadRomFS(FileSys::VirtualFile& out_file) { ResultStatus AppLoader_NSP::ReadRomFS(FileSys::VirtualFile& out_file) {
return secondary_loader->ReadRomFS(out_file); return secondary_loader->ReadRomFS(out_file);
} }

View File

@ -45,6 +45,8 @@ public:
LoadResult Load(Kernel::KProcess& process, Core::System& system) override; LoadResult Load(Kernel::KProcess& process, Core::System& system) override;
ResultStatus VerifyIntegrity(std::function<bool(size_t, size_t)> progress_callback) override;
ResultStatus ReadRomFS(FileSys::VirtualFile& out_file) override; ResultStatus ReadRomFS(FileSys::VirtualFile& out_file) override;
ResultStatus ReadUpdateRaw(FileSys::VirtualFile& out_file) override; ResultStatus ReadUpdateRaw(FileSys::VirtualFile& out_file) override;
ResultStatus ReadProgramId(u64& out_program_id) override; ResultStatus ReadProgramId(u64& out_program_id) override;

View File

@ -85,6 +85,40 @@ AppLoader_XCI::LoadResult AppLoader_XCI::Load(Kernel::KProcess& process, Core::S
return result; return result;
} }
ResultStatus AppLoader_XCI::VerifyIntegrity(std::function<bool(size_t, size_t)> progress_callback) {
// Verify secure partition, as it is the only thing we can process.
auto secure_partition = xci->GetSecurePartitionNSP();
// Get list of all NCAs.
const auto ncas = secure_partition->GetNCAsCollapsed();
size_t total_size = 0;
size_t processed_size = 0;
// Loop over NCAs, collecting the total size to verify.
for (const auto& nca : ncas) {
total_size += nca->GetBaseFile()->GetSize();
}
// Loop over NCAs again, verifying each.
for (const auto& nca : ncas) {
AppLoader_NCA loader_nca(nca->GetBaseFile());
const auto NcaProgressCallback = [&](size_t nca_processed_size, size_t nca_total_size) {
return progress_callback(processed_size + nca_processed_size, total_size);
};
const auto verification_result = loader_nca.VerifyIntegrity(NcaProgressCallback);
if (verification_result != ResultStatus::Success) {
return verification_result;
}
processed_size += nca->GetBaseFile()->GetSize();
}
return ResultStatus::Success;
}
ResultStatus AppLoader_XCI::ReadRomFS(FileSys::VirtualFile& out_file) { ResultStatus AppLoader_XCI::ReadRomFS(FileSys::VirtualFile& out_file) {
return nca_loader->ReadRomFS(out_file); return nca_loader->ReadRomFS(out_file);
} }

View File

@ -45,6 +45,8 @@ public:
LoadResult Load(Kernel::KProcess& process, Core::System& system) override; LoadResult Load(Kernel::KProcess& process, Core::System& system) override;
ResultStatus VerifyIntegrity(std::function<bool(size_t, size_t)> progress_callback) override;
ResultStatus ReadRomFS(FileSys::VirtualFile& out_file) override; ResultStatus ReadRomFS(FileSys::VirtualFile& out_file) override;
ResultStatus ReadUpdateRaw(FileSys::VirtualFile& out_file) override; ResultStatus ReadUpdateRaw(FileSys::VirtualFile& out_file) override;
ResultStatus ReadProgramId(u64& out_program_id) override; ResultStatus ReadProgramId(u64& out_program_id) override;

View File

@ -561,6 +561,7 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
QMenu* dump_romfs_menu = context_menu.addMenu(tr("Dump RomFS")); QMenu* dump_romfs_menu = context_menu.addMenu(tr("Dump RomFS"));
QAction* dump_romfs = dump_romfs_menu->addAction(tr("Dump RomFS")); QAction* dump_romfs = dump_romfs_menu->addAction(tr("Dump RomFS"));
QAction* dump_romfs_sdmc = dump_romfs_menu->addAction(tr("Dump RomFS to SDMC")); QAction* dump_romfs_sdmc = dump_romfs_menu->addAction(tr("Dump RomFS to SDMC"));
QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity"));
QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard")); QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard"));
QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry"));
#ifndef WIN32 #ifndef WIN32
@ -634,6 +635,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
connect(dump_romfs_sdmc, &QAction::triggered, [this, program_id, path]() { connect(dump_romfs_sdmc, &QAction::triggered, [this, program_id, path]() {
emit DumpRomFSRequested(program_id, path, DumpRomFSTarget::SDMC); emit DumpRomFSRequested(program_id, path, DumpRomFSTarget::SDMC);
}); });
connect(verify_integrity, &QAction::triggered,
[this, path]() { emit VerifyIntegrityRequested(path); });
connect(copy_tid, &QAction::triggered, connect(copy_tid, &QAction::triggered,
[this, program_id]() { emit CopyTIDRequested(program_id); }); [this, program_id]() { emit CopyTIDRequested(program_id); });
connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() {

View File

@ -117,6 +117,7 @@ signals:
const std::string& game_path); const std::string& game_path);
void RemovePlayTimeRequested(u64 program_id); void RemovePlayTimeRequested(u64 program_id);
void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target); void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
void VerifyIntegrityRequested(const std::string& game_path);
void CopyTIDRequested(u64 program_id); void CopyTIDRequested(u64 program_id);
void CreateShortcut(u64 program_id, const std::string& game_path, void CreateShortcut(u64 program_id, const std::string& game_path,
GameListShortcutTarget target); GameListShortcutTarget target);

View File

@ -1457,6 +1457,8 @@ void GMainWindow::ConnectWidgetEvents() {
connect(game_list, &GameList::RemovePlayTimeRequested, this, connect(game_list, &GameList::RemovePlayTimeRequested, this,
&GMainWindow::OnGameListRemovePlayTimeData); &GMainWindow::OnGameListRemovePlayTimeData);
connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS);
connect(game_list, &GameList::VerifyIntegrityRequested, this,
&GMainWindow::OnGameListVerifyIntegrity);
connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID); connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID);
connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
&GMainWindow::OnGameListNavigateToGamedbEntry); &GMainWindow::OnGameListNavigateToGamedbEntry);
@ -2729,6 +2731,54 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa
} }
} }
void GMainWindow::OnGameListVerifyIntegrity(const std::string& game_path) {
const auto NotImplemented = [this] {
QMessageBox::warning(this, tr("Integrity verification couldn't be performed!"),
tr("File contents were not checked for validity."));
};
const auto Failed = [this] {
QMessageBox::critical(this, tr("Integrity verification failed!"),
tr("File contents may be corrupt."));
};
const auto loader = Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::Mode::Read));
if (loader == nullptr) {
NotImplemented();
return;
}
QProgressDialog progress(tr("Verifying integrity..."), tr("Cancel"), 0, 100, this);
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(100);
progress.setAutoClose(false);
progress.setAutoReset(false);
const auto QtProgressCallback = [&](size_t processed_size, size_t total_size) {
if (progress.wasCanceled()) {
return false;
}
progress.setValue(static_cast<int>((processed_size * 100) / total_size));
return true;
};
const auto status = loader->VerifyIntegrity(QtProgressCallback);
if (progress.wasCanceled() ||
status == Loader::ResultStatus::ErrorIntegrityVerificationNotImplemented) {
NotImplemented();
return;
}
if (status == Loader::ResultStatus::ErrorIntegrityVerificationFailed) {
Failed();
return;
}
progress.close();
QMessageBox::information(this, tr("Integrity verification succeeded!"),
tr("The operation completed successfully."));
}
void GMainWindow::OnGameListCopyTID(u64 program_id) { void GMainWindow::OnGameListCopyTID(u64 program_id) {
QClipboard* clipboard = QGuiApplication::clipboard(); QClipboard* clipboard = QGuiApplication::clipboard();
clipboard->setText(QString::fromStdString(fmt::format("{:016X}", program_id))); clipboard->setText(QString::fromStdString(fmt::format("{:016X}", program_id)));

View File

@ -318,6 +318,7 @@ private slots:
const std::string& game_path); const std::string& game_path);
void OnGameListRemovePlayTimeData(u64 program_id); void OnGameListRemovePlayTimeData(u64 program_id);
void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target); void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
void OnGameListVerifyIntegrity(const std::string& game_path);
void OnGameListCopyTID(u64 program_id); void OnGameListCopyTID(u64 program_id);
void OnGameListNavigateToGamedbEntry(u64 program_id, void OnGameListNavigateToGamedbEntry(u64 program_id,
const CompatibilityList& compatibility_list); const CompatibilityList& compatibility_list);