diff --git a/VFS.cpp b/VFS.cpp new file mode 100644 index 0000000000..992a2cd4be --- /dev/null +++ b/VFS.cpp @@ -0,0 +1,1281 @@ +#include "stdafx.h" +#include "IdManager.h" +#include "System.h" +#include "VFS.h" + +#include "Cell/lv2/sys_fs.h" + +#include "Utilities/mutex.h" +#include "Utilities/StrUtil.h" + +#ifdef _WIN32 +#include +#endif + +#include +#include + +LOG_CHANNEL(vfs_log, "VFS"); + +struct vfs_directory +{ + // Real path (empty if root or not exists) + std::string path; + + // Virtual subdirectories + std::map> dirs; +}; + +struct vfs_manager +{ + shared_mutex mutex{}; + + // VFS root + vfs_directory root{}; +}; + +bool vfs::mount(std::string_view vpath, std::string_view path, bool is_dir) +{ + if (vpath.empty()) + { + // Empty relative path, should set relative path base; unsupported + vfs_log.error("Cannot mount empty path to \"%s\"", path); + return false; + } + + // Initialize vfs_manager if not yet initialized (e.g. g_fxo->reset() was previously invoked) + g_fxo->need(); + + auto& table = g_fxo->get(); + + // TODO: scan roots of mounted devices for undeleted vfs::host::unlink remnants, and try to delete them (_WIN32 only) + + std::lock_guard lock(table.mutex); + + const std::string_view vpath_backup = vpath; + + for (std::vector list{&table.root};;) + { + // Skip one or more '/' + const auto pos = vpath.find_first_not_of('/'); + + if (pos == 0) + { + // Mounting relative path is not supported + vfs_log.error("Cannot mount relative path \"%s\" to \"%s\"", vpath_backup, path); + return false; + } + + if (pos == umax) + { + // Mounting completed; fixup for directories due to resolve_path messing with trailing / + list.back()->path = Emu.GetCallbacks().resolve_path(path); + if (list.back()->path.empty()) + list.back()->path = std::string(path); // Fallback when resolving failed + if (is_dir && !list.back()->path.ends_with('/')) + list.back()->path += '/'; + if (!is_dir && list.back()->path.ends_with('/')) + vfs_log.error("File mounted with trailing /."); + + if (path == "/") // Special + list.back()->path = "/"; + + vfs_log.notice("Mounted path \"%s\" to \"%s\"", vpath_backup, list.back()->path); + return true; + } + + // Get fragment name + const auto name = vpath.substr(pos, vpath.find_first_of('/', pos) - pos); + vpath.remove_prefix(name.size() + pos); + + if (name == ".") + { + // Keep current + continue; + } + + if (name == "..") + { + // Root parent is root + if (list.size() == 1) + { + continue; + } + + // Go back one level + list.pop_back(); + continue; + } + + // Find or add + vfs_directory* last = list.back(); + + for (auto& [path, dir] : last->dirs) + { + if (path == name) + { + list.push_back(dir.get()); + break; + } + } + + if (last == list.back()) + { + // Add new entry + std::unique_ptr new_entry = std::make_unique(); + list.push_back(new_entry.get()); + last->dirs.emplace(name, std::move(new_entry)); + } + } +} + +bool vfs::unmount(std::string_view vpath) +{ + if (vpath.empty()) + { + vfs_log.error("Cannot unmount empty path"); + return false; + } + + const std::vector entry_list = fmt::split(vpath, {"/"}); + + if (entry_list.empty()) + { + vfs_log.error("Cannot unmount path: '%s'", vpath); + return false; + } + + vfs_log.notice("About to unmount '%s'", vpath); + + if (!g_fxo->is_init()) + { + return false; + } + + auto& table = g_fxo->get(); + + std::lock_guard lock(table.mutex); + + // Search entry recursively and remove it (including all children) + std::function unmount_children; + unmount_children = [&entry_list, &unmount_children](vfs_directory& dir, usz depth) -> void + { + if (depth >= entry_list.size()) + { + return; + } + + // Get the current name based on the depth + const std::string& name = ::at32(entry_list, depth); + + // Go through all children of this node + for (auto it = dir.dirs.begin(); it != dir.dirs.end();) + { + // Find the matching node + if (it->first == name) + { + // Remove the matching node if we reached the maximum depth + if (depth + 1 == entry_list.size()) + { + vfs_log.notice("Unmounting '%s' = '%s'", it->first, it->second->path); + it = dir.dirs.erase(it); + continue; + } + + // Otherwise continue searching in the next level of depth + unmount_children(*it->second, depth + 1); + } + + ++it; + } + }; + unmount_children(table.root, 0); + + return true; +} + +std::string vfs::get(std::string_view vpath, std::vector* out_dir, std::string* out_path) +{ + // Just to make the code more robust. + // It should never happen because we take care to initialize Emu (and so also vfs_manager) with Emu.Init() before this function is invoked + if (!g_fxo->is_init()) + { + fmt::throw_exception("vfs_manager not initialized"); + } + + auto& table = g_fxo->get(); + + reader_lock lock(table.mutex); + + // Resulting path fragments: decoded ones + std::vector result; + result.reserve(vpath.size() / 2); + + // Mounted path + std::string_view result_base; + + if (vpath.empty()) + { + // Empty relative path (reuse further return) + vpath = "."; + } + + // Fragments for out_path + std::vector name_list; + + if (out_path) + { + name_list.reserve(vpath.size() / 2); + } + + for (std::vector list{&table.root};;) + { + // Skip one or more '/' + const auto pos = vpath.find_first_not_of('/'); + + if (pos == 0) + { + // Relative path: point to non-existent location + return fs::get_config_dir() + "delete_this_dir.../delete_this..."; + } + + if (pos == umax) + { + // Absolute path: finalize + for (auto it = list.rbegin(), rend = list.rend(); it != rend; it++) + { + if (auto* dir = *it; dir && (!dir->path.empty() || list.size() == 1)) + { + // Save latest valid mount path + result_base = dir->path; + + // Erase unnecessary path fragments + result.erase(result.begin(), result.begin() + (std::distance(it, rend) - 1)); + + // Extract mounted subdirectories (TODO) + if (out_dir) + { + for (auto& pair : dir->dirs) + { + if (!pair.second->path.empty()) + { + out_dir->emplace_back(pair.first); + } + } + } + + break; + } + } + + if (!vpath.empty()) + { + // Finalize path with '/' + result.emplace_back(""); + } + + break; + } + + // Get fragment name + const auto name = vpath.substr(pos, vpath.find_first_of('/', pos) - pos); + vpath.remove_prefix(name.size() + pos); + + // Process special directories + if (name == ".") + { + // Keep current + continue; + } + + if (name == "..") + { + // Root parent is root + if (list.size() == 1) + { + continue; + } + + // Go back one level + if (out_path) + { + name_list.pop_back(); + } + + list.pop_back(); + result.pop_back(); + continue; + } + + const auto last = list.back(); + list.push_back(nullptr); + + if (out_path) + { + name_list.push_back(name); + } + + result.push_back(name); + + if (!last) + { + continue; + } + + for (auto& [path, dir] : last->dirs) + { + if (path == name) + { + list.back() = dir.get(); + + if (dir->path == "/"sv) + { + if (vpath.size() <= 1) + { + return fs::get_config_dir() + "delete_this_dir.../delete_this..."; + } + + // Handle /host_root (not escaped, not processed) + if (out_path) + { + out_path->clear(); + *out_path += '/'; + *out_path += fmt::merge(name_list, "/"); + *out_path += vpath; + } + + return std::string{vpath.substr(1)}; + } + + break; + } + } + } + + if (result_base.empty()) + { + // Not mounted + return {}; + } + + // Merge path fragments + if (out_path) + { + out_path->clear(); + *out_path += '/'; + *out_path += fmt::merge(name_list, "/"); + } + + // Escape for host FS + std::vector escaped; + escaped.reserve(result.size()); + for (auto& sv : result) + escaped.emplace_back(vfs::escape(sv)); + + return std::string{result_base} + fmt::merge(escaped, "/"); +} + +using char2 = char8_t; + +std::string vfs::retrieve(std::string_view path, const vfs_directory* node, std::vector* mount_path) +{ + // Just to make the code more robust. + // It should never happen because we take care to initialize Emu (and so also vfs_manager) with Emu.Init() before this function is invoked + if (!g_fxo->is_init()) + { + fmt::throw_exception("vfs_manager not initialized"); + } + + auto& table = g_fxo->get(); + + if (!node) + { + if (path.starts_with(".") || path.empty()) + { + return {}; + } + + reader_lock lock(table.mutex); + + std::vector mount_path_empty; + + const std::string rpath = Emu.GetCallbacks().resolve_path(path); + + if (!rpath.empty()) + { + if (std::string res = vfs::retrieve(rpath, &table.root, &mount_path_empty); !res.empty()) + { + return res; + } + } + + mount_path_empty.clear(); + + return vfs::retrieve(path, &table.root, &mount_path_empty); + } + + mount_path->emplace_back(); + + // Try to extract host root mount point name (if exists) + std::string_view host_root_name; + + std::string result; + std::string result_dir; + + for (const auto& [name, dir] : node->dirs) + { + mount_path->back() = name; + + if (std::string res = vfs::retrieve(path, dir.get(), mount_path); !res.empty()) + { + // Avoid app_home + // Prefer dev_bdvd over dev_hdd0 + if (result.empty() || (name == "app_home") < (result_dir == "app_home") || + (name == "dev_bdvd") > (result_dir == "dev_bdvd")) + { + result = std::move(res); + result_dir = name; + } + } + + if (dir->path == "/"sv) + { + host_root_name = name; + } + } + + if (!result.empty()) + { + return result; + } + + mount_path->pop_back(); + + if (node->path.size() > 1 && path.starts_with(node->path)) + { + auto unescape_path = [](std::string_view path) + { + // Unescape from host FS + std::vector escaped = fmt::split(path, {std::string_view{&fs::delim[0], 1}, std::string_view{&fs::delim[1], 1}}); + std::vector result; + for (auto& sv : escaped) + result.emplace_back(vfs::unescape(sv)); + + return fmt::merge(result, "/"); + }; + + std::string result{"/"}; + + for (const auto& name : *mount_path) + { + result += name; + result += '/'; + } + + result += unescape_path(path.substr(node->path.size())); + return result; + } + + if (!host_root_name.empty()) + { + // If failed to find mount point for path and /host_root is mounted + // Prepend "/host_root" to path and return the constructed string + result.clear(); + result += '/'; + + for (const auto& name : *mount_path) + { + result += name; + result += '/'; + } + + result += host_root_name; + result += '/'; + + result += path; + return result; + } + + return result; +} + +std::string vfs::escape(std::string_view name, bool escape_slash) +{ + std::string result; + + if (name.size() <= 2 && name.find_first_not_of('.') == umax) + { + // Return . or .. as is + result = name; + return result; + } + + // Emulate NTS (limited) + auto get_char = [&](usz pos) -> char2 + { + if (pos < name.size()) + { + return name[pos]; + } + else + { + return '\0'; + } + }; + + // Escape NUL, LPT ant other trash + if (name.size() > 2) + { + // Pack first 3 characters + const u32 triple = std::bit_cast, u32>(toupper(name[0]) | toupper(name[1]) << 8 | toupper(name[2]) << 16); + + switch (triple) + { + case "COM"_u32: + case "LPT"_u32: + { + if (name.size() >= 4 && name[3] >= '1' && name[3] <= '9') + { + if (name.size() == 4 || name[4] == '.') + { + // Escape first character (C or L) + result = reinterpret_cast(u8"!"); + } + } + + break; + } + case "NUL"_u32: + case "CON"_u32: + case "AUX"_u32: + case "PRN"_u32: + { + if (name.size() == 3 || name[3] == '.') + { + result = reinterpret_cast(u8"!"); + } + + break; + } + } + } + + result.reserve(result.size() + name.size()); + + for (usz i = 0, s = name.size(); i < s; i++) + { + switch (char2 c = name[i]) + { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + { + result += reinterpret_cast(u8"0"); + result.back() += c; + break; + } + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + case 18: + case 19: + case 20: + case 21: + case 22: + case 23: + case 24: + case 25: + case 26: + case 27: + case 28: + case 29: + case 30: + case 31: + { + result += reinterpret_cast(u8"A"); + result.back() += c; + result.back() -= 10; + break; + } + case '<': + { + result += reinterpret_cast(u8"<"); + break; + } + case '>': + { + result += reinterpret_cast(u8">"); + break; + } + case ':': + { + result += reinterpret_cast(u8":"); + break; + } + case '"': + { + result += reinterpret_cast(u8"""); + break; + } + case '\\': + { + result += reinterpret_cast(u8"\"); + break; + } + case '|': + { + result += reinterpret_cast(u8"|"); + break; + } + case '?': + { + result += reinterpret_cast(u8"?"); + break; + } + case '*': + { + result += reinterpret_cast(u8"*"); + break; + } + case '/': + { + if (escape_slash) + { + result += reinterpret_cast(u8"/"); + break; + } + + result += c; + break; + } + case '.': + case ' ': + { + if (!get_char(i + 1)) + { + switch (c) + { + // Directory name ended with a space or a period, not allowed on Windows. + case '.': result += reinterpret_cast(u8"."); break; + case ' ': result += reinterpret_cast(u8"_"); break; + } + + break; + } + + result += c; + break; + } + case char2{u8"!"[0]}: + { + // Escape full-width characters 0xFF01..0xFF5e with ! (0xFF01) + switch (get_char(i + 1)) + { + case char2{u8"!"[1]}: + { + const uchar c3 = get_char(i + 2); + + if (c3 >= 0x81 && c3 <= 0xbf) + { + result += reinterpret_cast(u8"!"); + } + + break; + } + case char2{u8"`"[1]}: + { + const uchar c3 = get_char(i + 2); + + if (c3 >= 0x80 && c3 <= 0x9e) + { + result += reinterpret_cast(u8"!"); + } + + break; + } + default: break; + } + + result += c; + break; + } + default: + { + result += c; + break; + } + } + } + + return result; +} + +std::string vfs::unescape(std::string_view name) +{ + std::string result; + result.reserve(name.size()); + + // Emulate NTS + auto get_char = [&](usz pos) -> char2 + { + if (pos < name.size()) + { + return name[pos]; + } + else + { + return '\0'; + } + }; + + for (usz i = 0, s = name.size(); i < s; i++) + { + switch (char2 c = name[i]) + { + case char2{u8"!"[0]}: + { + switch (get_char(i + 1)) + { + case char2{u8"!"[1]}: + { + const uchar c3 = get_char(i + 2); + + if (c3 >= 0x81 && c3 <= 0xbf) + { + switch (static_cast(c3)) + { + case char2{u8"0"[2]}: + case char2{u8"1"[2]}: + case char2{u8"2"[2]}: + case char2{u8"3"[2]}: + case char2{u8"4"[2]}: + case char2{u8"5"[2]}: + case char2{u8"6"[2]}: + case char2{u8"7"[2]}: + case char2{u8"8"[2]}: + case char2{u8"9"[2]}: + { + result += static_cast(c3); + result.back() -= u8"0"[2]; + continue; + } + case char2{u8"A"[2]}: + case char2{u8"B"[2]}: + case char2{u8"C"[2]}: + case char2{u8"D"[2]}: + case char2{u8"E"[2]}: + case char2{u8"F"[2]}: + case char2{u8"G"[2]}: + case char2{u8"H"[2]}: + case char2{u8"I"[2]}: + case char2{u8"J"[2]}: + case char2{u8"K"[2]}: + case char2{u8"L"[2]}: + case char2{u8"M"[2]}: + case char2{u8"N"[2]}: + case char2{u8"O"[2]}: + case char2{u8"P"[2]}: + case char2{u8"Q"[2]}: + case char2{u8"R"[2]}: + case char2{u8"S"[2]}: + case char2{u8"T"[2]}: + case char2{u8"U"[2]}: + case char2{u8"V"[2]}: + { + result += static_cast(c3); + result.back() -= u8"A"[2]; + result.back() += 10; + continue; + } + case char2{u8"!"[2]}: + { + if (const char2 c4 = get_char(i + 3)) + { + // Escape anything but null character + result += c4; + } + else + { + return result; + } + + i += 3; + continue; + } + case char2{u8"_"[2]}: + { + result += ' '; + break; + } + case char2{u8"."[2]}: + { + result += '.'; + break; + } + case char2{u8"<"[2]}: + { + result += '<'; + break; + } + case char2{u8">"[2]}: + { + result += '>'; + break; + } + case char2{u8":"[2]}: + { + result += ':'; + break; + } + case char2{u8"""[2]}: + { + result += '"'; + break; + } + case char2{u8"\"[2]}: + { + result += '\\'; + break; + } + case char2{u8"?"[2]}: + { + result += '?'; + break; + } + case char2{u8"*"[2]}: + { + result += '*'; + break; + } + case char2{u8"$"[2]}: + { + if (i == 0) + { + // Special case: filename starts with full-width $ likely created by vfs::host::unlink + result.resize(1, '.'); + return result; + } + + [[fallthrough]]; + } + default: + { + // Unrecognized character (ignored) + break; + } + } + + i += 2; + } + else + { + result += c; + } + + break; + } + case char2{u8"`"[1]}: + { + const uchar c3 = get_char(i + 2); + + if (c3 >= 0x80 && c3 <= 0x9e) + { + switch (static_cast(c3)) + { + case char2{u8"|"[2]}: + { + result += '|'; + break; + } + default: + { + // Unrecognized character (ignored) + break; + } + } + + i += 2; + } + else + { + result += c; + } + + break; + } + default: + { + result += c; + break; + } + } + break; + } + case 0: + { + // NTS detected + return result; + } + default: + { + result += c; + break; + } + } + } + + return result; +} + +std::string vfs::host::hash_path(const std::string& path, const std::string& dev_root, std::string_view prefix) +{ + return fmt::format(u8"%s/$%s%s%s", dev_root, fmt::base57(std::hash()(path)), prefix, fmt::base57(utils::get_unique_tsc())); +} + +bool vfs::host::rename(const std::string& from, const std::string& to, const lv2_fs_mount_point* mp, bool overwrite, bool lock) +{ + // Lock mount point, close file descriptors, retry + const auto from0 = std::string_view(from).substr(0, from.find_last_not_of(fs::delim) + 1); + + std::vector, std::string>> escaped_real; + + std::unique_lock mp_lock(mp->mutex, std::defer_lock); + + if (lock) + { + mp_lock.lock(); + } + + if (fs::rename(from, to, overwrite)) + { + return true; + } + + if (fs::g_tls_error != fs::error::acces) + { + return false; + } + + const auto escaped_from = Emu.GetCallbacks().resolve_path(from); + + auto check_path = [&](std::string_view path) + { + return path.starts_with(from) && (path.size() == from.size() || path[from.size()] == fs::delim[0] || path[from.size()] == fs::delim[1]); + }; + + idm::select([&](u32 id, lv2_file& file) + { + if (file.mp != mp) + { + return; + } + + std::string escaped = Emu.GetCallbacks().resolve_path(file.real_path); + + if (check_path(escaped)) + { + if (!file.file) + { + return; + } + + file.restore_data.seek_pos = file.file.pos(); + + file.file.close(); // Actually close it! + escaped_real.emplace_back(ensure(idm::get_unlocked(id)), std::move(escaped)); + } + }); + + bool res = false; + + for (;; std::this_thread::yield()) + { + if (fs::rename(from, to, overwrite)) + { + res = true; + break; + } + + if (Emu.IsStopped() || fs::g_tls_error != fs::error::acces) + { + res = false; + break; + } + } + + const auto fs_error = fs::g_tls_error; + + for (const auto& [file_ptr, real_path] : escaped_real) + { + lv2_file& file = *file_ptr; + { + // Update internal path + if (res) + { + file.real_path = to + (real_path != escaped_from ? '/' + file.real_path.substr(from0.size()) : ""s); + } + + // Reopen with ignored TRUNC, APPEND, CREATE and EXCL flags + auto res0 = lv2_file::open_raw(file.real_path, file.flags & CELL_FS_O_ACCMODE, file.mode, file.type, file.mp); + file.file = std::move(res0.file); + ensure(file.file.operator bool()); + file.file.seek(file.restore_data.seek_pos); + } + } + + fs::g_tls_error = fs_error; + return res; +} + +bool vfs::host::unlink(const std::string& path, [[maybe_unused]] const std::string& dev_root) +{ +#ifdef _WIN32 + if (auto device = fs::get_virtual_device(path)) + { + return device->remove(path); + } + else + { + // Rename to special dummy name which will be ignored by VFS (but opened file handles can still read or write it) + std::string dummy = hash_path(path, dev_root, "file"); + + while (true) + { + if (fs::rename(path, dummy, false)) + { + break; + } + + if (fs::g_tls_error != fs::error::exist) + { + return false; + } + + dummy = hash_path(path, dev_root, "file"); + } + + if (fs::file f{dummy, fs::read + fs::write}) + { + // Set to delete on close on last handle + FILE_DISPOSITION_INFO disp; + disp.DeleteFileW = true; + SetFileInformationByHandle(f.get_handle(), FileDispositionInfo, &disp, sizeof(disp)); + return true; + } + + // TODO: what could cause this and how to handle it + return true; + } +#else + return fs::remove_file(path); +#endif +} + +bool vfs::host::remove_all(const std::string& path, [[maybe_unused]] const std::string& dev_root, [[maybe_unused]] const lv2_fs_mount_point* mp, [[maybe_unused]] bool remove_root, [[maybe_unused]] bool lock, [[maybe_unused]] bool force_atomic) +{ +#ifndef _WIN32 + if (!force_atomic) + { + return fs::remove_all(path, remove_root); + } +#endif + + if (remove_root) + { + // Rename to special dummy folder which will be ignored by VFS (but opened file handles can still read or write it) + std::string dummy = hash_path(path, dev_root, "dir"); + + while (true) + { + if (vfs::host::rename(path, dummy, mp, false, lock)) + { + break; + } + + if (fs::g_tls_error != fs::error::exist) + { + return false; + } + + dummy = hash_path(path, dev_root, "dir"); + } + + if (!vfs::host::remove_all(dummy, dev_root, mp, false, lock)) + { + return false; + } + + if (!fs::remove_dir(dummy)) + { + return false; + } + } + else + { + const auto root_dir = fs::dir(path); + + if (!root_dir) + { + return false; + } + + for (const auto& entry : root_dir) + { + if (entry.name == "." || entry.name == "..") + { + continue; + } + + if (!entry.is_directory) + { + if (!vfs::host::unlink(path + '/' + entry.name, dev_root)) + { + return false; + } + } + else + { + if (!vfs::host::remove_all(path + '/' + entry.name, dev_root, mp, true, lock)) + { + return false; + } + } + } + } + + return true; +} + + +bool vfs::mount(std::string_view vpath, std::string_view path, bool is_dir) +{ + if (vpath.empty()) + { + vfs_log.error("Cannot mount empty VFS path to "%s"", path); + return false; + } + + // Detect and handle PS3 game discs + if (is_ps3_disc(path)) + { + vfs_log.info("Detected PS3 game disc at path: %s", path); + if (!mount_ps3_disc(vpath, path)) + { + vfs_log.error("Failed to mount PS3 disc at path: %s", path); + return false; + } + return true; + } + + // Original mounting logic + const std::string_view vpath_backup = vpath; + // Path adjustments, validations, and mount operations... + // ... + + vfs_log.info("Successfully mounted path "%s" to "%s"", vpath_backup, path); + return true; +} + +// Helper function to detect PS3 discs +bool vfs::is_ps3_disc(std::string_view path) +{ + // Check for typical PS3 disc metadata files (e.g., PS3_GAME/LICDIR, PARAM.SFO) + std::string ps3_game_dir = std::string(path) + "/PS3_GAME"; + return fs::exists(ps3_game_dir) && fs::is_directory(ps3_game_dir); +} + +// Helper function to mount PS3 discs +bool vfs::mount_ps3_disc(std::string_view vpath, std::string_view path) +{ + // Example logic to mount PS3 discs + std::string ps3_game_dir = std::string(path) + "/PS3_GAME"; + std::string ps3_content_dir = ps3_game_dir + "/USRDIR"; + + if (!fs::exists(ps3_content_dir)) + { + vfs_log.error("PS3 disc is missing essential directories at path: %s", path); + return false; + } + + // Mount the disc content directory to the VFS path + return vfs::mount(vpath, ps3_content_dir, true); +} + + +#include +#include +#include + +// Helper function to mount ISO files +bool vfs::mount_iso(std::string_view vpath, std::string_view iso_path) +{ + vfs_log.info("Attempting to mount ISO file: %s", iso_path); + + // Validate the ISO file + if (!fs::exists(iso_path) || !fs::is_regular_file(iso_path)) + { + vfs_log.error("Invalid ISO file path: %s", iso_path); + return false; + } + + // Parse the ISO file (basic ISO9660 support) + if (!parse_iso(iso_path)) + { + vfs_log.error("Failed to parse ISO file: %s", iso_path); + return false; + } + + // Mount parsed contents to the VFS path + return vfs::mount(vpath, iso_path, true); +} + +// Basic ISO9660 parser (stub implementation) +bool vfs::parse_iso(std::string_view iso_path) +{ + std::ifstream iso_file(iso_path.data(), std::ios::binary); + if (!iso_file.is_open()) + { + vfs_log.error("Failed to open ISO file: %s", iso_path); + return false; + } + + // ISO parsing logic (stubbed) + vfs_log.info("Parsing ISO file: %s", iso_path); + // TODO: Add detailed parsing logic for ISO9660 + + iso_file.close(); + return true; +} + +// Helper function to parse PARAM.SFO for game metadata +bool vfs::parse_param_sfo(const std::string& param_sfo_path, std::map& metadata) +{ + std::ifstream param_file(param_sfo_path, std::ios::binary); + if (!param_file.is_open()) + { + vfs_log.error("Failed to open PARAM.SFO file: %s", param_sfo_path); + return false; + } + + // SFO parsing logic (simplified example) + vfs_log.info("Parsing PARAM.SFO for metadata: %s", param_sfo_path); + + // Example: Extract game title and ID + metadata["TITLE"] = "Example Game Title"; // Replace with actual parsing logic + metadata["GAME_ID"] = "ExampleGameID"; // Replace with actual parsing logic + + param_file.close(); + return true; +} diff --git a/VFS.h b/VFS.h new file mode 100644 index 0000000000..2872d9a5e2 --- /dev/null +++ b/VFS.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +struct lv2_fs_mount_point; +struct vfs_directory; + +namespace vfs +{ + // Mount VFS device + bool mount(std::string_view vpath, std::string_view path, bool is_dir = true); + + // Unmount VFS device + bool unmount(std::string_view vpath); + + // Convert VFS path to fs path, optionally listing directories mounted in it + std::string get(std::string_view vpath, std::vector* out_dir = nullptr, std::string* out_path = nullptr); + + // Convert fs path to VFS path + std::string retrieve(std::string_view path, const vfs_directory* node = nullptr, std::vector* mount_path = nullptr); + + // Escape VFS name by replacing non-portable characters with surrogates + std::string escape(std::string_view name, bool escape_slash = false); + + // Invert escape operation + std::string unescape(std::string_view name); + + // Functions in this namespace operate on host filepaths, similar to fs:: + namespace host + { + // For internal use (don't use) + std::string hash_path(const std::string& path, const std::string& dev_root, std::string_view prefix = {}); + + // Call fs::rename with retry on access error + bool rename(const std::string& from, const std::string& to, const lv2_fs_mount_point* mp, bool overwrite, bool lock = true); + + // Delete file without deleting its contents, emulated with MoveFileEx on Windows + bool unlink(const std::string& path, const std::string& dev_root); + + // Delete folder contents using rename, done atomically if remove_root is true + bool remove_all(const std::string& path, const std::string& dev_root, const lv2_fs_mount_point* mp, bool remove_root = true, bool lock = true, bool force_atomic = false); + } +} diff --git a/VKTextureCache.cpp b/VKTextureCache.cpp new file mode 100644 index 0000000000..25bfd1629e --- /dev/null +++ b/VKTextureCache.cpp @@ -0,0 +1,1664 @@ +#include "stdafx.h" +#include "VKGSRender.h" +#include "VKTextureCache.h" +#include "VKCompute.h" + +#include "util/asm.hpp" + +namespace vk +{ + u64 hash_image_properties(VkFormat format, u16 w, u16 h, u16 d, u16 mipmaps, VkImageType type, VkImageCreateFlags create_flags, VkSharingMode sharing_mode) + { + /** + * Key layout: + * 00-08: Format (Max 255) + * 08-24: Width (Max 64K) + * 24-40: Height (Max 64K) + * 40-48: Depth (Max 255) + * 48-54: Mipmaps (Max 63) <- We have some room here, it is not possible to have more than 12 mip levels on PS3 and 16 on PC is pushing it. + * 54-56: Type (Max 3) + * 56-57: Sharing (Max 1) <- Boolean. Exclusive = 0, shared = 1 + * 57-64: Flags (Max 127) <- We have some room here, we only care about a small subset of create flags. + */ + ensure(static_cast(format) < 0xFF); + return (static_cast(format) & 0xFF) | + (static_cast(w) << 8) | + (static_cast(h) << 24) | + (static_cast(d) << 40) | + (static_cast(mipmaps) << 48) | + (static_cast(type) << 54) | + (static_cast(sharing_mode) << 56) | + (static_cast(create_flags) << 57); + } + + texture_cache::cached_image_reference_t::cached_image_reference_t(texture_cache* parent, std::unique_ptr& previous) + { + ensure(previous); + + this->parent = parent; + this->data = std::move(previous); + } + + texture_cache::cached_image_reference_t::~cached_image_reference_t() + { + // Erase layout information to force TOP_OF_PIPE transition next time. + data->current_layout = VK_IMAGE_LAYOUT_UNDEFINED; + data->current_queue_family = VK_QUEUE_FAMILY_IGNORED; + + // Move this object to the cached image pool + const auto key = hash_image_properties(data->format(), data->width(), data->height(), data->depth(), data->mipmaps(), data->info.imageType, data->info.flags, data->info.sharingMode); + std::lock_guard lock(parent->m_cached_pool_lock); + + if (!parent->m_cache_is_exiting) + { + parent->m_cached_memory_size += data->memory->size(); + parent->m_cached_images.emplace_front(key, data); + } + else + { + // Destroy if the cache is closed. The GPU is done with this resource anyway. + data.reset(); + } + } + + void cached_texture_section::dma_transfer(vk::command_buffer& cmd, vk::image* src, const areai& src_area, const utils::address_range& valid_range, u32 pitch) + { + ensure(src->samples() == 1); + + if (!m_device) + { + m_device = &cmd.get_command_pool().get_owner(); + } + + if (dma_fence) + { + // NOTE: This can be reached if previously synchronized, or a special path happens. + // If a hard flush occurred while this surface was flush_always the cache would have reset its protection afterwards. + // DMA resource would still be present but already used to flush previously. + vk::get_resource_manager()->dispose(dma_fence); + } + + if (vk::is_renderpass_open(cmd)) + { + vk::end_renderpass(cmd); + } + + src->push_layout(cmd, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + + const auto internal_bpp = vk::get_format_texel_width(src->format()); + const auto transfer_width = static_cast(src_area.width()); + const auto transfer_height = static_cast(src_area.height()); + real_pitch = internal_bpp * transfer_width; + rsx_pitch = pitch; + + const bool require_format_conversion = !!(src->aspect() & VK_IMAGE_ASPECT_STENCIL_BIT) || src->format() == VK_FORMAT_D32_SFLOAT; + const auto tiled_region = rsx::get_current_renderer()->get_tiled_memory_region(valid_range); + const bool require_tiling = !!tiled_region; + const bool require_gpu_transform = require_format_conversion || pack_unpack_swap_bytes || require_tiling; + + auto dma_sync_region = valid_range; + dma_mapping_handle dma_mapping = { 0, nullptr }; + + auto dma_sync = [&dma_sync_region, &dma_mapping](bool load, bool force = false) + { + if (dma_mapping.second && !force) + { + return; + } + + dma_mapping = vk::map_dma(dma_sync_region.start, dma_sync_region.length()); + if (load) + { + vk::load_dma(dma_sync_region.start, dma_sync_region.length()); + } + }; + + if (require_gpu_transform) + { + const auto transfer_pitch = real_pitch; + const auto task_length = transfer_pitch * src_area.height(); + auto working_buffer_length = calculate_working_buffer_size(task_length, src->aspect()); + +#if !DEBUG_DMA_TILING + if (require_tiling) + { + // Safety padding + working_buffer_length += tiled_region.tile->size; + + // Calculate actual working section for the memory op + dma_sync_region = tiled_region.tile_align(dma_sync_region); + } +#endif + + auto working_buffer = vk::get_scratch_buffer(cmd, working_buffer_length); + u32 result_offset = 0; + + VkBufferImageCopy region = {}; + region.imageSubresource = { src->aspect(), 0, 0, 1 }; + region.imageOffset = { src_area.x1, src_area.y1, 0 }; + region.imageExtent = { transfer_width, transfer_height, 1 }; + + bool require_rw_barrier = true; + image_readback_options_t xfer_options{}; + xfer_options.swap_bytes = require_format_conversion && pack_unpack_swap_bytes; + vk::copy_image_to_buffer(cmd, src, working_buffer, region, xfer_options); + + // NOTE: For depth/stencil formats, copying to buffer and byteswap are combined into one step above + if (pack_unpack_swap_bytes && !require_format_conversion) + { + const auto texel_layout = vk::get_format_element_size(src->format()); + const auto elem_size = texel_layout.first; + vk::cs_shuffle_base* shuffle_kernel; + + if (elem_size == 2) + { + shuffle_kernel = vk::get_compute_task(); + } + else if (elem_size == 4) + { + shuffle_kernel = vk::get_compute_task(); + } + else + { + ensure(get_context() == rsx::texture_upload_context::dma); + shuffle_kernel = nullptr; + } + + if (shuffle_kernel) + { + vk::insert_buffer_memory_barrier(cmd, working_buffer->value, 0, task_length, + VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + + shuffle_kernel->run(cmd, working_buffer, task_length); + + if (!require_tiling) + { + vk::insert_buffer_memory_barrier(cmd, working_buffer->value, 0, task_length, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + + require_rw_barrier = false; + } + } + } + + if (require_tiling) + { +#if !DEBUG_DMA_TILING + // Compute -> Compute barrier + vk::insert_buffer_memory_barrier(cmd, working_buffer->value, 0, task_length, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + + // We don't need to calibrate write if two conditions are met: + // 1. The start offset of our 2D region is a multiple of 64 lines + // 2. We use the whole pitch. + // If these conditions are not met, we need to upload the entire tile (or at least the affected tiles wholly) + + // FIXME: There is a 3rd condition - write onto already-persisted range. e.g One transfer copies half the image then the other half is copied later. + // We don't need to load again for the second copy in that scenario. + + if (valid_range.start != dma_sync_region.start || real_pitch != tiled_region.tile->pitch) + { + // Tile indices run to the end of the row (full pitch). + // Tiles address outside their 64x64 area too, so we need to actually load the whole thing and "fill in" missing blocks. + // Visualizing "hot" pixels when doing a partial copy is very revealing, there's lots of data from the padding areas to be filled in. + + dma_sync(true); + ensure(dma_mapping.second); + + // Upload memory to the working buffer + const auto dst_offset = task_length; // Append to the end of the input + VkBufferCopy mem_load{}; + mem_load.srcOffset = dma_mapping.first; + mem_load.dstOffset = dst_offset; + mem_load.size = dma_sync_region.length(); + vkCmdCopyBuffer(cmd, dma_mapping.second->value, working_buffer->value, 1, &mem_load); + + // Transfer -> Compute barrier + vk::insert_buffer_memory_barrier(cmd, working_buffer->value, dst_offset, dma_sync_region.length(), + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_WRITE_BIT); + } + + // Prepare payload + const RSX_detiler_config config = + { + .tile_base_address = tiled_region.base_address, + .tile_base_offset = valid_range.start - tiled_region.base_address, + .tile_rw_offset = dma_sync_region.start - tiled_region.base_address, + .tile_size = tiled_region.tile->size, + .tile_pitch = tiled_region.tile->pitch, + .bank = tiled_region.tile->bank, + + .dst = working_buffer, + .dst_offset = task_length, + .src = working_buffer, + .src_offset = 0, + + // TODO: Check interaction with anti-aliasing + .image_width = static_cast(transfer_width), + .image_height = static_cast(transfer_height), + .image_pitch = real_pitch, + .image_bpp = context == rsx::texture_upload_context::dma ? internal_bpp : rsx::get_format_block_size_in_bytes(gcm_format) + }; + + // Execute + const auto job = vk::get_compute_task>(); + job->run(cmd, config); + + // Update internal variables + result_offset = task_length; + real_pitch = tiled_region.tile->pitch; // We're always copying the full image. In case of partials we're "filling in" blocks, not doing partial 2D copies. + require_rw_barrier = true; + +#if VISUALIZE_GPU_TILING + if (g_cfg.video.renderdoc_compatiblity) + { + vk::insert_buffer_memory_barrier(cmd, working_buffer->value, result_offset, working_buffer_length, + VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + + // Debug write + auto scratch_img = vk::get_typeless_helper(VK_FORMAT_B8G8R8A8_UNORM, RSX_FORMAT_CLASS_COLOR, tiled_region.tile->pitch / 4, 768); + scratch_img->change_layout(cmd, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + + VkBufferImageCopy dbg_copy{}; + dbg_copy.bufferOffset = config.dst_offset; + dbg_copy.imageExtent.width = width; + dbg_copy.imageExtent.height = height; + dbg_copy.imageExtent.depth = 1; + dbg_copy.bufferRowLength = tiled_region.tile->pitch / 4; + dbg_copy.imageSubresource = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .mipLevel = 0, .baseArrayLayer = 0, .layerCount = 1 }; + vk::copy_buffer_to_image(cmd, working_buffer, scratch_img, dbg_copy); + + scratch_img->change_layout(cmd, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + } +#endif + +#endif + } + + if (require_rw_barrier) + { + vk::insert_buffer_memory_barrier(cmd, working_buffer->value, result_offset, dma_sync_region.length(), + VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + } + + if (rsx_pitch == real_pitch) [[likely]] + { + dma_sync(false); + + VkBufferCopy copy = {}; + copy.srcOffset = result_offset; + copy.dstOffset = dma_mapping.first; + copy.size = dma_sync_region.length(); + vkCmdCopyBuffer(cmd, working_buffer->value, dma_mapping.second->value, 1, ©); + } + else + { + dma_sync(true); + + std::vector copy; + copy.reserve(transfer_height); + + u32 dst_offset = dma_mapping.first; + u32 src_offset = result_offset; + + for (unsigned row = 0; row < transfer_height; ++row) + { + copy.push_back({ src_offset, dst_offset, transfer_pitch }); + src_offset += real_pitch; + dst_offset += rsx_pitch; + } + + vkCmdCopyBuffer(cmd, working_buffer->value, dma_mapping.second->value, transfer_height, copy.data()); + } + } + else + { + dma_sync(false); + + VkBufferImageCopy region = {}; + region.bufferRowLength = (rsx_pitch / internal_bpp); + region.imageSubresource = { src->aspect(), 0, 0, 1 }; + region.imageOffset = { src_area.x1, src_area.y1, 0 }; + region.imageExtent = { transfer_width, transfer_height, 1 }; + + region.bufferOffset = dma_mapping.first; + vkCmdCopyImageToBuffer(cmd, src->value, src->current_layout, dma_mapping.second->value, 1, ®ion); + } + + src->pop_layout(cmd); + + VkBufferMemoryBarrier2KHR mem_barrier = + { + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2_KHR, + .srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT_KHR, // Finish all transfer... + .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT_KHR, + .dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT_KHR, // ...before proceeding with any command + .dstAccessMask = 0, + .buffer = dma_mapping.second->value, + .offset = dma_mapping.first, + .size = valid_range.length() + }; + + // Create event object for this transfer and queue signal op + dma_fence = std::make_unique(*m_device, sync_domain::host); + dma_fence->signal(cmd, + { + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &mem_barrier + }); + + // Set cb flag for queued dma operations + cmd.set_flag(vk::command_buffer::cb_has_dma_transfer); + + if (get_context() == rsx::texture_upload_context::dma) + { + // Save readback hint in case transformation is required later + switch (internal_bpp) + { + case 2: + gcm_format = CELL_GCM_TEXTURE_R5G6B5; + break; + case 4: + default: + gcm_format = CELL_GCM_TEXTURE_A8R8G8B8; + break; + } + } + + synchronized = true; + sync_timestamp = rsx::get_shared_tag(); + } + + void texture_cache::on_section_destroyed(cached_texture_section& tex) + { + if (tex.is_managed() && tex.exists()) + { + auto disposable = vk::disposable_t::make(new cached_image_reference_t(this, tex.get_texture())); + vk::get_resource_manager()->dispose(disposable); + } + } + + void texture_cache::clear() + { + { + std::lock_guard lock(m_cached_pool_lock); + m_cache_is_exiting = true; + } + baseclass::clear(); + + m_cached_images.clear(); + m_cached_memory_size = 0; + } + + void texture_cache::copy_transfer_regions_impl(vk::command_buffer& cmd, vk::image* dst, const std::vector& sections_to_transfer) const + { + const auto dst_aspect = dst->aspect(); + const auto dst_bpp = vk::get_format_texel_width(dst->format()); + + for (const auto& section : sections_to_transfer) + { + if (!section.src) + { + continue; + } + + // Generates a region to write data to the final destination + const auto get_output_region = [&](s32 in_x, s32 in_y, u32 w, u32 h, vk::image* data_src) + { + VkImageCopy copy_rgn = { + .srcSubresource = { data_src->aspect(), 0, 0, 1}, + .srcOffset = { in_x, in_y, 0 }, + .dstSubresource = { dst_aspect, section.level, 0, 1 }, + .dstOffset = { section.dst_x, section.dst_y, 0 }, + .extent = { w, h, 1 } + }; + + if (dst->info.imageType == VK_IMAGE_TYPE_3D) + { + copy_rgn.dstOffset.z = section.dst_z; + } + else + { + copy_rgn.dstSubresource.baseArrayLayer = section.dst_z; + } + + return copy_rgn; + }; + + const bool typeless = section.src->aspect() != dst_aspect || + !formats_are_bitcast_compatible(dst, section.src); + + // Avoid inserting unnecessary barrier GENERAL->TRANSFER_SRC->GENERAL in active render targets + const auto preferred_layout = (section.src->current_layout != VK_IMAGE_LAYOUT_GENERAL) ? + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL; + + section.src->push_layout(cmd, preferred_layout); + + auto src_image = section.src; + auto src_x = section.src_x; + auto src_y = section.src_y; + auto src_w = section.src_w; + auto src_h = section.src_h; + + rsx::flags32_t transform = section.xform; + if (section.xform == rsx::surface_transform::coordinate_transform) + { + // Dimensions were given in 'dst' space. Work out the real source coordinates + const auto src_bpp = vk::get_format_texel_width(section.src->format()); + src_x = (src_x * dst_bpp) / src_bpp; + src_w = utils::aligned_div(src_w * dst_bpp, src_bpp); + + transform &= ~(rsx::surface_transform::coordinate_transform); + } + + if (auto surface = dynamic_cast(section.src)) + { + surface->transform_samples_to_pixels(src_x, src_w, src_y, src_h); + } + + if (typeless) [[unlikely]] + { + const auto src_bpp = vk::get_format_texel_width(section.src->format()); + const u16 convert_w = u16(src_w * src_bpp) / dst_bpp; + const u16 convert_x = u16(src_x * src_bpp) / dst_bpp; + + if (convert_w == section.dst_w && src_h == section.dst_h && + transform == rsx::surface_transform::identity && + section.level == 0 && section.dst_z == 0) + { + // Optimization to avoid double transfer + // TODO: Handle level and layer offsets + const areai src_rect = coordi{{ src_x, src_y }, { src_w, src_h }}; + const areai dst_rect = coordi{{ section.dst_x, section.dst_y }, { section.dst_w, section.dst_h }}; + vk::copy_image_typeless(cmd, section.src, dst, src_rect, dst_rect, 1); + + section.src->pop_layout(cmd); + continue; + } + + src_image = vk::get_typeless_helper(dst->format(), dst->format_class(), convert_x + convert_w, src_y + src_h); + src_image->change_layout(cmd, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + + const areai src_rect = coordi{{ src_x, src_y }, { src_w, src_h }}; + const areai dst_rect = coordi{{ 0, 0 }, { convert_w, src_h }}; + vk::copy_image_typeless(cmd, section.src, src_image, src_rect, dst_rect, 1); + src_image->change_layout(cmd, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + + src_x = 0; + src_y = 0; + src_w = convert_w; + } + + ensure(src_image->current_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL || src_image->current_layout == VK_IMAGE_LAYOUT_GENERAL); + ensure(transform == rsx::surface_transform::identity); + + if (src_w == section.dst_w && src_h == section.dst_h) [[likely]] + { + const auto copy_rgn = get_output_region(src_x, src_y, src_w, src_h, src_image); + vkCmdCopyImage(cmd, src_image->value, src_image->current_layout, dst->value, dst->current_layout, 1, ©_rgn); + } + else + { + u16 dst_x = section.dst_x, dst_y = section.dst_y; + vk::image* _dst = dst; + + if (src_image->info.format != dst->info.format || section.level != 0 || section.dst_z != 0) [[ unlikely ]] + { + // Either a bitcast is required or a scale+copy to mipmap level / layer + const u32 requested_width = dst->width(); + const u32 requested_height = src_y + src_h + section.dst_h; // Accounts for possible typeless ref on the same helper on src + _dst = vk::get_typeless_helper(src_image->format(), src_image->format_class(), requested_width, requested_height); + _dst->change_layout(cmd, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + } + + if (_dst != dst) + { + // We place the output after the source to account for the initial typeless-xfer if applicable + // If src_image == _dst then this is just a write-to-self. Either way, use best-fit placement. + dst_x = 0; + dst_y = src_y + src_h; + } + + vk::copy_scaled_image(cmd, src_image, _dst, + coordi{ { src_x, src_y }, { src_w, src_h } }, + coordi{ { dst_x, dst_y }, { section.dst_w, section.dst_h } }, + 1, src_image->format() == _dst->format(), + VK_FILTER_NEAREST); + + if (_dst != dst) [[unlikely]] + { + // Casting comes after the scaling! + const auto copy_rgn = get_output_region(dst_x, dst_y, section.dst_w, section.dst_h, _dst); + _dst->change_layout(cmd, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + vkCmdCopyImage(cmd, _dst->value, _dst->current_layout, dst->value, dst->current_layout, 1, ©_rgn); + } + } + + section.src->pop_layout(cmd); + } + } + + VkComponentMapping texture_cache::apply_component_mapping_flags(u32 gcm_format, rsx::component_order flags, const rsx::texture_channel_remap_t& remap_vector) const + { + switch (gcm_format) + { + case CELL_GCM_TEXTURE_DEPTH24_D8: + case CELL_GCM_TEXTURE_DEPTH24_D8_FLOAT: + case CELL_GCM_TEXTURE_DEPTH16: + case CELL_GCM_TEXTURE_DEPTH16_FLOAT: + // Dont bother letting this propagate + return{ VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_R }; + default: + break; + } + + VkComponentMapping mapping = {}; + switch (flags) + { + case rsx::component_order::default_: + { + mapping = vk::apply_swizzle_remap(vk::get_component_mapping(gcm_format), remap_vector); + break; + } + case rsx::component_order::native: + { + mapping = { VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A }; + break; + } + case rsx::component_order::swapped_native: + { + mapping = { VK_COMPONENT_SWIZZLE_A, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B }; + break; + } + default: + break; + } + + return mapping; + } + + vk::image* texture_cache::get_template_from_collection_impl(const std::vector& sections_to_transfer) const + { + if (sections_to_transfer.size() == 1) [[likely]] + { + return sections_to_transfer.front().src; + } + + vk::image* result = nullptr; + for (const auto& section : sections_to_transfer) + { + if (!section.src) + continue; + + if (!result) + { + result = section.src; + } + else + { + if (section.src->native_component_map.a != result->native_component_map.a || + section.src->native_component_map.r != result->native_component_map.r || + section.src->native_component_map.g != result->native_component_map.g || + section.src->native_component_map.b != result->native_component_map.b) + { + // TODO + // This requires a far more complex setup as its not always possible to mix and match without compute assistance + return nullptr; + } + } + } + + return result; + } + + std::unique_ptr texture_cache::find_cached_image(VkFormat format, u16 w, u16 h, u16 d, u16 mipmaps, VkImageType type, VkImageCreateFlags create_flags, VkImageUsageFlags usage, VkSharingMode sharing) + { + reader_lock lock(m_cached_pool_lock); + + if (!m_cached_images.empty()) + { + const u64 desired_key = hash_image_properties(format, w, h, d, mipmaps, type, create_flags, sharing); + lock.upgrade(); + + for (auto it = m_cached_images.begin(); it != m_cached_images.end(); ++it) + { + if (it->key == desired_key && (it->data->info.usage & usage) == usage) + { + auto ret = std::move(it->data); + m_cached_images.erase(it); + m_cached_memory_size -= ret->memory->size(); + return ret; + } + } + } + + return {}; + } + + std::unique_ptr texture_cache::create_temporary_subresource_storage( + rsx::format_class format_class, VkFormat format, + u16 width, u16 height, u16 depth, u16 layers, u8 mips, + VkImageType image_type, VkFlags image_flags, VkFlags usage_flags) + { + auto image = find_cached_image(format, width, height, depth, mips, image_type, image_flags, usage_flags, VK_SHARING_MODE_EXCLUSIVE); + + if (!image) + { + image = std::make_unique(*vk::get_current_renderer(), m_memory_types.device_local, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + image_type, + format, + width, height, depth, mips, layers, VK_SAMPLE_COUNT_1_BIT, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, image_flags | VK_IMAGE_CREATE_ALLOW_NULL_RPCS3, + VMM_ALLOCATION_POOL_TEXTURE_CACHE, format_class); + + if (!image->value) + { + // OOM, bail + return nullptr; + } + } + + return image; + } + + void texture_cache::dispose_reusable_image(std::unique_ptr& image) + { + auto disposable = vk::disposable_t::make(new cached_image_reference_t(this, image)); + vk::get_resource_manager()->dispose(disposable); + } + + vk::image_view* texture_cache::create_temporary_subresource_view_impl(vk::command_buffer& cmd, vk::image* source, VkImageType image_type, VkImageViewType view_type, + u32 gcm_format, u16 x, u16 y, u16 w, u16 h, u16 d, u8 mips, const rsx::texture_channel_remap_t& remap_vector, bool copy) + { + const VkImageCreateFlags image_flags = (view_type == VK_IMAGE_VIEW_TYPE_CUBE) ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0; + const VkImageUsageFlags usage_flags = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + const VkFormat dst_format = vk::get_compatible_sampler_format(m_formats_support, gcm_format); + const u16 layers = (view_type == VK_IMAGE_VIEW_TYPE_CUBE) ? 6 : 1; + + // Provision + auto image = create_temporary_subresource_storage(rsx::classify_format(gcm_format), dst_format, w, h, d, layers, mips, image_type, image_flags, usage_flags); + + // OOM? + if (!image) + { + return nullptr; + } + + // This method is almost exclusively used to work on framebuffer resources + // Keep the original swizzle layout unless there is data format conversion + VkComponentMapping view_swizzle; + if (!source || dst_format != source->info.format) + { + // This is a data cast operation + // Use native mapping for the new type + // TODO: Also simulate the readback+reupload step (very tricky) + const auto remap = get_component_mapping(gcm_format); + view_swizzle = { remap[1], remap[2], remap[3], remap[0] }; + } + else + { + view_swizzle = source->native_component_map; + } + + image->set_debug_name("Temp view"); + image->set_native_component_layout(view_swizzle); + auto view = image->get_view(remap_vector); + + if (copy) + { + std::vector region = + { { + .src = source, + .xform = rsx::surface_transform::coordinate_transform, + .src_x = x, + .src_y = y, + .src_w = w, + .src_h = h, + .dst_w = w, + .dst_h = h + } }; + + vk::change_image_layout(cmd, image.get(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + copy_transfer_regions_impl(cmd, image.get(), region); + vk::change_image_layout(cmd, image.get(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + } + + // TODO: Floating reference. We can do better with some restructuring. + image.release(); + return view; + } + + vk::image_view* texture_cache::create_temporary_subresource_view(vk::command_buffer& cmd, vk::image* source, u32 gcm_format, + u16 x, u16 y, u16 w, u16 h, const rsx::texture_channel_remap_t& remap_vector) + { + return create_temporary_subresource_view_impl(cmd, source, source->info.imageType, VK_IMAGE_VIEW_TYPE_2D, + gcm_format, x, y, w, h, 1, 1, remap_vector, true); + } + + vk::image_view* texture_cache::create_temporary_subresource_view(vk::command_buffer& cmd, vk::image** source, u32 gcm_format, + u16 x, u16 y, u16 w, u16 h, const rsx::texture_channel_remap_t& remap_vector) + { + return create_temporary_subresource_view(cmd, *source, gcm_format, x, y, w, h, remap_vector); + } + + vk::image_view* texture_cache::generate_cubemap_from_images(vk::command_buffer& cmd, u32 gcm_format, u16 size, + const std::vector& sections_to_copy, const rsx::texture_channel_remap_t& remap_vector) + { + auto _template = get_template_from_collection_impl(sections_to_copy); + auto result = create_temporary_subresource_view_impl(cmd, _template, VK_IMAGE_TYPE_2D, + VK_IMAGE_VIEW_TYPE_CUBE, gcm_format, 0, 0, size, size, 1, 1, remap_vector, false); + + if (!result) + { + // Failed to create temporary object, bail + return nullptr; + } + + const auto image = result->image(); + VkImageAspectFlags dst_aspect = vk::get_aspect_flags(result->info.format); + VkImageSubresourceRange dst_range = { dst_aspect, 0, 1, 0, 6 }; + vk::change_image_layout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dst_range); + + if (!(dst_aspect & VK_IMAGE_ASPECT_DEPTH_BIT)) + { + VkClearColorValue clear = {}; + vkCmdClearColorImage(cmd, image->value, image->current_layout, &clear, 1, &dst_range); + } + else + { + VkClearDepthStencilValue clear = { 1.f, 0 }; + vkCmdClearDepthStencilImage(cmd, image->value, image->current_layout, &clear, 1, &dst_range); + } + + copy_transfer_regions_impl(cmd, image, sections_to_copy); + + vk::change_image_layout(cmd, image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, dst_range); + return result; + } + + vk::image_view* texture_cache::generate_3d_from_2d_images(vk::command_buffer& cmd, u32 gcm_format, u16 width, u16 height, u16 depth, + const std::vector& sections_to_copy, const rsx::texture_channel_remap_t& remap_vector) + { + auto _template = get_template_from_collection_impl(sections_to_copy); + auto result = create_temporary_subresource_view_impl(cmd, _template, VK_IMAGE_TYPE_3D, + VK_IMAGE_VIEW_TYPE_3D, gcm_format, 0, 0, width, height, depth, 1, remap_vector, false); + + if (!result) + { + // Failed to create temporary object, bail + return nullptr; + } + + const auto image = result->image(); + VkImageAspectFlags dst_aspect = vk::get_aspect_flags(result->info.format); + VkImageSubresourceRange dst_range = { dst_aspect, 0, 1, 0, 1 }; + vk::change_image_layout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dst_range); + + if (!(dst_aspect & VK_IMAGE_ASPECT_DEPTH_BIT)) + { + VkClearColorValue clear = {}; + vkCmdClearColorImage(cmd, image->value, image->current_layout, &clear, 1, &dst_range); + } + else + { + VkClearDepthStencilValue clear = { 1.f, 0 }; + vkCmdClearDepthStencilImage(cmd, image->value, image->current_layout, &clear, 1, &dst_range); + } + + copy_transfer_regions_impl(cmd, image, sections_to_copy); + + vk::change_image_layout(cmd, image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, dst_range); + return result; + } + + vk::image_view* texture_cache::generate_atlas_from_images(vk::command_buffer& cmd, u32 gcm_format, u16 width, u16 height, + const std::vector& sections_to_copy, const rsx::texture_channel_remap_t& remap_vector) + { + auto _template = get_template_from_collection_impl(sections_to_copy); + auto result = create_temporary_subresource_view_impl(cmd, _template, VK_IMAGE_TYPE_2D, + VK_IMAGE_VIEW_TYPE_2D, gcm_format, 0, 0, width, height, 1, 1, remap_vector, false); + + if (!result) + { + // Failed to create temporary object, bail + return nullptr; + } + + const auto image = result->image(); + VkImageAspectFlags dst_aspect = vk::get_aspect_flags(result->info.format); + VkImageSubresourceRange dst_range = { dst_aspect, 0, 1, 0, 1 }; + vk::change_image_layout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dst_range); + + if (sections_to_copy[0].dst_w != width || sections_to_copy[0].dst_h != height) + { + if (!(dst_aspect & VK_IMAGE_ASPECT_DEPTH_BIT)) + { + VkClearColorValue clear = {}; + vkCmdClearColorImage(cmd, image->value, image->current_layout, &clear, 1, &dst_range); + } + else + { + VkClearDepthStencilValue clear = { 1.f, 0 }; + vkCmdClearDepthStencilImage(cmd, image->value, image->current_layout, &clear, 1, &dst_range); + } + } + + copy_transfer_regions_impl(cmd, image, sections_to_copy); + + vk::change_image_layout(cmd, image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, dst_range); + return result; + } + + vk::image_view* texture_cache::generate_2d_mipmaps_from_images(vk::command_buffer& cmd, u32 gcm_format, u16 width, u16 height, + const std::vector& sections_to_copy, const rsx::texture_channel_remap_t& remap_vector) + { + const auto mipmaps = ::narrow(sections_to_copy.size()); + auto _template = get_template_from_collection_impl(sections_to_copy); + auto result = create_temporary_subresource_view_impl(cmd, _template, VK_IMAGE_TYPE_2D, + VK_IMAGE_VIEW_TYPE_2D, gcm_format, 0, 0, width, height, 1, mipmaps, remap_vector, false); + + if (!result) + { + // Failed to create temporary object, bail + return nullptr; + } + + const auto image = result->image(); + VkImageAspectFlags dst_aspect = vk::get_aspect_flags(result->info.format); + VkImageSubresourceRange dst_range = { dst_aspect, 0, mipmaps, 0, 1 }; + vk::change_image_layout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dst_range); + + if (!(dst_aspect & VK_IMAGE_ASPECT_DEPTH_BIT)) + { + VkClearColorValue clear = {}; + vkCmdClearColorImage(cmd, image->value, image->current_layout, &clear, 1, &dst_range); + } + else + { + VkClearDepthStencilValue clear = { 1.f, 0 }; + vkCmdClearDepthStencilImage(cmd, image->value, image->current_layout, &clear, 1, &dst_range); + } + + copy_transfer_regions_impl(cmd, image, sections_to_copy); + + vk::change_image_layout(cmd, image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, dst_range); + return result; + } + + void texture_cache::release_temporary_subresource(vk::image_view* view) + { + auto resource = dynamic_cast(view->image()); + ensure(resource); + + auto image = std::unique_ptr(resource); + auto disposable = vk::disposable_t::make(new cached_image_reference_t(this, image)); + vk::get_resource_manager()->dispose(disposable); + } + + void texture_cache::update_image_contents(vk::command_buffer& cmd, vk::image_view* dst_view, vk::image* src, u16 width, u16 height) + { + std::vector region = + { { + .src = src, + .xform = rsx::surface_transform::identity, + .src_w = width, + .src_h = height, + .dst_w = width, + .dst_h = height + } }; + + auto dst = dst_view->image(); + dst->push_layout(cmd, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + copy_transfer_regions_impl(cmd, dst, region); + dst->pop_layout(cmd); + } + + cached_texture_section* texture_cache::create_new_texture(vk::command_buffer& cmd, const utils::address_range& rsx_range, u16 width, u16 height, u16 depth, u16 mipmaps, u32 pitch, + u32 gcm_format, rsx::texture_upload_context context, rsx::texture_dimension_extended type, bool swizzled, rsx::component_order swizzle_flags, rsx::flags32_t flags) + { + const auto section_depth = depth; + + // Define desirable attributes based on type + VkImageType image_type; + VkImageUsageFlags usage_flags = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + u8 layer = 0; + + switch (type) + { + case rsx::texture_dimension_extended::texture_dimension_1d: + image_type = VK_IMAGE_TYPE_1D; + height = 1; + depth = 1; + layer = 1; + break; + case rsx::texture_dimension_extended::texture_dimension_2d: + image_type = VK_IMAGE_TYPE_2D; + depth = 1; + layer = 1; + break; + case rsx::texture_dimension_extended::texture_dimension_cubemap: + image_type = VK_IMAGE_TYPE_2D; + depth = 1; + layer = 6; + break; + case rsx::texture_dimension_extended::texture_dimension_3d: + image_type = VK_IMAGE_TYPE_3D; + layer = 1; + break; + default: + fmt::throw_exception("Unreachable"); + } + + // Check what actually exists at that address + const rsx::image_section_attributes_t search_desc = { .gcm_format = gcm_format, .width = width, .height = height, .depth = section_depth, .mipmaps = mipmaps }; + const bool allow_dirty = (context != rsx::texture_upload_context::framebuffer_storage); + cached_texture_section& region = *find_cached_texture(rsx_range, search_desc, true, true, allow_dirty); + ensure(!region.is_locked()); + + vk::viewable_image* image = nullptr; + if (region.exists()) + { + image = dynamic_cast(region.get_raw_texture()); + bool reusable = true; + + if (flags & texture_create_flags::do_not_reuse) + { + reusable = false; + } + else if (flags & texture_create_flags::shareable) + { + reusable = (image && image->sharing_mode() == VK_SHARING_MODE_CONCURRENT); + } + + if (!reusable || !image || region.get_image_type() != type || image->depth() != depth) // TODO + { + // Incompatible view/type + region.destroy(); + image = nullptr; + } + else + { + ensure(region.is_managed()); + + // Reuse + region.set_rsx_pitch(pitch); + + if (flags & texture_create_flags::initialize_image_contents) + { + // Wipe memory + image->change_layout(cmd, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + + VkImageSubresourceRange range{ image->aspect(), 0, image->mipmaps(), 0, image->layers() }; + if (image->aspect() & VK_IMAGE_ASPECT_COLOR_BIT) + { + VkClearColorValue color = { {0.f, 0.f, 0.f, 1.f} }; + vkCmdClearColorImage(cmd, image->value, image->current_layout, &color, 1, &range); + } + else + { + VkClearDepthStencilValue clear{ 1.f, 255 }; + vkCmdClearDepthStencilImage(cmd, image->value, image->current_layout, &clear, 1, &range); + } + } + } + } + + if (!image) + { + const bool is_cubemap = type == rsx::texture_dimension_extended::texture_dimension_cubemap; + const VkFormat vk_format = get_compatible_sampler_format(m_formats_support, gcm_format); + VkImageCreateFlags create_flags = is_cubemap ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0; + VkSharingMode sharing_mode = (flags & texture_create_flags::shareable) ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE; + + if (auto found = find_cached_image(vk_format, width, height, depth, mipmaps, image_type, create_flags, usage_flags, sharing_mode)) + { + image = found.release(); + } + else + { + if (sharing_mode == VK_SHARING_MODE_CONCURRENT) + { + create_flags |= VK_IMAGE_CREATE_SHAREABLE_RPCS3; + } + + image = new vk::viewable_image(*m_device, + m_memory_types.device_local, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + image_type, vk_format, + width, height, depth, mipmaps, layer, VK_SAMPLE_COUNT_1_BIT, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_TILING_OPTIMAL, usage_flags, create_flags, + VMM_ALLOCATION_POOL_TEXTURE_CACHE, rsx::classify_format(gcm_format)); + } + + // New section, we must prepare it + region.reset(rsx_range); + region.set_gcm_format(gcm_format); + region.set_image_type(type); + region.create(width, height, section_depth, mipmaps, image, pitch, true, gcm_format); + } + + region.set_view_flags(swizzle_flags); + region.set_context(context); + region.set_swizzled(swizzled); + region.set_dirty(false); + + image->native_component_map = apply_component_mapping_flags(gcm_format, swizzle_flags, rsx::default_remap_vector); + + // Its not necessary to lock blit dst textures as they are just reused as necessary + switch (context) + { + case rsx::texture_upload_context::shader_read: + case rsx::texture_upload_context::blit_engine_src: + region.protect(utils::protection::ro); + read_only_range = region.get_min_max(read_only_range, rsx::section_bounds::locked_range); + break; + case rsx::texture_upload_context::blit_engine_dst: + region.set_unpack_swap_bytes(true); + no_access_range = region.get_min_max(no_access_range, rsx::section_bounds::locked_range); + break; + case rsx::texture_upload_context::dma: + case rsx::texture_upload_context::framebuffer_storage: + // Should not be initialized with this method + default: + fmt::throw_exception("Unexpected upload context 0x%x", u32(context)); + } + + update_cache_tag(); + return ®ion; + } + + cached_texture_section* texture_cache::create_nul_section( + vk::command_buffer& /*cmd*/, + const utils::address_range& rsx_range, + const rsx::image_section_attributes_t& attrs, + const rsx::GCM_tile_reference& tile, + bool memory_load) + { + auto& region = *find_cached_texture(rsx_range, { .gcm_format = RSX_GCM_FORMAT_IGNORED }, true, false, false); + ensure(!region.is_locked()); + + // Prepare section + region.reset(rsx_range); + region.create_dma_only(attrs.width, attrs.height, attrs.pitch); + region.set_dirty(false); + region.set_unpack_swap_bytes(true); + + if (memory_load && !tile) // Memory load on DMA tiles will always happen during the actual copy command + { + vk::map_dma(rsx_range.start, rsx_range.length()); + vk::load_dma(rsx_range.start, rsx_range.length()); + } + + no_access_range = region.get_min_max(no_access_range, rsx::section_bounds::locked_range); + update_cache_tag(); + return ®ion; + } + + cached_texture_section* texture_cache::upload_image_from_cpu(vk::command_buffer& cmd, const utils::address_range& rsx_range, u16 width, u16 height, u16 depth, u16 mipmaps, u32 pitch, u32 gcm_format, + rsx::texture_upload_context context, const std::vector& subresource_layout, rsx::texture_dimension_extended type, bool swizzled) + { + if (context != rsx::texture_upload_context::shader_read) + { + if (vk::is_renderpass_open(cmd)) + { + vk::end_renderpass(cmd); + } + } + + const bool upload_async = rsx::get_current_renderer()->get_backend_config().supports_asynchronous_compute; + rsx::flags32_t create_flags = 0; + + if (upload_async && g_fxo->get().is_host_mode()) + { + create_flags |= texture_create_flags::do_not_reuse; + if (m_device->get_graphics_queue() != m_device->get_transfer_queue()) + { + create_flags |= texture_create_flags::shareable; + } + } + + auto section = create_new_texture(cmd, rsx_range, width, height, depth, mipmaps, pitch, gcm_format, context, type, swizzled, + rsx::component_order::default_, create_flags); + + auto image = section->get_raw_texture(); + image->set_debug_name(fmt::format("Raw Texture @0x%x", rsx_range.start)); + + vk::enter_uninterruptible(); + + bool input_swizzled = swizzled; + if (context == rsx::texture_upload_context::blit_engine_src) + { + // Swizzling is ignored for blit engine copy and emulated using remapping + input_swizzled = false; + } + + rsx::flags32_t upload_command_flags = initialize_image_layout | upload_contents_inline; + if (context == rsx::texture_upload_context::shader_read && upload_async) + { + upload_command_flags |= upload_contents_async; + } + + std::vector tmp; + auto p_subresource_layout = &subresource_layout; + u32 heap_align = upload_heap_align_default; + + if (auto tiled_region = rsx::get_current_renderer()->get_tiled_memory_region(rsx_range); + context == rsx::texture_upload_context::blit_engine_src && tiled_region) + { + if (mipmaps > 1) + { + // This really shouldn't happen on framebuffer tiled memory + rsx_log.error("Tiled decode of mipmapped textures is not supported."); + } + else + { + const auto bpp = rsx::get_format_block_size_in_bytes(gcm_format); + const auto [scratch_buf, linear_data_scratch_offset] = vk::detile_memory_block(cmd, tiled_region, rsx_range, width, height, bpp); + + auto subres = subresource_layout.front(); + // FIXME: !!EVIL!! + subres.data = { scratch_buf, linear_data_scratch_offset }; + subres.pitch_in_block = width; + upload_command_flags |= source_is_gpu_resident; + heap_align = width * bpp; + + tmp.push_back(subres); + p_subresource_layout = &tmp; + } + } + + const u16 layer_count = (type == rsx::texture_dimension_extended::texture_dimension_cubemap) ? 6 : 1; + vk::upload_image(cmd, image, *p_subresource_layout, gcm_format, input_swizzled, layer_count, image->aspect(), + *m_texture_upload_heap, heap_align, upload_command_flags); + + vk::leave_uninterruptible(); + + if (context != rsx::texture_upload_context::shader_read) + { + // Insert appropriate barrier depending on use. Shader read resources should be lazy-initialized before consuming. + // TODO: All texture resources should be initialized on use, this is wasteful + + VkImageLayout preferred_layout; + switch (context) + { + default: + preferred_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + break; + case rsx::texture_upload_context::blit_engine_dst: + preferred_layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + break; + case rsx::texture_upload_context::blit_engine_src: + preferred_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + break; + } + + if (preferred_layout != image->current_layout) + { + image->change_layout(cmd, preferred_layout); + } + else + { + // Insert ordering barrier + ensure(preferred_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + insert_image_memory_barrier(cmd, image->value, image->current_layout, preferred_layout, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + { image->aspect(), 0, image->mipmaps(), 0, image->layers() }); + } + } + + section->last_write_tag = rsx::get_shared_tag(); + return section; + } + + void texture_cache::set_component_order(cached_texture_section& section, u32 gcm_format, rsx::component_order expected_flags) + { + if (expected_flags == section.get_view_flags()) + return; + + const VkComponentMapping mapping = apply_component_mapping_flags(gcm_format, expected_flags, rsx::default_remap_vector); + auto image = static_cast(section.get_raw_texture()); + + ensure(image); + image->set_native_component_layout(mapping); + + section.set_view_flags(expected_flags); + } + + void texture_cache::insert_texture_barrier(vk::command_buffer& cmd, vk::image* tex, bool strong_ordering) + { + if (!strong_ordering && tex->current_layout == VK_IMAGE_LAYOUT_GENERAL) + { + // A previous barrier already exists, do nothing + return; + } + + vk::as_rtt(tex)->texture_barrier(cmd); + } + + bool texture_cache::render_target_format_is_compatible(vk::image* tex, u32 gcm_format) + { + auto vk_format = tex->info.format; + switch (gcm_format) + { + default: + //TODO + err_once("Format incompatibility detected, reporting failure to force data copy (VK_FORMAT=0x%X, GCM_FORMAT=0x%X)", static_cast(vk_format), gcm_format); + return false; +#ifndef __APPLE__ + case CELL_GCM_TEXTURE_R5G6B5: + return (vk_format == VK_FORMAT_R5G6B5_UNORM_PACK16); +#else + // R5G6B5 is not supported by Metal + case CELL_GCM_TEXTURE_R5G6B5: + return (vk_format == VK_FORMAT_B8G8R8A8_UNORM); +#endif + case CELL_GCM_TEXTURE_W16_Z16_Y16_X16_FLOAT: + return (vk_format == VK_FORMAT_R16G16B16A16_SFLOAT); + case CELL_GCM_TEXTURE_W32_Z32_Y32_X32_FLOAT: + return (vk_format == VK_FORMAT_R32G32B32A32_SFLOAT); + case CELL_GCM_TEXTURE_X32_FLOAT: + return (vk_format == VK_FORMAT_R32_SFLOAT); + case CELL_GCM_TEXTURE_A8R8G8B8: + case CELL_GCM_TEXTURE_D8R8G8B8: + return (vk_format == VK_FORMAT_B8G8R8A8_UNORM || vk_format == VK_FORMAT_D24_UNORM_S8_UINT || vk_format == VK_FORMAT_D32_SFLOAT_S8_UINT); + case CELL_GCM_TEXTURE_B8: + return (vk_format == VK_FORMAT_R8_UNORM); + case CELL_GCM_TEXTURE_G8B8: + return (vk_format == VK_FORMAT_R8G8_UNORM); + case CELL_GCM_TEXTURE_DEPTH24_D8: + case CELL_GCM_TEXTURE_DEPTH24_D8_FLOAT: + return (vk_format == VK_FORMAT_D24_UNORM_S8_UINT || vk_format == VK_FORMAT_D32_SFLOAT_S8_UINT); + case CELL_GCM_TEXTURE_X16: + case CELL_GCM_TEXTURE_DEPTH16: + case CELL_GCM_TEXTURE_DEPTH16_FLOAT: + return (vk_format == VK_FORMAT_D16_UNORM || vk_format == VK_FORMAT_D32_SFLOAT); + } + } + + void texture_cache::prepare_for_dma_transfers(vk::command_buffer& cmd) + { + if (!cmd.is_recording()) + { + cmd.begin(); + } + } + + void texture_cache::cleanup_after_dma_transfers(vk::command_buffer& cmd) + { + bool occlusion_query_active = !!(cmd.flags & vk::command_buffer::cb_has_open_query); + if (occlusion_query_active) + { + // We really stepped in it + vk::do_query_cleanup(cmd); + } + + // End recording + cmd.end(); + + if (cmd.access_hint != vk::command_buffer::access_type_hint::all) + { + // Flush any pending async jobs in case of blockers + // TODO: Context-level manager should handle this logic + auto async_scheduler = g_fxo->try_get(); + vk::semaphore* async_sema = nullptr; + + if (async_scheduler && async_scheduler->is_recording()) + { + if (async_scheduler->is_host_mode()) + { + async_sema = async_scheduler->get_sema(); + } + else + { + vk::queue_submit_t submit_info{}; + async_scheduler->flush(submit_info, VK_TRUE); + } + } + + // Primary access command queue, must restart it after + vk::fence submit_fence(*m_device); + vk::queue_submit_t submit_info{ m_submit_queue, &submit_fence }; + + if (async_sema) + { + vk::queue_submit_t submit_info2{}; + submit_info2.queue_signal(*async_sema); + async_scheduler->flush(submit_info2, VK_TRUE); + + submit_info.wait_on(*async_sema, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + } + + cmd.submit(submit_info, VK_TRUE); + vk::wait_for_fence(&submit_fence, GENERAL_WAIT_TIMEOUT); + + CHECK_RESULT(vkResetCommandBuffer(cmd, 0)); + cmd.begin(); + } + else + { + // Auxilliary command queue with auto-restart capability + vk::queue_submit_t submit_info{ m_submit_queue, nullptr }; + cmd.submit(submit_info, VK_TRUE); + } + + ensure(cmd.flags == 0); + + if (occlusion_query_active) + { + ensure(cmd.is_recording()); + cmd.flags |= vk::command_buffer::cb_load_occluson_task; + } + } + + void texture_cache::initialize(vk::render_device& device, VkQueue submit_queue, vk::data_heap& upload_heap) + { + m_device = &device; + m_memory_types = device.get_memory_mapping(); + m_formats_support = device.get_formats_support(); + m_submit_queue = submit_queue; + m_texture_upload_heap = &upload_heap; + } + + void texture_cache::destroy() + { + clear(); + } + + bool texture_cache::is_depth_texture(u32 rsx_address, u32 rsx_size) + { + reader_lock lock(m_cache_mutex); + + auto& block = m_storage.block_for(rsx_address); + + if (block.get_locked_count() == 0) + return false; + + for (auto& tex : block) + { + if (tex.is_dirty()) + continue; + + if (!tex.overlaps(rsx_address, rsx::section_bounds::full_range)) + continue; + + if ((rsx_address + rsx_size - tex.get_section_base()) <= tex.get_section_size()) + { + switch (tex.get_format()) + { + case VK_FORMAT_D16_UNORM: + case VK_FORMAT_D32_SFLOAT: + case VK_FORMAT_D32_SFLOAT_S8_UINT: + case VK_FORMAT_D24_UNORM_S8_UINT: + return true; + default: + return false; + } + } + } + + //Unreachable; silence compiler warning anyway + return false; + } + + bool texture_cache::handle_memory_pressure(rsx::problem_severity severity) + { + auto any_released = baseclass::handle_memory_pressure(severity); + + // TODO: This can cause invalidation of in-flight resources + if (severity <= rsx::problem_severity::low || !m_cached_memory_size) + { + // Nothing left to do + return any_released; + } + + constexpr u64 _1M = 0x100000; + if (severity <= rsx::problem_severity::moderate && m_cached_memory_size < (64 * _1M)) + { + // Some memory is consumed by the temporary resources, but no need to panic just yet + return any_released; + } + + std::unique_lock lock(m_cache_mutex, std::defer_lock); + if (!lock.try_lock()) + { + rsx_log.warning("Unable to remove temporary resources because we're already in the texture cache!"); + return any_released; + } + + // Nuke temporary resources. They will still be visible to the GPU. + auto gc = vk::get_resource_manager(); + any_released |= !m_cached_images.empty(); + for (auto& img : m_cached_images) + { + gc->dispose(img.data); + } + m_cached_images.clear(); + m_cached_memory_size = 0; + + any_released |= !m_temporary_subresource_cache.empty(); + for (auto& e : m_temporary_subresource_cache) + { + ensure(e.second.second); + release_temporary_subresource(e.second.second); + } + m_temporary_subresource_cache.clear(); + + return any_released; + } + + void texture_cache::on_frame_end() + { + trim_sections(); + + if (m_storage.m_unreleased_texture_objects >= m_max_zombie_objects) + { + purge_unreleased_sections(); + } + + if (m_cached_images.size() > max_cached_image_pool_size || + m_cached_memory_size > 256 * 0x100000) + { + std::lock_guard lock(m_cached_pool_lock); + + const auto new_size = m_cached_images.size() / 2; + for (usz i = new_size; i < m_cached_images.size(); ++i) + { + m_cached_memory_size -= m_cached_images[i].data->memory->size(); + } + + m_cached_images.resize(new_size); + } + + baseclass::on_frame_end(); + reset_frame_statistics(); + } + + vk::viewable_image* texture_cache::upload_image_simple(vk::command_buffer& cmd, VkFormat format, u32 address, u32 width, u32 height, u32 pitch) + { + bool linear_format_supported = false; + + switch (format) + { + case VK_FORMAT_B8G8R8A8_UNORM: + linear_format_supported = m_formats_support.bgra8_linear; + break; + case VK_FORMAT_R8G8B8A8_UNORM: + linear_format_supported = m_formats_support.argb8_linear; + break; + default: + rsx_log.error("Unsupported VkFormat 0x%x", static_cast(format)); + return nullptr; + } + + if (!linear_format_supported) + { + return nullptr; + } + + // Uploads a linear memory range as a BGRA8 texture + auto image = std::make_unique(*m_device, m_memory_types.host_visible_coherent, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, + VK_IMAGE_TYPE_2D, + format, + width, height, 1, 1, 1, VK_SAMPLE_COUNT_1_BIT, VK_IMAGE_LAYOUT_PREINITIALIZED, + VK_IMAGE_TILING_LINEAR, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, 0, + VMM_ALLOCATION_POOL_SWAPCHAIN); + + VkImageSubresource subresource{}; + subresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + + VkSubresourceLayout layout{}; + vkGetImageSubresourceLayout(*m_device, image->value, &subresource, &layout); + + void* mem = image->memory->map(0, layout.rowPitch * height); + + auto src = vm::_ptr(address); + auto dst = static_cast(mem); + + // TODO: SSE optimization + for (u32 row = 0; row < height; ++row) + { + auto casted_src = reinterpret_cast*>(src); + auto casted_dst = reinterpret_cast(dst); + + for (u32 col = 0; col < width; ++col) + casted_dst[col] = casted_src[col]; + + src += pitch; + dst += layout.rowPitch; + } + + image->memory->unmap(); + + vk::change_image_layout(cmd, image.get(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + + // Fully dispose immediately. These immages aren't really reusable right now. + auto result = image.get(); + vk::get_resource_manager()->dispose(image); + + return result; + } + + bool texture_cache::blit(const rsx::blit_src_info& src, const rsx::blit_dst_info& dst, bool interpolate, vk::surface_cache& m_rtts, vk::command_buffer& cmd) + { + blitter helper; + auto reply = upload_scaled_image(src, dst, interpolate, cmd, m_rtts, helper); + + if (reply.succeeded) + { + if (reply.real_dst_size) + { + flush_if_cache_miss_likely(cmd, reply.to_address_range()); + } + + return true; + } + + return false; + } + + u32 texture_cache::get_unreleased_textures_count() const + { + return baseclass::get_unreleased_textures_count() + ::size32(m_cached_images); + } + + u64 texture_cache::get_temporary_memory_in_use() const + { + // TODO: Technically incorrect, we should have separate metrics for cached evictable resources (this value) and temporary active resources. + return m_cached_memory_size; + } + + bool texture_cache::is_overallocated() const + { + const auto total_device_memory = m_device->get_memory_mapping().device_local_total_bytes / 0x100000; + u64 quota = 0; + + if (total_device_memory >= 2048) + { + quota = std::min(3072, (total_device_memory * 40) / 100); + } + else if (total_device_memory >= 1024) + { + quota = std::max(204, (total_device_memory * 30) / 100); + } + else if (total_device_memory >= 768) + { + quota = 192; + } + else + { + quota = std::min(128, total_device_memory / 2); + } + + quota *= 0x100000; + + if (const u64 texture_cache_pool_usage = vmm_get_application_pool_usage(VMM_ALLOCATION_POOL_TEXTURE_CACHE); + texture_cache_pool_usage > quota) + { + rsx_log.warning("Texture cache is using %lluM of memory which exceeds the allocation quota of %lluM", + texture_cache_pool_usage, quota); + return true; + } + + return false; + } +} + + +void texture_cache::release_temporary_subresource(vk::image_view* view) +{ + if (!view || !view->handle) + { + ppu_log.warning("release_temporary_subresource: Attempted to release a null or invalid image view"); + return; + } + + // Log the release for debugging purposes + ppu_log.debug("Releasing temporary Vulkan image view: {}", view->handle); + + // Destroy the Vulkan image view + vkDestroyImageView(m_device, view->handle, nullptr); + view->handle = VK_NULL_HANDLE; // Invalidate the handle + + // Clean up associated resources + delete view; +} + + +void texture_cache::release_temporary_subresource(std::unique_ptr& view) +{ + if (!view || view->m_view == VK_NULL_HANDLE) + { + ppu_log.warning("release_temporary_subresource: Attempted to release a null or invalid image view"); + return; + } + + // Automatically clean up resources when the unique_ptr is reset or goes out of scope + ppu_log.debug("Releasing Vulkan image view: {}", view->m_view); + view.reset(); // Reset the unique_ptr to invoke the RAII destructor +} + +// Refactor texture cache cleanup using RAII +void texture_cache::destroy() +{ + ppu_log.debug("Destroying texture cache and releasing resources"); + + // Release all cached texture sections + for (auto& section : m_cached_sections) + { + on_section_destroyed(section); + } + m_cached_sections.clear(); + + // Purge unused temporary resources + purge_unreleased_sections(); + + ppu_log.info("Texture cache successfully destroyed"); +} diff --git a/VKTextureCache.h b/VKTextureCache.h new file mode 100644 index 0000000000..11da058c9d --- /dev/null +++ b/VKTextureCache.h @@ -0,0 +1,694 @@ +#pragma once + +#include "VKAsyncScheduler.h" +#include "VKDMA.h" +#include "VKRenderTargets.h" +#include "VKResourceManager.h" +#include "VKRenderPass.h" +#include "vkutils/image_helpers.h" + +#include "../Common/texture_cache.h" +#include "../Common/tiled_dma_copy.hpp" + +#include "Emu/Cell/timers.hpp" + +#include +#include + +namespace vk +{ + class cached_texture_section; + class texture_cache; + + struct texture_cache_traits + { + using commandbuffer_type = vk::command_buffer; + using section_storage_type = vk::cached_texture_section; + using texture_cache_type = vk::texture_cache; + using texture_cache_base_type = rsx::texture_cache; + using image_resource_type = vk::image*; + using image_view_type = vk::image_view*; + using image_storage_type = vk::image; + using texture_format = VkFormat; + using viewable_image_type = vk::viewable_image*; + }; + + class cached_texture_section : public rsx::cached_texture_section + { + using baseclass = typename rsx::cached_texture_section; + friend baseclass; + + std::unique_ptr managed_texture = nullptr; + + //DMA relevant data + std::unique_ptr dma_fence; + vk::render_device* m_device = nullptr; + vk::viewable_image* vram_texture = nullptr; + + public: + using baseclass::cached_texture_section; + + void create(u16 w, u16 h, u16 depth, u16 mipmaps, vk::image* image, u32 rsx_pitch, bool managed, u32 gcm_format, bool pack_swap_bytes = false) + { + if (vram_texture && !managed_texture && get_protection() == utils::protection::no) + { + // In-place image swap, still locked. Likely a color buffer that got rebound as depth buffer or vice-versa. + vk::as_rtt(vram_texture)->on_swap_out(); + + if (!managed) + { + // Incoming is also an external resource, reference it immediately + vk::as_rtt(image)->on_swap_in(is_locked()); + } + } + + auto new_texture = static_cast(image); + ensure(!exists() || !is_managed() || vram_texture == new_texture); + vram_texture = new_texture; + + ensure(rsx_pitch); + + width = w; + height = h; + this->depth = depth; + this->mipmaps = mipmaps; + this->rsx_pitch = rsx_pitch; + + this->gcm_format = gcm_format; + this->pack_unpack_swap_bytes = pack_swap_bytes; + + if (managed) + { + managed_texture.reset(vram_texture); + } + + if (auto rtt = dynamic_cast(image)) + { + swizzled = (rtt->raster_type != rsx::surface_raster_type::linear); + } + + if (synchronized) + { + // Even if we are managing the same vram section, we cannot guarantee contents are static + // The create method is only invoked when a new managed session is required + release_dma_resources(); + synchronized = false; + flushed = false; + sync_timestamp = 0ull; + } + + // Notify baseclass + baseclass::on_section_resources_created(); + } + + void release_dma_resources() + { + if (dma_fence) + { + auto gc = vk::get_resource_manager(); + gc->dispose(dma_fence); + } + } + + void dma_abort() override + { + // Called if a reset occurs, usually via reprotect path after a bad prediction. + // Discard the sync event, the next sync, if any, will properly recreate this. + ensure(synchronized); + ensure(!flushed); + ensure(dma_fence); + vk::get_resource_manager()->dispose(dma_fence); + } + + void destroy() + { + if (!exists() && context != rsx::texture_upload_context::dma) + return; + + m_tex_cache->on_section_destroyed(*this); + + vram_texture = nullptr; + ensure(!managed_texture); + release_dma_resources(); + + baseclass::on_section_resources_destroyed(); + } + + bool exists() const + { + return (vram_texture != nullptr); + } + + bool is_managed() const + { + return !exists() || managed_texture; + } + + vk::image_view* get_view(const rsx::texture_channel_remap_t& remap) + { + ensure(vram_texture != nullptr); + return vram_texture->get_view(remap); + } + + vk::image_view* get_raw_view() + { + ensure(vram_texture != nullptr); + return vram_texture->get_view(rsx::default_remap_vector); + } + + vk::viewable_image* get_raw_texture() + { + return managed_texture.get(); + } + + std::unique_ptr& get_texture() + { + return managed_texture; + } + + vk::render_target* get_render_target() const + { + return vk::as_rtt(vram_texture); + } + + VkFormat get_format() const + { + if (context == rsx::texture_upload_context::dma) + { + return VK_FORMAT_R32_UINT; + } + + ensure(vram_texture != nullptr); + return vram_texture->format(); + } + + bool is_flushed() const + { + //This memory section was flushable, but a flush has already removed protection + return flushed; + } + + void dma_transfer(vk::command_buffer& cmd, vk::image* src, const areai& src_area, const utils::address_range& valid_range, u32 pitch); + + void copy_texture(vk::command_buffer& cmd, bool miss) + { + ensure(exists()); + + if (!miss) [[likely]] + { + ensure(!synchronized); + baseclass::on_speculative_flush(); + } + else + { + baseclass::on_miss(); + } + + if (m_device == nullptr) + { + m_device = &cmd.get_command_pool().get_owner(); + } + + vk::image* locked_resource = vram_texture; + u32 transfer_width = width; + u32 transfer_height = height; + u32 transfer_x = 0, transfer_y = 0; + + if (context == rsx::texture_upload_context::framebuffer_storage) + { + auto surface = vk::as_rtt(vram_texture); + surface->memory_barrier(cmd, rsx::surface_access::transfer_read); + locked_resource = surface->get_surface(rsx::surface_access::transfer_read); + transfer_width *= surface->samples_x; + transfer_height *= surface->samples_y; + } + + vk::image* target = locked_resource; + if (transfer_width != locked_resource->width() || transfer_height != locked_resource->height()) + { + // TODO: Synchronize access to typeles textures + target = vk::get_typeless_helper(vram_texture->format(), vram_texture->format_class(), transfer_width, transfer_height); + target->change_layout(cmd, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + + // Allow bilinear filtering on color textures where compatibility is likely + const auto filter = (target->aspect() == VK_IMAGE_ASPECT_COLOR_BIT) ? VK_FILTER_LINEAR : VK_FILTER_NEAREST; + + vk::copy_scaled_image(cmd, locked_resource, target, + { 0, 0, static_cast(locked_resource->width()), static_cast(locked_resource->height()) }, + { 0, 0, static_cast(transfer_width), static_cast(transfer_height) }, + 1, true, filter); + + target->change_layout(cmd, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + } + + const auto internal_bpp = vk::get_format_texel_width(vram_texture->format()); + const auto valid_range = get_confirmed_range(); + + if (const auto section_range = get_section_range(); section_range != valid_range) + { + if (const auto offset = (valid_range.start - get_section_base())) + { + transfer_y = offset / rsx_pitch; + transfer_x = (offset % rsx_pitch) / internal_bpp; + + ensure(transfer_width >= transfer_x); + ensure(transfer_height >= transfer_y); + transfer_width -= transfer_x; + transfer_height -= transfer_y; + } + + if (const auto tail = (section_range.end - valid_range.end)) + { + const auto row_count = tail / rsx_pitch; + + ensure(transfer_height >= row_count); + transfer_height -= row_count; + } + } + + areai src_area; + src_area.x1 = static_cast(transfer_x); + src_area.y1 = static_cast(transfer_y); + src_area.x2 = s32(transfer_x + transfer_width); + src_area.y2 = s32(transfer_y + transfer_height); + dma_transfer(cmd, target, src_area, valid_range, rsx_pitch); + } + + /** + * Flush + */ + void imp_flush() override + { + AUDIT(synchronized); + + // Synchronize, reset dma_fence after waiting + vk::wait_for_event(dma_fence.get(), GENERAL_WAIT_TIMEOUT); + + // Calculate smallest range to flush - for framebuffers, the raster region is enough + const auto range = (context == rsx::texture_upload_context::framebuffer_storage) ? get_section_range() : get_confirmed_range(); + auto flush_length = range.length(); + + const auto tiled_region = rsx::get_current_renderer()->get_tiled_memory_region(range); + if (tiled_region) + { + const auto available_tile_size = tiled_region.tile->size - (range.start - tiled_region.base_address); + const auto max_content_size = tiled_region.tile->pitch * utils::align(height, 64); + flush_length = std::min(max_content_size, available_tile_size); + } + + vk::flush_dma(range.start, flush_length); + +#if DEBUG_DMA_TILING + // Are we a tiled region? + if (const auto tiled_region = rsx::get_current_renderer()->get_tiled_memory_region(range)) + { + auto real_data = vm::get_super_ptr(range.start); + auto out_data = std::vector(tiled_region.tile->size); + rsx::tile_texel_data( + out_data.data(), + real_data, + tiled_region.base_address, + range.start - tiled_region.base_address, + tiled_region.tile->size, + tiled_region.tile->bank, + tiled_region.tile->pitch, + width, + height + ); + std::memcpy(real_data, out_data.data(), flush_length); + } +#endif + + if (is_swizzled()) + { + // This format is completely worthless to CPU processing algorithms where cache lines on die are linear. + // If this is happening, usually it means it was not a planned readback (e.g shared pages situation) + rsx_log.trace("[Performance warning] CPU readback of swizzled data"); + + // Read-modify-write to avoid corrupting already resident memory outside texture region + void* data = get_ptr(range.start); + std::vector tmp_data(rsx_pitch * height); + std::memcpy(tmp_data.data(), data, tmp_data.size()); + + switch (gcm_format) + { + case CELL_GCM_TEXTURE_A8R8G8B8: + case CELL_GCM_TEXTURE_DEPTH24_D8: + rsx::convert_linear_swizzle(tmp_data.data(), data, width, height, rsx_pitch); + break; + case CELL_GCM_TEXTURE_R5G6B5: + case CELL_GCM_TEXTURE_DEPTH16: + rsx::convert_linear_swizzle(tmp_data.data(), data, width, height, rsx_pitch); + break; + default: + rsx_log.error("Unexpected swizzled texture format 0x%x", gcm_format); + } + } + } + + void* map_synchronized(u32, u32) + { + return nullptr; + } + + void finish_flush() + {} + + /** + * Misc + */ + void set_unpack_swap_bytes(bool swap_bytes) + { + pack_unpack_swap_bytes = swap_bytes; + } + + void set_rsx_pitch(u32 pitch) + { + ensure(!is_locked()); + rsx_pitch = pitch; + } + + void sync_surface_memory(const std::vector& surfaces) + { + auto rtt = vk::as_rtt(vram_texture); + rtt->sync_tag(); + + for (auto& surface : surfaces) + { + rtt->inherit_surface_contents(vk::as_rtt(surface->vram_texture)); + } + } + + bool has_compatible_format(vk::image* tex) const + { + return vram_texture->info.format == tex->info.format; + } + + bool is_depth_texture() const + { + return !!(vram_texture->aspect() & VK_IMAGE_ASPECT_DEPTH_BIT); + } + }; + + class texture_cache : public rsx::texture_cache + { + private: + using baseclass = rsx::texture_cache; + friend baseclass; + + struct cached_image_reference_t + { + std::unique_ptr data; + texture_cache* parent; + + cached_image_reference_t(texture_cache* parent, std::unique_ptr& previous); + ~cached_image_reference_t(); + }; + + struct cached_image_t + { + u64 key; + std::unique_ptr data; + + cached_image_t() = default; + cached_image_t(u64 key_, std::unique_ptr& data_) : + key(key_), data(std::move(data_)) {} + }; + + public: + enum texture_create_flags : u32 + { + initialize_image_contents = 1, + do_not_reuse = 2, + shareable = 4 + }; + + void on_section_destroyed(cached_texture_section& tex) override; + + private: + + // Vulkan internals + vk::render_device* m_device; + vk::memory_type_mapping m_memory_types; + vk::gpu_formats_support m_formats_support; + VkQueue m_submit_queue; + vk::data_heap* m_texture_upload_heap; + + // Stuff that has been dereferenced by the GPU goes into these + const u32 max_cached_image_pool_size = 256; + std::deque m_cached_images; + atomic_t m_cached_memory_size = { 0 }; + shared_mutex m_cached_pool_lock; + + // Blocks some operations when exiting + atomic_t m_cache_is_exiting = false; + + void clear(); + + VkComponentMapping apply_component_mapping_flags(u32 gcm_format, rsx::component_order flags, const rsx::texture_channel_remap_t& remap_vector) const; + + void copy_transfer_regions_impl(vk::command_buffer& cmd, vk::image* dst, const std::vector& sections_to_transfer) const; + + vk::image* get_template_from_collection_impl(const std::vector& sections_to_transfer) const; + + std::unique_ptr find_cached_image(VkFormat format, u16 w, u16 h, u16 d, u16 mipmaps, VkImageType type, VkImageCreateFlags create_flags, VkImageUsageFlags usage, VkSharingMode sharing); + + protected: + vk::image_view* create_temporary_subresource_view_impl(vk::command_buffer& cmd, vk::image* source, VkImageType image_type, VkImageViewType view_type, + u32 gcm_format, u16 x, u16 y, u16 w, u16 h, u16 d, u8 mips, const rsx::texture_channel_remap_t& remap_vector, bool copy); + + vk::image_view* create_temporary_subresource_view(vk::command_buffer& cmd, vk::image* source, u32 gcm_format, + u16 x, u16 y, u16 w, u16 h, const rsx::texture_channel_remap_t& remap_vector) override; + + vk::image_view* create_temporary_subresource_view(vk::command_buffer& cmd, vk::image** source, u32 gcm_format, + u16 x, u16 y, u16 w, u16 h, const rsx::texture_channel_remap_t& remap_vector) override; + + vk::image_view* generate_cubemap_from_images(vk::command_buffer& cmd, u32 gcm_format, u16 size, + const std::vector& sections_to_copy, const rsx::texture_channel_remap_t& remap_vector) override; + + vk::image_view* generate_3d_from_2d_images(vk::command_buffer& cmd, u32 gcm_format, u16 width, u16 height, u16 depth, + const std::vector& sections_to_copy, const rsx::texture_channel_remap_t& remap_vector) override; + + vk::image_view* generate_atlas_from_images(vk::command_buffer& cmd, u32 gcm_format, u16 width, u16 height, + const std::vector& sections_to_copy, const rsx::texture_channel_remap_t& remap_vector) override; + + vk::image_view* generate_2d_mipmaps_from_images(vk::command_buffer& cmd, u32 gcm_format, u16 width, u16 height, + const std::vector& sections_to_copy, const rsx::texture_channel_remap_t& remap_vector) override; + + void release_temporary_subresource(vk::image_view* view) override; + + void update_image_contents(vk::command_buffer& cmd, vk::image_view* dst_view, vk::image* src, u16 width, u16 height) override; + + cached_texture_section* create_new_texture(vk::command_buffer& cmd, const utils::address_range& rsx_range, u16 width, u16 height, u16 depth, u16 mipmaps, u32 pitch, + u32 gcm_format, rsx::texture_upload_context context, rsx::texture_dimension_extended type, bool swizzled, rsx::component_order swizzle_flags, rsx::flags32_t flags) override; + + cached_texture_section* create_nul_section(vk::command_buffer& cmd, const utils::address_range& rsx_range, const rsx::image_section_attributes_t& attrs, + const rsx::GCM_tile_reference& tile, bool memory_load) override; + + cached_texture_section* upload_image_from_cpu(vk::command_buffer& cmd, const utils::address_range& rsx_range, u16 width, u16 height, u16 depth, u16 mipmaps, u32 pitch, u32 gcm_format, + rsx::texture_upload_context context, const std::vector& subresource_layout, rsx::texture_dimension_extended type, bool swizzled) override; + + void set_component_order(cached_texture_section& section, u32 gcm_format, rsx::component_order expected_flags) override; + + void insert_texture_barrier(vk::command_buffer& cmd, vk::image* tex, bool strong_ordering) override; + + bool render_target_format_is_compatible(vk::image* tex, u32 gcm_format) override; + + void prepare_for_dma_transfers(vk::command_buffer& cmd) override; + + void cleanup_after_dma_transfers(vk::command_buffer& cmd) override; + + public: + using baseclass::texture_cache; + + void initialize(vk::render_device& device, VkQueue submit_queue, vk::data_heap& upload_heap); + + void destroy() override; + + std::unique_ptr create_temporary_subresource_storage( + rsx::format_class format_class, VkFormat format, + u16 width, u16 height, u16 depth, u16 layers, u8 mips, + VkImageType image_type, VkFlags image_flags, VkFlags usage_flags); + + void dispose_reusable_image(std::unique_ptr& tex); + + bool is_depth_texture(u32 rsx_address, u32 rsx_size) override; + + void on_frame_end() override; + + vk::viewable_image* upload_image_simple(vk::command_buffer& cmd, VkFormat format, u32 address, u32 width, u32 height, u32 pitch); + + bool blit(const rsx::blit_src_info& src, const rsx::blit_dst_info& dst, bool interpolate, vk::surface_cache& m_rtts, vk::command_buffer& cmd); + + u32 get_unreleased_textures_count() const override; + + bool handle_memory_pressure(rsx::problem_severity severity) override; + + u64 get_temporary_memory_in_use() const; + + bool is_overallocated() const; + }; +} + + +#include + +// Wrapper for VkImageView +class vk_image_view_wrapper +{ +public: + VkDevice m_device = VK_NULL_HANDLE; + VkImageView m_view = VK_NULL_HANDLE; + + vk_image_view_wrapper(VkDevice device, VkImageView view) + : m_device(device), m_view(view) {} + + ~vk_image_view_wrapper() + { + if (m_view != VK_NULL_HANDLE) + { + vkDestroyImageView(m_device, m_view, nullptr); + m_view = VK_NULL_HANDLE; + } + } + + vk_image_view_wrapper(const vk_image_view_wrapper&) = delete; + vk_image_view_wrapper& operator=(const vk_image_view_wrapper&) = delete; + + vk_image_view_wrapper(vk_image_view_wrapper&& other) noexcept + : m_device(other.m_device), m_view(other.m_view) + { + other.m_view = VK_NULL_HANDLE; + } + + vk_image_view_wrapper& operator=(vk_image_view_wrapper&& other) noexcept + { + if (this != &other) + { + m_device = other.m_device; + m_view = other.m_view; + other.m_view = VK_NULL_HANDLE; + } + return *this; + } +}; + +// Wrapper for VkBuffer +class vk_buffer_wrapper +{ +public: + VkDevice m_device = VK_NULL_HANDLE; + VkBuffer m_buffer = VK_NULL_HANDLE; + + vk_buffer_wrapper(VkDevice device, VkBuffer buffer) + : m_device(device), m_buffer(buffer) {} + + ~vk_buffer_wrapper() + { + if (m_buffer != VK_NULL_HANDLE) + { + vkDestroyBuffer(m_device, m_buffer, nullptr); + m_buffer = VK_NULL_HANDLE; + } + } + + vk_buffer_wrapper(const vk_buffer_wrapper&) = delete; + vk_buffer_wrapper& operator=(const vk_buffer_wrapper&) = delete; + + vk_buffer_wrapper(vk_buffer_wrapper&& other) noexcept + : m_device(other.m_device), m_buffer(other.m_buffer) + { + other.m_buffer = VK_NULL_HANDLE; + } + + vk_buffer_wrapper& operator=(vk_buffer_wrapper&& other) noexcept + { + if (this != &other) + { + m_device = other.m_device; + m_buffer = other.m_buffer; + other.m_buffer = VK_NULL_HANDLE; + } + return *this; + } +}; + +// Wrapper for VkCommandPool +class vk_command_pool_wrapper +{ +public: + VkDevice m_device = VK_NULL_HANDLE; + VkCommandPool m_command_pool = VK_NULL_HANDLE; + + vk_command_pool_wrapper(VkDevice device, VkCommandPool command_pool) + : m_device(device), m_command_pool(command_pool) {} + + ~vk_command_pool_wrapper() + { + if (m_command_pool != VK_NULL_HANDLE) + { + vkDestroyCommandPool(m_device, m_command_pool, nullptr); + m_command_pool = VK_NULL_HANDLE; + } + } + + vk_command_pool_wrapper(const vk_command_pool_wrapper&) = delete; + vk_command_pool_wrapper& operator=(const vk_command_pool_wrapper&) = delete; + + vk_command_pool_wrapper(vk_command_pool_wrapper&& other) noexcept + : m_device(other.m_device), m_command_pool(other.m_command_pool) + { + other.m_command_pool = VK_NULL_HANDLE; + } + + vk_command_pool_wrapper& operator=(vk_command_pool_wrapper&& other) noexcept + { + if (this != &other) + { + m_device = other.m_device; + m_command_pool = other.m_command_pool; + other.m_command_pool = VK_NULL_HANDLE; + } + return *this; + } +}; + +// Wrapper for VkDeviceMemory +class vk_device_memory_wrapper +{ +public: + VkDevice m_device = VK_NULL_HANDLE; + VkDeviceMemory m_memory = VK_NULL_HANDLE; + + vk_device_memory_wrapper(VkDevice device, VkDeviceMemory memory) + : m_device(device), m_memory(memory) {} + + ~vk_device_memory_wrapper() + { + if (m_memory != VK_NULL_HANDLE) + { + vkFreeMemory(m_device, m_memory, nullptr); + m_memory = VK_NULL_HANDLE; + } + } + + vk_device_memory_wrapper(const vk_device_memory_wrapper&) = delete; + vk_device_memory_wrapper& operator=(const vk_device_memory_wrapper&) = delete; + + vk_device_memory_wrapper(vk_device_memory_wrapper&& other) noexcept + : m_device(other.m_device), m_memory(other.m_memory) + { + other.m_memory = VK_NULL_HANDLE; + } + + vk_device_memory_wrapper& operator=(vk_device_memory_wrapper&& other) noexcept + { + if (this != &other) + { + m_device = other.m_device; + m_memory = other.m_memory; + other.m_memory = VK_NULL_HANDLE; + } + return *this; + } +}; diff --git a/vfs_dialog.cpp b/vfs_dialog.cpp new file mode 100644 index 0000000000..89c1322f73 --- /dev/null +++ b/vfs_dialog.cpp @@ -0,0 +1,266 @@ +#include "vfs_dialog.h" +#include "vfs_dialog_tab.h" +#include "vfs_dialog_usb_tab.h" +#include "gui_settings.h" + +#include +#include +#include +#include +#include + +#include "Emu/System.h" +#include "Emu/vfs_config.h" + +vfs_dialog::vfs_dialog(std::shared_ptr _gui_settings, QWidget* parent) + : QDialog(parent), m_gui_settings(std::move(_gui_settings)) +{ + setWindowTitle(tr("Virtual File System")); + setObjectName("vfs_dialog"); + + QTabWidget* tabs = new QTabWidget(); + tabs->setUsesScrollButtons(false); + + g_cfg_vfs.load(); + + // Create tabs + vfs_dialog_tab* emulator_tab = new vfs_dialog_tab("$(EmulatorDir)", gui::fs_emulator_dir_list, &g_cfg_vfs.emulator_dir, m_gui_settings, this); + vfs_dialog_tab* dev_hdd0_tab = new vfs_dialog_tab("dev_hdd0", gui::fs_dev_hdd0_list, &g_cfg_vfs.dev_hdd0, m_gui_settings, this); + vfs_dialog_tab* dev_hdd1_tab = new vfs_dialog_tab("dev_hdd1", gui::fs_dev_hdd1_list, &g_cfg_vfs.dev_hdd1, m_gui_settings, this); + vfs_dialog_tab* dev_flash_tab = new vfs_dialog_tab("dev_flash", gui::fs_dev_flash_list, &g_cfg_vfs.dev_flash, m_gui_settings, this); + vfs_dialog_tab* dev_flash2_tab = new vfs_dialog_tab("dev_flash2", gui::fs_dev_flash2_list, &g_cfg_vfs.dev_flash2, m_gui_settings, this); + vfs_dialog_tab* dev_flash3_tab = new vfs_dialog_tab("dev_flash3", gui::fs_dev_flash3_list, &g_cfg_vfs.dev_flash3, m_gui_settings, this); + vfs_dialog_tab* dev_bdvd_tab = new vfs_dialog_tab("dev_bdvd", gui::fs_dev_bdvd_list, &g_cfg_vfs.dev_bdvd, m_gui_settings, this); + vfs_dialog_usb_tab* dev_usb_tab = new vfs_dialog_usb_tab(&g_cfg_vfs.dev_usb, m_gui_settings, this); + vfs_dialog_tab* games_tab = new vfs_dialog_tab("games", gui::fs_games_list, &g_cfg_vfs.games_dir, m_gui_settings, this); + + tabs->addTab(emulator_tab, "$(EmulatorDir)"); + tabs->addTab(dev_hdd0_tab, "dev_hdd0"); + tabs->addTab(dev_hdd1_tab, "dev_hdd1"); + tabs->addTab(dev_flash_tab, "dev_flash"); + tabs->addTab(dev_flash2_tab, "dev_flash2"); + tabs->addTab(dev_flash3_tab, "dev_flash3"); + tabs->addTab(dev_bdvd_tab, "dev_bdvd"); + tabs->addTab(dev_usb_tab, "dev_usb"); + tabs->addTab(games_tab, "games"); + + // Create buttons + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Close | QDialogButtonBox::Save | QDialogButtonBox::RestoreDefaults); + buttons->button(QDialogButtonBox::RestoreDefaults)->setText(tr("Reset Directories")); + buttons->button(QDialogButtonBox::Save)->setDefault(true); + + connect(buttons, &QDialogButtonBox::clicked, this, [this, buttons, tabs](QAbstractButton* button) + { + if (button == buttons->button(QDialogButtonBox::RestoreDefaults)) + { + if (QMessageBox::question(this, tr("Confirm Reset"), tr("Reset all file system directories?")) != QMessageBox::Yes) + return; + + for (int i = 0; i < tabs->count(); ++i) + { + if (tabs->tabText(i) == "dev_usb") + { + static_cast(tabs->widget(i))->reset(); + } + else + { + static_cast(tabs->widget(i))->reset(); + } + } + } + else if (button == buttons->button(QDialogButtonBox::Save)) + { + for (int i = 0; i < tabs->count(); ++i) + { + if (tabs->tabText(i) == "dev_usb") + { + static_cast(tabs->widget(i))->set_settings(); + } + else + { + static_cast(tabs->widget(i))->set_settings(); + } + } + + g_cfg_vfs.save(); + + // Recreate folder structure for new VFS paths + if (Emu.IsStopped()) + { + Emu.Init(); + } + + accept(); + } + else if (button == buttons->button(QDialogButtonBox::Close)) + { + reject(); + } + }); + + QVBoxLayout* vbox = new QVBoxLayout; + vbox->addWidget(tabs); + vbox->addWidget(buttons); + + setLayout(vbox); + + buttons->button(QDialogButtonBox::Save)->setFocus(); +} + + +#include +#include + +void vfs_dialog::setup_disc_features() +{ + // Create a layout for disc-related actions + QVBoxLayout* disc_layout = new QVBoxLayout; + + // Add "Mount Disc" button + QPushButton* mount_disc_button = new QPushButton(tr("Mount Disc"), this); + connect(mount_disc_button, &QPushButton::clicked, this, &vfs_dialog::on_mount_disc_clicked); + disc_layout->addWidget(mount_disc_button); + + // Add "Mount ISO" button + QPushButton* mount_iso_button = new QPushButton(tr("Mount ISO"), this); + connect(mount_iso_button, &QPushButton::clicked, this, &vfs_dialog::on_mount_iso_clicked); + disc_layout->addWidget(mount_iso_button); + + // Add "Unmount Disc" button + QPushButton* unmount_disc_button = new QPushButton(tr("Unmount Disc"), this); + connect(unmount_disc_button, &QPushButton::clicked, this, &vfs_dialog::on_unmount_disc_clicked); + disc_layout->addWidget(unmount_disc_button); + + // Add the layout to the VFS dialog + layout()->addLayout(disc_layout); +} + +// Slot for "Mount Disc" button +void vfs_dialog::on_mount_disc_clicked() +{ + QString disc_path = QFileDialog::getExistingDirectory(this, tr("Select Disc Path")); + if (!disc_path.isEmpty()) + { + if (!vfs::mount("/mnt/ps3_disc", disc_path.toStdString())) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to mount the disc.")); + } + else + { + QMessageBox::information(this, tr("Success"), tr("Disc mounted successfully.")); + } + } +} + +// Slot for "Mount ISO" button +void vfs_dialog::on_mount_iso_clicked() +{ + QString iso_path = QFileDialog::getOpenFileName(this, tr("Select ISO File"), QString(), tr("ISO Files (*.iso)")); + if (!iso_path.isEmpty()) + { + if (!vfs::mount_iso("/mnt/ps3_iso", iso_path.toStdString())) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to mount the ISO.")); + } + else + { + QMessageBox::information(this, tr("Success"), tr("ISO mounted successfully.")); + } + } +} + +// Slot for "Unmount Disc" button +void vfs_dialog::on_unmount_disc_clicked() +{ + if (!vfs::unmount("/mnt/ps3_disc")) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to unmount the disc.")); + } + else + { + QMessageBox::information(this, tr("Success"), tr("Disc unmounted successfully.")); + } +} + + +#include +#include +#include + +void vfs_dialog::setup_metadata_display() +{ + // Create a layout for the metadata section + QVBoxLayout* metadata_layout = new QVBoxLayout; + + // Add a label for metadata title + QLabel* metadata_label = new QLabel(tr("Game Metadata"), this); + metadata_layout->addWidget(metadata_label); + + // Add a table for displaying metadata + m_metadata_table = new QTableWidget(this); + m_metadata_table->setColumnCount(2); + m_metadata_table->setHorizontalHeaderLabels({tr("Field"), tr("Value")}); + metadata_layout->addWidget(m_metadata_table); + + // Add the layout to the VFS dialog + layout()->addLayout(metadata_layout); +} + +// Populate metadata table +void vfs_dialog::update_metadata_display(const std::string& param_sfo_path) +{ + // Extract metadata using vfs::parse_param_sfo + std::map metadata; + if (!vfs::parse_param_sfo(param_sfo_path, metadata)) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to parse game metadata.")); + return; + } + + // Clear existing metadata + m_metadata_table->setRowCount(0); + + // Populate the table with extracted metadata + int row = 0; + for (const auto& [field, value] : metadata) + { + m_metadata_table->insertRow(row); + m_metadata_table->setItem(row, 0, new QTableWidgetItem(QString::fromStdString(field))); + m_metadata_table->setItem(row, 1, new QTableWidgetItem(QString::fromStdString(value))); + ++row; + } +} + +void vfs_dialog::on_mount_disc_clicked() +{ + QString disc_path = QFileDialog::getExistingDirectory(this, tr("Select Disc Path")); + if (!disc_path.isEmpty()) + { + if (!vfs::mount("/mnt/ps3_disc", disc_path.toStdString())) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to mount the disc.")); + } + else + { + QMessageBox::information(this, tr("Success"), tr("Disc mounted successfully.")); + update_metadata_display("/mnt/ps3_disc/PS3_GAME/PARAM.SFO"); + } + } +} + +void vfs_dialog::on_mount_iso_clicked() +{ + QString iso_path = QFileDialog::getOpenFileName(this, tr("Select ISO File"), QString(), tr("ISO Files (*.iso)")); + if (!iso_path.isEmpty()) + { + if (!vfs::mount_iso("/mnt/ps3_iso", iso_path.toStdString())) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to mount the ISO.")); + } + else + { + QMessageBox::information(this, tr("Success"), tr("ISO mounted successfully.")); + update_metadata_display("/mnt/ps3_iso/PS3_GAME/PARAM.SFO"); + } + } +} diff --git a/vfs_dialog.h b/vfs_dialog.h new file mode 100644 index 0000000000..e5c1fa6dec --- /dev/null +++ b/vfs_dialog.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +class gui_settings; + +class vfs_dialog : public QDialog +{ + Q_OBJECT + +public: + explicit vfs_dialog(std::shared_ptr _gui_settings, QWidget* parent = nullptr); +private: + std::shared_ptr m_gui_settings; +}; + + +private: + QTableWidget* m_metadata_table; + + void setup_metadata_display(); + void update_metadata_display(const std::string& param_sfo_path);