This commit is contained in:
VincentAdamNemessis 2026-07-04 16:31:11 +02:00 committed by GitHub
commit e6d8bee650
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 262 additions and 82 deletions

View File

@ -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}"

View File

@ -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 {

View File

@ -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

View File

@ -0,0 +1,24 @@
// Copyright 2026 Azahar Emulator Project
// Licensed under GPLv2 or any later version.
// Refer to the license.txt file included.
#include <catch2/catch_test_macros.hpp>
#include "common/slot_vector.h"
TEST_CASE("SlotVector contains", "[common]") {
Common::SlotVector<int> 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));
}

View File

@ -2,6 +2,7 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <charconv>
#include <json.hpp>
#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<bool()>&& 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<bool>();
flip_png_files = options["flip_png_files"].get<bool>();
use_new_hash = options["use_new_hash"].get<bool>();
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<bool>();
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;
}

View File

@ -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<u8> 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<const u8> 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<std::size_t>(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<std::size_t>(texture->type)] = nullptr;
continue;
}
}
state = DecodeState::Decoded;

View File

@ -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<u64> hashes;
std::mutex decode_mutex;
CustomPixelFormat format;
CustomFileFormat file_format;
CustomPixelFormat format{CustomPixelFormat::Invalid};
CustomFileFormat file_format{};
std::vector<u8> data;
MapType type;
MapType type{MapType::Color};
};
struct Material {
u32 width;
u32 height;
u64 size;
u64 hash;
CustomPixelFormat format;
std::array<CustomTexture*, MAX_MAPS> textures;
u32 width{};
u32 height{};
u64 size{};
u64 hash{};
CustomPixelFormat format{CustomPixelFormat::Invalid};
std::array<CustomTexture*, MAX_MAPS> textures{};
std::atomic<DecodeState> state{};
void LoadFromDisk(bool flip_png) noexcept;

View File

@ -594,10 +594,10 @@ SurfaceId RasterizerCache<T>::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<T> RasterizerCache<T>::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<T>::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<T>::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<T>::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<T>::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<const SurfaceParams&>(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<T>::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<T>::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<u32>(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<u8, 4> 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];
}
}

View File

@ -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<Pica::TexturingRegs::TextureFormat>(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<u8> 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<u8, 4> SurfaceBase::MakeFillBuffer(PAddr copy_addr) {
const PAddr fill_offset = (copy_addr - addr) % fill_size;
std::array<u8, 4> fill_buffer;
std::array<u8, 4> 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];

View File

@ -407,8 +407,8 @@ static constexpr std::array<MortonFunc, 18> UNSWIZZLE_TABLE_CONVERTED = {
nullptr, // 9
nullptr, // 10
nullptr, // 11
nullptr, // 12
nullptr, // 13
MortonCopy<true, PixelFormat::ETC1, true>, // 12
MortonCopy<true, PixelFormat::ETC1A4, true>, // 13
MortonCopy<true, PixelFormat::D16, true>, // 14
nullptr, // 15
MortonCopy<true, PixelFormat::D24, true>, // 16

View File

@ -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);

View File

@ -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;
}

View File

@ -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