diff --git a/CMakeModules/BundleTarget.cmake b/CMakeModules/BundleTarget.cmake index 45e5ee296..cee7c8284 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 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}") + 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/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 d7bee5da4..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,13 +115,27 @@ void Material::LoadFromDisk(bool flip_png) noexcept { if (IsDecoded()) { return; } - for (CustomTexture* const texture : textures) { - if (!texture || texture->IsLoaded()) { + 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) { 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 (!loaded) { + 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 +155,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..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(); @@ -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/rasterizer_cache.h b/src/video_core/rasterizer_cache/rasterizer_cache.h index c25bac021..3e5ab3116 100644 --- a/src/video_core/rasterizer_cache/rasterizer_cache.h +++ b/src/video_core/rasterizer_cache/rasterizer_cache.h @@ -594,10 +594,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); @@ -774,12 +774,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, @@ -963,23 +966,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); }; @@ -987,9 +990,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. @@ -1020,13 +1026,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); } @@ -1109,22 +1116,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; }; @@ -1166,6 +1193,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]] { @@ -1175,21 +1205,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 c9c645db6..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) { @@ -138,7 +149,13 @@ 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] = 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; break; } @@ -168,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]; 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_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.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index dad2bf8f8..6a88ae461 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -829,9 +829,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; @@ -1006,8 +1007,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); } } @@ -1282,7 +1286,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..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; } @@ -192,7 +196,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