mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-20 02:53:57 +00:00
feat: Add ZIP firmware installation and update Android VVL to 1.4.328.1
Firmware Installation: - Add OnInstallFirmwareFromZip() to install firmware from ZIP archives - Implement ExtractZipToDirectory() with libarchive (primary) and PowerShell fallback (Windows) - Add user dialog to choose between folder or ZIP installation - Validate NCA files at ZIP root before installation - Automatic cleanup of temporary extraction directory Android Vulkan Validation Layers: - Update from sdk-1.3.261.1 to vulkan-sdk-1.4.328.1 - Fix extraction path for new VVL archive structure - Add file existence checks and improved error messages Benefits: - Users can install firmware directly from ZIP files - No manual extraction required - Better debugging on newer Android devices Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
@@ -98,21 +98,32 @@ endif()
|
||||
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
|
||||
|
||||
if (ANDROID AND CITRON_DOWNLOAD_ANDROID_VVL)
|
||||
set(vvl_version "sdk-1.3.261.1")
|
||||
set(vvl_version "vulkan-sdk-1.4.328.1")
|
||||
set(vvl_version_plain "1.4.328.1")
|
||||
set(vvl_zip_file "${CMAKE_BINARY_DIR}/externals/vvl-android.zip")
|
||||
set(vvl_extract_dir "${CMAKE_BINARY_DIR}/externals")
|
||||
|
||||
if (NOT EXISTS "${vvl_zip_file}")
|
||||
# Download and extract validation layer release to externals directory
|
||||
set(vvl_base_url "https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download")
|
||||
file(DOWNLOAD "${vvl_base_url}/${vvl_version}/android-binaries-${vvl_version}-android.zip"
|
||||
file(DOWNLOAD "${vvl_base_url}/${vvl_version}/android-binaries-${vvl_version_plain}.zip"
|
||||
"${vvl_zip_file}" SHOW_PROGRESS)
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${vvl_zip_file}"
|
||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals")
|
||||
WORKING_DIRECTORY "${vvl_extract_dir}")
|
||||
endif()
|
||||
|
||||
# Copy the arm64 binary to src/android/app/main/jniLibs
|
||||
# New structure: android-binaries-X.X.X.X/android-binaries-X.X.X.X/arm64-v8a/libVkLayer_khronos_validation.so
|
||||
set(vvl_lib_path "${CMAKE_CURRENT_SOURCE_DIR}/src/android/app/src/main/jniLibs/arm64-v8a/")
|
||||
file(COPY "${CMAKE_BINARY_DIR}/externals/android-binaries-${vvl_version}/arm64-v8a/libVkLayer_khronos_validation.so"
|
||||
DESTINATION "${vvl_lib_path}")
|
||||
set(vvl_source_file "${vvl_extract_dir}/android-binaries-${vvl_version_plain}/android-binaries-${vvl_version_plain}/arm64-v8a/libVkLayer_khronos_validation.so")
|
||||
|
||||
if (EXISTS "${vvl_source_file}")
|
||||
file(COPY "${vvl_source_file}" DESTINATION "${vvl_lib_path}")
|
||||
message(STATUS "Successfully copied VVL ${vvl_version_plain} to ${vvl_lib_path}")
|
||||
else()
|
||||
message(WARNING "Could not find Vulkan Validation Layer at: ${vvl_source_file}")
|
||||
message(STATUS "Please manually download from: ${vvl_base_url}/${vvl_version}/android-binaries-${vvl_version_plain}.zip")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (ANDROID)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cinttypes>
|
||||
#include <clocale>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
@@ -22,6 +23,11 @@
|
||||
#include "common/linux/gamemode.h"
|
||||
#endif
|
||||
|
||||
#ifdef CITRON_ENABLE_LIBARCHIVE
|
||||
#include <archive.h>
|
||||
#include <archive_entry.h>
|
||||
#endif
|
||||
|
||||
#include <boost/container/flat_set.hpp>
|
||||
|
||||
// VFS includes must be before glad as they will conflict with Windows file api, which uses defines.
|
||||
@@ -4187,6 +4193,255 @@ void GMainWindow::OnVerifyInstalledContents() {
|
||||
}
|
||||
}
|
||||
|
||||
bool GMainWindow::ExtractZipToDirectory(const std::filesystem::path& zip_path, const std::filesystem::path& extract_path) {
|
||||
#ifdef CITRON_ENABLE_LIBARCHIVE
|
||||
// Use libarchive if available (similar to updater code)
|
||||
struct archive* a = archive_read_new();
|
||||
struct archive* ext = archive_write_disk_new();
|
||||
struct archive_entry* entry;
|
||||
int r;
|
||||
|
||||
if (!a || !ext) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configure archive reader for zip
|
||||
archive_read_support_format_zip(a);
|
||||
archive_read_support_filter_all(a);
|
||||
|
||||
// Configure archive writer
|
||||
archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM);
|
||||
archive_write_disk_set_standard_lookup(ext);
|
||||
|
||||
r = archive_read_open_filename(a, zip_path.string().c_str(), 10240);
|
||||
if (r != ARCHIVE_OK) {
|
||||
archive_read_free(a);
|
||||
archive_write_free(ext);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create extraction directory
|
||||
std::filesystem::create_directories(extract_path);
|
||||
|
||||
// Extract files
|
||||
while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
|
||||
// Set the extraction path
|
||||
std::filesystem::path entry_path = extract_path / archive_entry_pathname(entry);
|
||||
archive_entry_set_pathname(entry, entry_path.string().c_str());
|
||||
|
||||
r = archive_write_header(ext, entry);
|
||||
if (r != ARCHIVE_OK) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (archive_entry_size(entry) > 0) {
|
||||
const void* buff;
|
||||
size_t size;
|
||||
la_int64_t offset;
|
||||
|
||||
while (archive_read_data_block(a, &buff, &size, &offset) == ARCHIVE_OK) {
|
||||
if (archive_write_data_block(ext, buff, size, offset) != ARCHIVE_OK) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
archive_write_finish_entry(ext);
|
||||
}
|
||||
|
||||
archive_read_free(a);
|
||||
archive_write_free(ext);
|
||||
return true;
|
||||
#else
|
||||
#ifdef _WIN32
|
||||
// Windows fallback: use PowerShell Expand-Archive
|
||||
std::filesystem::create_directories(extract_path);
|
||||
|
||||
std::string powershell_cmd = "powershell -NoProfile -NonInteractive -Command \"Expand-Archive -Path \\\"" +
|
||||
zip_path.string() + "\\\" -DestinationPath \\\"" +
|
||||
extract_path.string() + "\\\" -Force\"";
|
||||
|
||||
LOG_INFO(Frontend, "Extracting firmware ZIP with PowerShell: {}", powershell_cmd);
|
||||
|
||||
int result = std::system(powershell_cmd.c_str());
|
||||
if (result == 0) {
|
||||
LOG_INFO(Frontend, "Firmware ZIP extracted successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_ERROR(Frontend, "Failed to extract firmware ZIP file");
|
||||
return false;
|
||||
#else
|
||||
// On other platforms, require libarchive
|
||||
LOG_ERROR(Frontend, "ZIP extraction requires libarchive on this platform");
|
||||
(void)zip_path;
|
||||
(void)extract_path;
|
||||
return false;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void GMainWindow::OnInstallFirmwareFromZip() {
|
||||
// Don't do this while emulation is running, that'd probably be a bad idea.
|
||||
if (emu_thread != nullptr && emu_thread->IsRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for installed keys, error out, suggest restart?
|
||||
if (!ContentManager::AreKeysPresent()) {
|
||||
QMessageBox::information(
|
||||
this, tr("Keys not installed"),
|
||||
tr("Install decryption keys and restart citron before attempting to install firmware."));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString firmware_zip_location = QFileDialog::getOpenFileName(
|
||||
this, tr("Select Firmware ZIP File"), {}, QStringLiteral("ZIP Files (*.zip)"));
|
||||
if (firmware_zip_location.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QProgressDialog progress(tr("Installing Firmware..."), tr("Cancel"), 0, 100, this);
|
||||
progress.setWindowModality(Qt::WindowModal);
|
||||
progress.setMinimumDuration(100);
|
||||
progress.setAutoClose(false);
|
||||
progress.setAutoReset(false);
|
||||
progress.show();
|
||||
|
||||
// Declare progress callback.
|
||||
auto QtProgressCallback = [&](size_t total_size, size_t processed_size) {
|
||||
progress.setValue(static_cast<int>((processed_size * 100) / total_size));
|
||||
return progress.wasCanceled();
|
||||
};
|
||||
|
||||
LOG_INFO(Frontend, "Installing firmware from ZIP: {}", firmware_zip_location.toStdString());
|
||||
|
||||
QtProgressCallback(100, 5);
|
||||
|
||||
// Create temporary extraction directory
|
||||
std::filesystem::path temp_extract_path = std::filesystem::temp_directory_path() / "citron_firmware_temp";
|
||||
|
||||
// Clean up any existing temp directory
|
||||
if (std::filesystem::exists(temp_extract_path)) {
|
||||
std::filesystem::remove_all(temp_extract_path);
|
||||
}
|
||||
|
||||
progress.setLabelText(tr("Extracting firmware ZIP..."));
|
||||
QtProgressCallback(100, 10);
|
||||
|
||||
// Extract the ZIP file
|
||||
if (!ExtractZipToDirectory(firmware_zip_location.toStdString(), temp_extract_path)) {
|
||||
progress.close();
|
||||
std::filesystem::remove_all(temp_extract_path);
|
||||
QMessageBox::critical(this, tr("Firmware install failed"),
|
||||
tr("Failed to extract firmware ZIP file. Make sure the file is a valid ZIP archive."));
|
||||
return;
|
||||
}
|
||||
|
||||
QtProgressCallback(100, 15);
|
||||
|
||||
// Check for .nca files in the extracted directory
|
||||
std::vector<std::filesystem::path> out;
|
||||
const Common::FS::DirEntryCallable callback =
|
||||
[&out](const std::filesystem::directory_entry& entry) {
|
||||
if (entry.path().has_extension() && entry.path().extension() == ".nca") {
|
||||
out.emplace_back(entry.path());
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
Common::FS::IterateDirEntries(temp_extract_path, callback, Common::FS::DirEntryFilter::File);
|
||||
|
||||
if (out.size() <= 0) {
|
||||
progress.close();
|
||||
std::filesystem::remove_all(temp_extract_path);
|
||||
QMessageBox::warning(this, tr("Firmware install failed"),
|
||||
tr("Unable to locate firmware NCA files in the ZIP. Make sure the NCA files are at the root of the ZIP archive."));
|
||||
return;
|
||||
}
|
||||
|
||||
QtProgressCallback(100, 20);
|
||||
|
||||
// Locate and erase the content of nand/system/Content/registered/*.nca, if any.
|
||||
auto sysnand_content_vdir = system->GetFileSystemController().GetSystemNANDContentDirectory();
|
||||
if (!sysnand_content_vdir->CleanSubdirectoryRecursive("registered")) {
|
||||
progress.close();
|
||||
std::filesystem::remove_all(temp_extract_path);
|
||||
QMessageBox::critical(this, tr("Firmware install failed"),
|
||||
tr("Failed to delete one or more firmware file."));
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO(Frontend,
|
||||
"Cleaned nand/system/Content/registered folder in preparation for new firmware.");
|
||||
|
||||
QtProgressCallback(100, 25);
|
||||
|
||||
auto firmware_vdir = sysnand_content_vdir->GetDirectoryRelative("registered");
|
||||
|
||||
bool success = true;
|
||||
int i = 0;
|
||||
for (const auto& firmware_src_path : out) {
|
||||
i++;
|
||||
auto firmware_src_vfile =
|
||||
vfs->OpenFile(firmware_src_path.generic_string(), FileSys::OpenMode::Read);
|
||||
auto firmware_dst_vfile =
|
||||
firmware_vdir->CreateFileRelative(firmware_src_path.filename().string());
|
||||
|
||||
if (!VfsRawCopy(firmware_src_vfile, firmware_dst_vfile)) {
|
||||
LOG_ERROR(Frontend, "Failed to copy firmware file {} to {} in registered folder!",
|
||||
firmware_src_path.generic_string(), firmware_src_path.filename().string());
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (QtProgressCallback(
|
||||
100, 25 + static_cast<int>(((i) / static_cast<float>(out.size())) * 60.0))) {
|
||||
progress.close();
|
||||
std::filesystem::remove_all(temp_extract_path);
|
||||
QMessageBox::warning(
|
||||
this, tr("Firmware install failed"),
|
||||
tr("Firmware installation cancelled, firmware may be in bad state, "
|
||||
"restart citron or re-install firmware."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up temporary directory
|
||||
std::filesystem::remove_all(temp_extract_path);
|
||||
|
||||
if (!success) {
|
||||
progress.close();
|
||||
QMessageBox::critical(this, tr("Firmware install failed"),
|
||||
tr("One or more firmware files failed to copy into NAND."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-scan VFS for the newly placed firmware files.
|
||||
system->GetFileSystemController().CreateFactories(*vfs);
|
||||
|
||||
auto VerifyFirmwareCallback = [&](size_t total_size, size_t processed_size) {
|
||||
progress.setValue(85 + static_cast<int>((processed_size * 15) / total_size));
|
||||
return progress.wasCanceled();
|
||||
};
|
||||
|
||||
auto result =
|
||||
ContentManager::VerifyInstalledContents(*system, *provider, VerifyFirmwareCallback, true);
|
||||
|
||||
if (result.size() > 0) {
|
||||
const auto failed_names =
|
||||
QString::fromStdString(fmt::format("{}", fmt::join(result, "\n")));
|
||||
progress.close();
|
||||
QMessageBox::critical(
|
||||
this, tr("Firmware integrity verification failed!"),
|
||||
tr("Verification failed for the following files:\n\n%1").arg(failed_names));
|
||||
return;
|
||||
}
|
||||
|
||||
progress.close();
|
||||
QMessageBox::information(this, tr("Firmware installed successfully"),
|
||||
tr("The firmware has been installed successfully."));
|
||||
OnCheckFirmwareDecryption();
|
||||
}
|
||||
|
||||
void GMainWindow::OnInstallFirmware() {
|
||||
// Don't do this while emulation is running, that'd probably be a bad idea.
|
||||
if (emu_thread != nullptr && emu_thread->IsRunning()) {
|
||||
@@ -4201,6 +4456,31 @@ void GMainWindow::OnInstallFirmware() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask user to choose between folder or ZIP file
|
||||
QMessageBox msgBox(this);
|
||||
msgBox.setWindowTitle(tr("Install Firmware"));
|
||||
msgBox.setText(tr("Choose firmware installation method:"));
|
||||
msgBox.setInformativeText(tr("Select a folder containing NCA files, or select a ZIP archive."));
|
||||
QPushButton* folderButton = msgBox.addButton(tr("Select Folder"), QMessageBox::ActionRole);
|
||||
QPushButton* zipButton = msgBox.addButton(tr("Select ZIP File"), QMessageBox::ActionRole);
|
||||
QPushButton* cancelButton = msgBox.addButton(QMessageBox::Cancel);
|
||||
|
||||
msgBox.setDefaultButton(zipButton);
|
||||
msgBox.exec();
|
||||
|
||||
if (msgBox.clickedButton() == cancelButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (msgBox.clickedButton() == zipButton) {
|
||||
OnInstallFirmwareFromZip();
|
||||
return;
|
||||
}
|
||||
|
||||
// User clicked folder button - continue with folder selection (original implementation)
|
||||
if (msgBox.clickedButton() != folderButton) {
|
||||
return;
|
||||
}
|
||||
const QString firmware_source_location = QFileDialog::getExistingDirectory(
|
||||
this, tr("Select Dumped Firmware Source Location"), {}, QFileDialog::ShowDirsOnly);
|
||||
if (firmware_source_location.isEmpty()) {
|
||||
|
||||
@@ -395,6 +395,8 @@ private slots:
|
||||
void OnOpenCitronFolder();
|
||||
void OnVerifyInstalledContents();
|
||||
void OnInstallFirmware();
|
||||
void OnInstallFirmwareFromZip();
|
||||
bool ExtractZipToDirectory(const std::filesystem::path& zip_path, const std::filesystem::path& extract_path);
|
||||
void OnInstallDecryptionKeys();
|
||||
void OnAbout();
|
||||
void OnCheckForUpdates();
|
||||
|
||||
Reference in New Issue
Block a user