From 3330f835f4fc8753678efc6aeec7b68f7107af3b Mon Sep 17 00:00:00 2001 From: vincentadamnemessis Date: Mon, 22 Jun 2026 02:41:46 +0800 Subject: [PATCH 1/3] video_core: handle optional custom texture maps safely Skip invalid optional custom texture maps instead of failing the whole material. Keep the base color map strict, fall back to the base Vulkan image when an optional custom map is absent, and avoid allocating custom normal-map images on Vulkan until that backend binds them. Also clear macOS bundle extended attributes before ad-hoc signing so macdeployqt output from FileProvider-backed folders can be packaged locally. --- CMakeModules/BundleTarget.cmake | 9 ++++++ src/video_core/custom_textures/material.cpp | 28 +++++++++++++++---- src/video_core/custom_textures/material.h | 22 +++++++-------- .../rasterizer_cache/surface_base.cpp | 7 ++++- .../rasterizer_cache/texture_codec.h | 4 +-- .../renderer_vulkan/vk_texture_runtime.cpp | 21 ++++++++++---- .../renderer_vulkan/vk_texture_runtime.h | 7 ++++- 7 files changed, 72 insertions(+), 26 deletions(-) diff --git a/CMakeModules/BundleTarget.cmake b/CMakeModules/BundleTarget.cmake index 45e5ee296..713a4cab1 100644 --- a/CMakeModules/BundleTarget.cmake +++ b/CMakeModules/BundleTarget.cmake @@ -93,6 +93,15 @@ if (BUNDLE_TARGET_EXECUTE) message(FATAL_ERROR "macdeployqt failed: ${macdeployqt_result}") endif() + # Some local filesystem providers attach FinderInfo/resource-fork xattrs during copy, + # which causes codesign to reject the bundle. + find_program(xattr_executable xattr REQUIRED) + execute_process(COMMAND "${xattr_executable}" -cr "${executable_path}" + RESULT_VARIABLE xattr_result) + if (NOT xattr_result EQUAL "0") + message(FATAL_ERROR "xattr cleanup failed: ${xattr_result}") + endif() + # Bundling libraries can rewrite path information and break code signatures of system libraries. # Perform an ad-hoc re-signing on the whole app bundle to fix this. execute_process(COMMAND codesign --deep -fs - "${executable_path}" diff --git a/src/video_core/custom_textures/material.cpp b/src/video_core/custom_textures/material.cpp index d7bee5da4..ee7030276 100644 --- a/src/video_core/custom_textures/material.cpp +++ b/src/video_core/custom_textures/material.cpp @@ -100,13 +100,23 @@ void Material::LoadFromDisk(bool flip_png) noexcept { if (IsDecoded()) { return; } - for (CustomTexture* const texture : textures) { + for (std::size_t index = 0; index < textures.size(); index++) { + CustomTexture* const texture = textures[index]; if (!texture || texture->IsLoaded()) { continue; } texture->LoadFromDisk(flip_png); size += texture->data.size(); LOG_DEBUG(Render, "Loading {} map {}", MapTypeName(texture->type), texture->path); + if (!texture->IsLoaded()) { + LOG_ERROR(Render, "Failed to load {} map {} of material with hash {:#016X}", + MapTypeName(texture->type), texture->path, hash); + if (texture->type == MapType::Color) { + state = DecodeState::Failed; + return; + } + textures[index] = nullptr; + } } if (!textures[0]) { LOG_ERROR(Render, "Unable to create material without color texture!"); @@ -126,16 +136,24 @@ void Material::LoadFromDisk(bool flip_png) noexcept { "which do not match the color texture dimentions {}x{}", MapTypeName(texture->type), texture->path, hash, texture->width, texture->height, width, height); - state = DecodeState::Failed; - return; + if (texture->type == MapType::Color) { + state = DecodeState::Failed; + return; + } + textures[static_cast(texture->type)] = nullptr; + continue; } if (texture->format != format) { LOG_ERROR( Render, "{} map {} is stored with {} format which does not match color format {}", MapTypeName(texture->type), texture->path, CustomPixelFormatAsString(texture->format), CustomPixelFormatAsString(format)); - state = DecodeState::Failed; - return; + if (texture->type == MapType::Color) { + state = DecodeState::Failed; + return; + } + textures[static_cast(texture->type)] = nullptr; + continue; } } state = DecodeState::Decoded; diff --git a/src/video_core/custom_textures/material.h b/src/video_core/custom_textures/material.h index 69d6a838c..9164328a0 100644 --- a/src/video_core/custom_textures/material.h +++ b/src/video_core/custom_textures/material.h @@ -55,23 +55,23 @@ private: public: Frontend::ImageInterface& image_interface; std::string path; - u32 width; - u32 height; + u32 width{}; + u32 height{}; std::vector hashes; std::mutex decode_mutex; - CustomPixelFormat format; - CustomFileFormat file_format; + CustomPixelFormat format{CustomPixelFormat::Invalid}; + CustomFileFormat file_format{}; std::vector data; - MapType type; + MapType type{MapType::Color}; }; struct Material { - u32 width; - u32 height; - u64 size; - u64 hash; - CustomPixelFormat format; - std::array textures; + u32 width{}; + u32 height{}; + u64 size{}; + u64 hash{}; + CustomPixelFormat format{CustomPixelFormat::Invalid}; + std::array textures{}; std::atomic state{}; void LoadFromDisk(bool flip_png) noexcept; diff --git a/src/video_core/rasterizer_cache/surface_base.cpp b/src/video_core/rasterizer_cache/surface_base.cpp index c9c645db6..633a74531 100644 --- a/src/video_core/rasterizer_cache/surface_base.cpp +++ b/src/video_core/rasterizer_cache/surface_base.cpp @@ -138,7 +138,12 @@ ClearValue SurfaceBase::MakeClearValue(PAddr copy_addr, PixelFormat dst_format) case SurfaceType::Fill: { Pica::Texture::TextureInfo tex_info{}; tex_info.format = static_cast(dst_format); - const auto color = Pica::Texture::LookupTexture(fill_buffer.data(), 0, 0, tex_info); + const std::size_t tile_size = Pica::Texture::CalculateTileSize(tex_info.format); + std::vector fill_tile(std::max(tile_size, fill_buffer.size())); + for (std::size_t i = 0; i < fill_tile.size(); i++) { + fill_tile[i] = fill_data[(copy_addr - addr + i) % fill_size]; + } + const auto color = Pica::Texture::LookupTexture(fill_tile.data(), 0, 0, tex_info); result.color = color / 255.f; break; } diff --git a/src/video_core/rasterizer_cache/texture_codec.h b/src/video_core/rasterizer_cache/texture_codec.h index 76ac6d996..7baad7b3c 100644 --- a/src/video_core/rasterizer_cache/texture_codec.h +++ b/src/video_core/rasterizer_cache/texture_codec.h @@ -407,8 +407,8 @@ static constexpr std::array UNSWIZZLE_TABLE_CONVERTED = { nullptr, // 9 nullptr, // 10 nullptr, // 11 - nullptr, // 12 - nullptr, // 13 + MortonCopy, // 12 + MortonCopy, // 13 MortonCopy, // 14 nullptr, // 15 MortonCopy, // 16 diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index c62757384..6a246c019 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -828,9 +828,10 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface raw_images[num_images++] = handles[Type::Scaled].image; } if (has_normal) { - handles[Type::Custom].Create(mat->width, mat->height, levels, texture_type, format, - traits.usage, flags, traits.aspect, false, debug_name); - raw_images[num_images++] = handles[Type::Custom].image; + LOG_WARNING(Render_Vulkan, + "Ignoring custom normal map for material hash {:#016X}: Vulkan does not bind " + "custom normal maps yet", + mat->hash); } current = res_scale != 1 ? Type::Scaled : Type::Base; @@ -1005,8 +1006,11 @@ void Surface::UploadCustom(const VideoCore::Material* material, u32 level) { }; upload(Type::Base, color); - if (auto* texture = material->textures[u32(MapType::Normal)]) { - upload(Type::Custom, texture); + if (material->textures[u32(MapType::Normal)]) { + LOG_DEBUG(Render_Vulkan, + "Skipping custom normal map upload for material hash {:#016X}: Vulkan does not " + "bind custom normal maps yet", + material->hash); } } @@ -1281,7 +1285,12 @@ vk::ImageView Surface::CopyImageView() noexcept { } vk::ImageView Surface::ImageView(ViewType view_type, Type type) noexcept { - auto& handle = handles[type == Type::Current ? current : type]; + const Type resolved_type = type == Type::Current ? current : type; + if (!handles[resolved_type] && resolved_type != Type::Base) { + return ImageView(view_type, Type::Base); + } + + auto& handle = handles[resolved_type]; if (auto image_view = handle.image_views[view_type]) { return image_view; } diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.h b/src/video_core/renderer_vulkan/vk_texture_runtime.h index b46479c58..cdacd047d 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.h +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.h @@ -192,7 +192,12 @@ public: /// Returns the image at index, otherwise the base image vk::Image Image(Type type = Type::Current) const noexcept { - return handles[type == Type::Current ? current : type].image; + const Type resolved_type = type == Type::Current ? current : type; + const auto& handle = handles[resolved_type]; + if (!handle && resolved_type != Type::Base) { + return handles[Type::Base].image; + } + return handle.image; } /// Returns the image view at index, otherwise the base view From 33bda94b8c5c594e7cff4b8a42eeff1e7d21cc19 Mon Sep 17 00:00:00 2001 From: vincentadamnemessis Date: Fri, 26 Jun 2026 22:07:22 +0800 Subject: [PATCH 2/3] video_core: harden fill surface uploads --- CMakeModules/BundleTarget.cmake | 2 +- .../rasterizer_cache/rasterizer_cache.h | 21 ++++++------------- .../rasterizer_cache/surface_base.cpp | 21 ++++++++++++++++--- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/CMakeModules/BundleTarget.cmake b/CMakeModules/BundleTarget.cmake index 713a4cab1..cee7c8284 100644 --- a/CMakeModules/BundleTarget.cmake +++ b/CMakeModules/BundleTarget.cmake @@ -96,7 +96,7 @@ if (BUNDLE_TARGET_EXECUTE) # Some local filesystem providers attach FinderInfo/resource-fork xattrs during copy, # which causes codesign to reject the bundle. find_program(xattr_executable xattr REQUIRED) - execute_process(COMMAND "${xattr_executable}" -cr "${executable_path}" + execute_process(COMMAND find "${executable_path}" -exec "${xattr_executable}" -c "{}" "+" RESULT_VARIABLE xattr_result) if (NOT xattr_result EQUAL "0") message(FATAL_ERROR "xattr cleanup failed: ${xattr_result}") diff --git a/src/video_core/rasterizer_cache/rasterizer_cache.h b/src/video_core/rasterizer_cache/rasterizer_cache.h index 26156cb56..fdc51e414 100644 --- a/src/video_core/rasterizer_cache/rasterizer_cache.h +++ b/src/video_core/rasterizer_cache/rasterizer_cache.h @@ -1150,6 +1150,9 @@ void RasterizerCache::DownloadFillSurface(Surface& surface, SurfaceInterval i const u32 flush_start = boost::icl::first(interval); const u32 flush_end = boost::icl::last_next(interval); ASSERT(flush_start >= surface.addr && flush_end <= surface.end); + if (surface.fill_size == 0 || surface.fill_size > surface.fill_data.size()) [[unlikely]] { + return; + } MemoryRef dest_ptr = memory.GetPhysicalRef(flush_start); if (!dest_ptr) [[unlikely]] { @@ -1159,21 +1162,9 @@ void RasterizerCache::DownloadFillSurface(Surface& surface, SurfaceInterval i const u32 start_offset = flush_start - surface.addr; const u32 download_size = std::clamp(flush_end - flush_start, 0u, static_cast(dest_ptr.GetSize())); - const u32 coarse_start_offset = start_offset - (start_offset % surface.fill_size); - const u32 backup_bytes = start_offset % surface.fill_size; - - std::array backup_data; - if (backup_bytes) { - std::memcpy(backup_data.data(), &dest_ptr[coarse_start_offset], backup_bytes); - } - - for (u32 offset = coarse_start_offset; offset < download_size; offset += surface.fill_size) { - std::memcpy(&dest_ptr[offset], &surface.fill_data[0], - std::min(surface.fill_size, download_size - offset)); - } - - if (backup_bytes) { - std::memcpy(&dest_ptr[coarse_start_offset], &backup_data[0], backup_bytes); + const u32 fill_start = start_offset % surface.fill_size; + for (u32 offset = 0; offset < download_size; ++offset) { + dest_ptr[offset] = surface.fill_data[(fill_start + offset) % surface.fill_size]; } } diff --git a/src/video_core/rasterizer_cache/surface_base.cpp b/src/video_core/rasterizer_cache/surface_base.cpp index 633a74531..42fb8b62a 100644 --- a/src/video_core/rasterizer_cache/surface_base.cpp +++ b/src/video_core/rasterizer_cache/surface_base.cpp @@ -8,6 +8,13 @@ #include "video_core/texture/texture_decode.h" namespace VideoCore { +namespace { + +bool IsValidFillSize(u32 fill_size) { + return fill_size != 0 && fill_size <= sizeof(SurfaceBase::fill_data); +} + +} // Anonymous namespace SurfaceBase::SurfaceBase(const SurfaceParams& params, const SurfaceFlagBits& initial_flag_bits) : SurfaceParams{params}, flags(initial_flag_bits) {} @@ -20,6 +27,9 @@ bool SurfaceBase::CanFill(const SurfaceParams& dest_surface, SurfaceInterval fil boost::icl::last_next(fill_interval) <= end && // dest_surface is within our fill range dest_surface.FromInterval(fill_interval).GetInterval() == fill_interval) { // make sure interval is a rectangle in dest surface + if (!IsValidFillSize(fill_size)) { + return false; + } if (fill_size * 8 != dest_surface.GetFormatBpp()) { // Check if bits repeat for our fill_size const u32 dest_bytes_per_pixel = std::max(dest_surface.GetFormatBpp() / 8, 1u); @@ -130,6 +140,7 @@ bool SurfaceBase::HasNormalMap() const noexcept { ClearValue SurfaceBase::MakeClearValue(PAddr copy_addr, PixelFormat dst_format) { const SurfaceType dst_type = GetFormatType(dst_format); const std::array fill_buffer = MakeFillBuffer(copy_addr); + const bool valid_fill = IsValidFillSize(fill_size); ClearValue result{}; switch (dst_type) { @@ -141,7 +152,8 @@ ClearValue SurfaceBase::MakeClearValue(PAddr copy_addr, PixelFormat dst_format) const std::size_t tile_size = Pica::Texture::CalculateTileSize(tex_info.format); std::vector fill_tile(std::max(tile_size, fill_buffer.size())); for (std::size_t i = 0; i < fill_tile.size(); i++) { - fill_tile[i] = fill_data[(copy_addr - addr + i) % fill_size]; + fill_tile[i] = valid_fill ? fill_data[(copy_addr - addr + i) % fill_size] + : fill_buffer[i % fill_buffer.size()]; } const auto color = Pica::Texture::LookupTexture(fill_tile.data(), 0, 0, tex_info); result.color = color / 255.f; @@ -173,9 +185,12 @@ ClearValue SurfaceBase::MakeClearValue(PAddr copy_addr, PixelFormat dst_format) } std::array SurfaceBase::MakeFillBuffer(PAddr copy_addr) { - const PAddr fill_offset = (copy_addr - addr) % fill_size; - std::array fill_buffer; + std::array fill_buffer{}; + if (!IsValidFillSize(fill_size)) { + return fill_buffer; + } + const PAddr fill_offset = (copy_addr - addr) % fill_size; u32 fill_buff_pos = fill_offset; for (std::size_t i = 0; i < fill_buffer.size(); i++) { fill_buffer[i] = fill_data[fill_buff_pos++ % fill_size]; From b5e4ed28944b4ab7a11119889720b3ba4326bb25 Mon Sep 17 00:00:00 2001 From: vincentadamnemessis Date: Sat, 27 Jun 2026 16:23:36 +0800 Subject: [PATCH 3/3] video_core: harden custom texture lifetimes --- src/common/slot_vector.h | 9 ++- src/tests/CMakeLists.txt | 1 + src/tests/common/slot_vector.cpp | 24 +++++++ .../custom_textures/custom_tex_manager.cpp | 66 ++++++++++++++++--- src/video_core/custom_textures/material.cpp | 33 ++++++++-- src/video_core/custom_textures/material.h | 2 +- .../rasterizer_cache/rasterizer_cache.h | 65 ++++++++++++------ .../renderer_opengl/gl_texture_runtime.h | 4 ++ .../renderer_vulkan/vk_texture_runtime.h | 4 ++ 9 files changed, 168 insertions(+), 40 deletions(-) create mode 100644 src/tests/common/slot_vector.cpp diff --git a/src/common/slot_vector.h b/src/common/slot_vector.h index 204630acd..dd20c2e97 100644 --- a/src/common/slot_vector.h +++ b/src/common/slot_vector.h @@ -85,6 +85,11 @@ public: return values_capacity - free_list.size(); } + [[nodiscard]] bool contains(SlotId id) const noexcept { + return id && id.index / 64 < stored_bitset.size() && + ((stored_bitset[id.index / 64] >> (id.index % 64)) & 1) != 0; + } + private: struct NonTrivialDummy { NonTrivialDummy() noexcept {} @@ -111,9 +116,7 @@ private: } void ValidateIndex([[maybe_unused]] SlotId id) const noexcept { - DEBUG_ASSERT(id); - DEBUG_ASSERT(id.index / 64 < stored_bitset.size()); - DEBUG_ASSERT(((stored_bitset[id.index / 64] >> (id.index % 64)) & 1) != 0); + DEBUG_ASSERT(contains(id)); } [[nodiscard]] u32 FreeValueIndex() noexcept { diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index cf99e004d..eacc6a12c 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -2,6 +2,7 @@ add_executable(tests common/bit_field.cpp common/file_util.cpp common/param_package.cpp + common/slot_vector.cpp core/core_timing.cpp core/file_sys/path_parser.cpp core/hle/kernel/hle_ipc.cpp diff --git a/src/tests/common/slot_vector.cpp b/src/tests/common/slot_vector.cpp new file mode 100644 index 000000000..5cdd6f341 --- /dev/null +++ b/src/tests/common/slot_vector.cpp @@ -0,0 +1,24 @@ +// Copyright 2026 Azahar Emulator Project +// Licensed under GPLv2 or any later version. +// Refer to the license.txt file included. + +#include +#include "common/slot_vector.h" + +TEST_CASE("SlotVector contains", "[common]") { + Common::SlotVector slots; + const Common::SlotId invalid{}; + const Common::SlotId out_of_range{128}; + + REQUIRE_FALSE(slots.contains(invalid)); + REQUIRE_FALSE(slots.contains(out_of_range)); + + const Common::SlotId first = slots.insert(1); + const Common::SlotId second = slots.insert(2); + REQUIRE(slots.contains(first)); + REQUIRE(slots.contains(second)); + + slots.erase(first); + REQUIRE_FALSE(slots.contains(first)); + REQUIRE(slots.contains(second)); +} diff --git a/src/video_core/custom_textures/custom_tex_manager.cpp b/src/video_core/custom_textures/custom_tex_manager.cpp index 58e5fe918..02309acb1 100644 --- a/src/video_core/custom_textures/custom_tex_manager.cpp +++ b/src/video_core/custom_textures/custom_tex_manager.cpp @@ -2,6 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include "common/file_util.h" #include "common/literals.h" @@ -124,7 +125,7 @@ void CustomTexManager::FindCustomTextures() { bool CustomTexManager::ParseFilename(const FileUtil::FSTEntry& file, CustomTexture* texture) { auto parts = Common::SplitString(file.virtualName, '.'); - if (parts.size() > 3) { + if (parts.empty() || parts.size() > 3) { LOG_ERROR(Render, "Invalid filename {}, ignoring", file.virtualName); return false; } @@ -139,6 +140,10 @@ bool CustomTexManager::ParseFilename(const FileUtil::FSTEntry& file, CustomTextu } texture->file_format = file_format; parts.pop_back(); + if (parts.empty()) { + LOG_ERROR(Render, "Invalid filename {}, ignoring", file.virtualName); + return false; + } // This means the texture is a material type other than color. texture->type = MapType::Color; @@ -169,6 +174,10 @@ bool CustomTexManager::ParseFilename(const FileUtil::FSTEntry& file, CustomTextu } texture->path = file.physicalName; + if (!texture->IsParsed()) { + LOG_ERROR(Render, "Unable to identify custom texture {}, ignoring", file.virtualName); + return false; + } return true; } @@ -299,6 +308,9 @@ Material* CustomTexManager::GetMaterial(u64 data_hash) { bool CustomTexManager::Decode(Material* material, std::function&& upload) { if (!async_custom_loading) { material->LoadFromDisk(flip_png_files); + if (!material->IsDecoded()) { + return false; + } return upload(); } if (material->IsUnloaded()) { @@ -331,22 +343,56 @@ bool CustomTexManager::ReadConfig(u64 title_id, bool options_only) { return false; } - nlohmann::json json = nlohmann::json::parse(config, nullptr, false, true); + const nlohmann::json json = nlohmann::json::parse(config, nullptr, false, true); + if (json.is_discarded() || !json.is_object()) { + LOG_ERROR(Render, "Failed to parse custom texture config {}", config_path); + return false; + } - const auto& options = json["options"]; - skip_mipmap = options["skip_mipmap"].get(); - flip_png_files = options["flip_png_files"].get(); - use_new_hash = options["use_new_hash"].get(); + const auto options_it = json.find("options"); + if (options_it == json.end() || !options_it->is_object()) { + LOG_ERROR(Render, "Custom texture config {} has no valid options object", config_path); + return false; + } + const auto read_bool = [&](std::string_view key, bool& value) { + const auto it = options_it->find(key); + if (it == options_it->end() || !it->is_boolean()) { + LOG_ERROR(Render, "Custom texture config {} has invalid option {}", config_path, key); + return false; + } + value = it->get(); + return true; + }; + bool parsed_skip_mipmap{}; + bool parsed_flip_png_files{}; + bool parsed_use_new_hash{}; + if (!read_bool("skip_mipmap", parsed_skip_mipmap) || + !read_bool("flip_png_files", parsed_flip_png_files) || + !read_bool("use_new_hash", parsed_use_new_hash)) { + return false; + } + skip_mipmap = parsed_skip_mipmap; + flip_png_files = parsed_flip_png_files; + use_new_hash = parsed_use_new_hash; if (options_only) { return true; } - const auto& textures = json["textures"]; + const auto textures_it = json.find("textures"); + if (textures_it == json.end()) { + return true; + } + if (!textures_it->is_object()) { + LOG_ERROR(Render, "Custom texture config {} has an invalid textures object", config_path); + return false; + } + const auto& textures = *textures_it; for (const auto& material : textures.items()) { - std::size_t idx{}; - const u64 hash = std::stoull(material.key(), &idx, 16); - if (!idx) { + u64 hash{}; + const std::string& key = material.key(); + const auto [end, error] = std::from_chars(key.data(), key.data() + key.size(), hash, 16); + if (error != std::errc{} || end != key.data() + key.size()) { LOG_ERROR(Render, "Key {} is invalid, skipping", material.key()); continue; } diff --git a/src/video_core/custom_textures/material.cpp b/src/video_core/custom_textures/material.cpp index ee7030276..e73ef8499 100644 --- a/src/video_core/custom_textures/material.cpp +++ b/src/video_core/custom_textures/material.cpp @@ -32,7 +32,7 @@ CustomPixelFormat ToCustomPixelFormat(ddsktx_format format) { return CustomPixelFormat::ASTC8; default: LOG_ERROR(Common, "Unknown dds/ktx pixel format {}", format); - return CustomPixelFormat::RGBA8; + return CustomPixelFormat::Invalid; } } @@ -54,18 +54,27 @@ CustomTexture::CustomTexture(Frontend::ImageInterface& image_interface_) CustomTexture::~CustomTexture() = default; -void CustomTexture::LoadFromDisk(bool flip_png) { +bool CustomTexture::LoadFromDisk(bool flip_png) { std::scoped_lock lock{decode_mutex}; if (IsLoaded()) { - return; + return true; } FileUtil::IOFile file{path, "rb"}; + if (!file.IsOpen() || file.GetSize() == 0) { + LOG_CRITICAL(Render, "Failed to open custom texture: {}", path); + return false; + } std::vector input(file.GetSize()); if (file.ReadBytes(input.data(), input.size()) != input.size()) { LOG_CRITICAL(Render, "Failed to open custom texture: {}", path); - return; + return false; } + + data.clear(); + width = 0; + height = 0; + format = CustomPixelFormat::Invalid; switch (file_format) { case CustomFileFormat::PNG: LoadPNG(input, flip_png); @@ -77,6 +86,12 @@ void CustomTexture::LoadFromDisk(bool flip_png) { default: LOG_ERROR(Render, "Unknown file format {}", file_format); } + if (format == CustomPixelFormat::Invalid || data.empty() || width == 0 || height == 0) { + data.clear(); + format = CustomPixelFormat::Invalid; + return false; + } + return true; } void CustomTexture::LoadPNG(std::span input, bool flip_png) { @@ -100,15 +115,19 @@ void Material::LoadFromDisk(bool flip_png) noexcept { if (IsDecoded()) { return; } + size = 0; + width = 0; + height = 0; + format = CustomPixelFormat::Invalid; for (std::size_t index = 0; index < textures.size(); index++) { CustomTexture* const texture = textures[index]; - if (!texture || texture->IsLoaded()) { + if (!texture) { continue; } - texture->LoadFromDisk(flip_png); + const bool loaded = texture->LoadFromDisk(flip_png); size += texture->data.size(); LOG_DEBUG(Render, "Loading {} map {}", MapTypeName(texture->type), texture->path); - if (!texture->IsLoaded()) { + if (!loaded) { LOG_ERROR(Render, "Failed to load {} map {} of material with hash {:#016X}", MapTypeName(texture->type), texture->path, hash); if (texture->type == MapType::Color) { diff --git a/src/video_core/custom_textures/material.h b/src/video_core/custom_textures/material.h index 9164328a0..2aaab9294 100644 --- a/src/video_core/custom_textures/material.h +++ b/src/video_core/custom_textures/material.h @@ -37,7 +37,7 @@ public: explicit CustomTexture(Frontend::ImageInterface& image_interface); ~CustomTexture(); - void LoadFromDisk(bool flip_png); + [[nodiscard]] bool LoadFromDisk(bool flip_png); [[nodiscard]] bool IsParsed() const noexcept { return file_format != CustomFileFormat::None && !hashes.empty(); diff --git a/src/video_core/rasterizer_cache/rasterizer_cache.h b/src/video_core/rasterizer_cache/rasterizer_cache.h index fdc51e414..0eae6f917 100644 --- a/src/video_core/rasterizer_cache/rasterizer_cache.h +++ b/src/video_core/rasterizer_cache/rasterizer_cache.h @@ -578,10 +578,10 @@ SurfaceId RasterizerCache::GetTextureSurface(const Pica::Texture::TextureInfo } const auto [src_surface_id, rect] = GetSurfaceSubRect(params, ScaleMatch::Ignore, true, initial_flags); - Surface& src_surface = slot_surfaces[src_surface_id]; - params.res_scale = src_surface.res_scale; + params.res_scale = slot_surfaces[src_surface_id].res_scale; SurfaceId tmp_surface_id = CreateSurface(params, initial_flags); + Surface& src_surface = slot_surfaces[src_surface_id]; Surface& tmp_surface = slot_surfaces[tmp_surface_id]; sentenced.emplace_back(tmp_surface_id, frame_tick); @@ -758,12 +758,15 @@ FramebufferHelper RasterizerCache::GetFramebufferSurfaces(bool using_color ValidateSurface(color_id, boost::icl::first(color_vp_interval), boost::icl::length(color_vp_interval)); } + depth_surface = depth_id ? &slot_surfaces[depth_id] : nullptr; if (depth_id) { depth_level = depth_surface->LevelOf(depth_params.addr); depth_surface->flags |= SurfaceFlagBits::RenderTarget; ValidateSurface(depth_id, boost::icl::first(depth_vp_interval), boost::icl::length(depth_vp_interval)); } + color_surface = color_id ? &slot_surfaces[color_id] : nullptr; + depth_surface = depth_id ? &slot_surfaces[depth_id] : nullptr; const FramebufferParams fb_params = { .color_id = color_id, @@ -947,23 +950,23 @@ void RasterizerCache::ValidateSurface(SurfaceId surface_id, PAddr addr, u32 s return; } - Surface& surface = slot_surfaces[surface_id]; const SurfaceInterval validate_interval(addr, addr + size); - if (surface.type == SurfaceType::Fill) { - ASSERT_MSG(surface.IsRegionValid(validate_interval), + Surface& initial_surface = slot_surfaces[surface_id]; + if (initial_surface.type == SurfaceType::Fill) { + ASSERT_MSG(initial_surface.IsRegionValid(validate_interval), "Attempted to validate a non-valid fill surface"); return; } - SurfaceRegions validate_regions = surface.invalid_regions & validate_interval; + SurfaceRegions validate_regions = initial_surface.invalid_regions & validate_interval; if (validate_regions.empty()) { return; } auto notify_validated = [&](SurfaceInterval interval) { - surface.MarkValid(interval); + slot_surfaces[surface_id].MarkValid(interval); validate_regions.erase(interval); }; @@ -971,9 +974,12 @@ void RasterizerCache::ValidateSurface(SurfaceId surface_id, PAddr addr, u32 s "RasterizerCache::ValidateSurface (from {:#x} to {:#x})", addr, addr + size}; - u32 level = surface.LevelOf(addr); - SurfaceInterval level_interval = surface.LevelInterval(level); + u32 level = initial_surface.LevelOf(addr); + SurfaceInterval level_interval = initial_surface.LevelInterval(level); while (!validate_regions.empty()) { + // UploadCustomSurface may grow slot_surfaces and invalidate references to its entries. + Surface& surface = slot_surfaces[surface_id]; + // Take an invalid interval from the validation regions and clamp it // to the current level interval. If the interval is empty // then we have validated the entire level so move to the next. @@ -1004,13 +1010,14 @@ void RasterizerCache::ValidateSurface(SurfaceId surface_id, PAddr addr, u32 s FlushRegion(params.addr, params.size); if (!use_custom_textures || !UploadCustomSurface(surface_id, interval)) { - UploadSurface(surface, interval); + UploadSurface(slot_surfaces[surface_id], interval); } notify_validated(params.GetInterval()); } // Filtered mipmaps often look really bad. We can achieve better quality by // generating them from the base level. + Surface& surface = slot_surfaces[surface_id]; if (surface.res_scale != 1 && level != 0) { runtime.GenerateMipmaps(surface); } @@ -1093,22 +1100,42 @@ bool RasterizerCache::UploadCustomSurface(SurfaceId surface_id, SurfaceInterv return true; } - surface.flags |= SurfaceFlagBits::Custom; + const SurfaceParams expected_surface = surface; - const auto upload = [this, level, surface_id, material]() -> bool { - ASSERT_MSG(True(slot_surfaces[surface_id].flags & SurfaceFlagBits::Custom), - "Surface is not suitable for custom upload, aborting!"); - if (!slot_surfaces[surface_id].IsCustom()) { - const SurfaceBase old_surface{slot_surfaces[surface_id]}; + const auto upload = [this, level, surface_id, material, expected_surface, load_info, + hash]() -> bool { + if (!slot_surfaces.contains(surface_id)) { + return false; + } + + Surface& target = slot_surfaces[surface_id]; + if (False(target.flags & SurfaceFlagBits::Registered) || + !(static_cast(target) == expected_surface) || + !runtime.SupportsCustomFormat(material->format)) { + return false; + } + + MemoryRef source_ptr = memory.GetPhysicalRef(load_info.addr); + if (!source_ptr) { + return false; + } + const auto upload_data = source_ptr.GetWriteBytes(load_info.end - load_info.addr); + if (ComputeHash(load_info, upload_data) != hash) { + return false; + } + + target.flags |= SurfaceFlagBits::Custom; + if (!target.IsCustom() || target.material != material) { + const SurfaceBase old_surface{target}; const SurfaceId old_id = slot_surfaces.swap_and_insert(surface_id, runtime, old_surface, material); slot_surfaces[old_id].flags &= ~SurfaceFlagBits::Registered; sentenced.emplace_back(old_id, frame_tick); } - Surface& surface = slot_surfaces[surface_id]; - surface.UploadCustom(material, level); + Surface& custom_surface = slot_surfaces[surface_id]; + custom_surface.UploadCustom(material, level); if (custom_tex_manager.SkipMipmaps()) { - runtime.GenerateMipmaps(surface); + runtime.GenerateMipmaps(custom_surface); } return true; }; diff --git a/src/video_core/renderer_opengl/gl_texture_runtime.h b/src/video_core/renderer_opengl/gl_texture_runtime.h index d25ba102c..b7a2104b1 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.h +++ b/src/video_core/renderer_opengl/gl_texture_runtime.h @@ -58,6 +58,10 @@ public: const FormatTuple& GetFormatTuple(VideoCore::PixelFormat pixel_format) const; const FormatTuple& GetFormatTuple(VideoCore::CustomPixelFormat pixel_format); + bool SupportsCustomFormat(VideoCore::CustomPixelFormat pixel_format) const { + return driver.IsCustomFormatSupported(pixel_format); + } + /// Attempts to reinterpret a rectangle of source to another rectangle of dest bool Reinterpret(Surface& source, Surface& dest, const VideoCore::TextureCopy& copy); diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.h b/src/video_core/renderer_vulkan/vk_texture_runtime.h index cdacd047d..248e08320 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.h +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.h @@ -117,6 +117,10 @@ public: return instance; } + bool SupportsCustomFormat(VideoCore::CustomPixelFormat pixel_format) const { + return instance.GetTraits(pixel_format).transfer_support; + } + Scheduler& GetScheduler() const { return scheduler; }