2032 lines
75 KiB
C++
2032 lines
75 KiB
C++
#include "graphics/host_gpu/renderer/cache/textureCache.h"
|
|
|
|
#include "common/alignment.h"
|
|
#include "common/assert.h"
|
|
#include "common/emulatorConfig.h"
|
|
#include "common/logging/log.h"
|
|
#include "common/profiler.h"
|
|
#include "graphics/guest_gpu/gpu_format.h"
|
|
#include "graphics/guest_gpu/tile.h"
|
|
#include "graphics/host_gpu/graphicContext.h"
|
|
#include "graphics/host_gpu/renderer/cache/bufferCache.h"
|
|
#include "graphics/host_gpu/renderer/commandScheduler.h"
|
|
#include "graphics/host_gpu/renderer/image/imageView.h"
|
|
#include "graphics/host_gpu/renderer/image/textureCommon.h"
|
|
#include "graphics/host_gpu/renderer/image/tiler.h"
|
|
#include "graphics/host_gpu/renderer/render.h"
|
|
#include "kernel/memory.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <bit>
|
|
#include <cinttypes>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <mutex>
|
|
#include <span>
|
|
#include <tuple>
|
|
#include <vulkan/vulkan_format_traits.hpp>
|
|
|
|
namespace Libs::Graphics {
|
|
|
|
namespace {
|
|
|
|
constexpr uint64_t NumFramesBeforeRemoval = 32;
|
|
|
|
[[nodiscard]] bool DecodeDccClear(const TextureCache::ImageDesc& desc, vk::Format format,
|
|
uint32_t fill, vk::ClearColorValue& clear) {
|
|
const auto code = static_cast<uint8_t>(fill);
|
|
if (fill != static_cast<uint32_t>(code) * 0x01010101u) {
|
|
return false;
|
|
}
|
|
const auto& metadata = desc.info.metadata;
|
|
if (code == 0x20) {
|
|
// Clear-to-register is a color-buffer operation; the texture pipe cannot decode it.
|
|
return desc.type == TextureCache::BindingType::RenderTarget &&
|
|
metadata.dcc_clear_register_valid &&
|
|
DecodePackedColorClear(format, metadata.dcc_clear_word, clear);
|
|
}
|
|
if (code != 0x00 && code != 0x40 && code != 0x80 && code != 0xc0) {
|
|
return false;
|
|
}
|
|
clear = {};
|
|
if (code == 0x00) {
|
|
return true;
|
|
}
|
|
switch (format) {
|
|
case vk::Format::eR8Unorm:
|
|
case vk::Format::eR8G8Unorm:
|
|
case vk::Format::eR8G8B8A8Unorm:
|
|
case vk::Format::eR8G8B8A8Srgb:
|
|
case vk::Format::eB8G8R8A8Unorm:
|
|
case vk::Format::eB8G8R8A8Srgb:
|
|
case vk::Format::eA2B10G10R10UnormPack32:
|
|
case vk::Format::eA2R10G10B10UnormPack32:
|
|
case vk::Format::eR5G6B5UnormPack16:
|
|
case vk::Format::eA1R5G5B5UnormPack16:
|
|
case vk::Format::eR4G4B4A4UnormPack16:
|
|
case vk::Format::eR16Unorm:
|
|
case vk::Format::eR16G16Unorm:
|
|
case vk::Format::eR16G16B16A16Unorm:
|
|
case vk::Format::eR16Sfloat:
|
|
case vk::Format::eR16G16Sfloat:
|
|
case vk::Format::eR16G16B16A16Sfloat:
|
|
case vk::Format::eR32Sfloat:
|
|
case vk::Format::eR32G32Sfloat:
|
|
case vk::Format::eR32G32B32A32Sfloat:
|
|
case vk::Format::eB10G11R11UfloatPack32: break;
|
|
default: return false;
|
|
}
|
|
const float rgb = (code & 0x80u) != 0 ? 1.0f : 0.0f;
|
|
const float alpha = (code & 0x40u) != 0 ? 1.0f : 0.0f;
|
|
std::array<float, 4> channels {rgb, rgb, rgb, alpha};
|
|
if (!metadata.dcc_alpha_msb) {
|
|
std::swap(channels[0], channels[3]);
|
|
}
|
|
// DCC clear decoding clamps missing lanes before applying the format swizzle.
|
|
const auto components = vk::componentCount(format);
|
|
if (components == 1) {
|
|
channels[0] = channels[3];
|
|
} else if (components == 2) {
|
|
channels[1] = channels[3];
|
|
}
|
|
switch (format) {
|
|
case vk::Format::eB8G8R8A8Unorm:
|
|
case vk::Format::eB8G8R8A8Srgb:
|
|
case vk::Format::eA2R10G10B10UnormPack32:
|
|
case vk::Format::eA1R5G5B5UnormPack16:
|
|
case vk::Format::eR5G6B5UnormPack16: std::swap(channels[0], channels[2]); break;
|
|
case vk::Format::eR4G4B4A4UnormPack16:
|
|
std::reverse(channels.begin(), channels.end());
|
|
break;
|
|
default: break;
|
|
}
|
|
clear.float32 = channels;
|
|
return true;
|
|
}
|
|
|
|
[[nodiscard]] const char* BindingTypeName(TextureCache::BindingType type) {
|
|
switch (type) {
|
|
case TextureCache::BindingType::Texture: return "Texture";
|
|
case TextureCache::BindingType::Storage: return "StorageTexture";
|
|
case TextureCache::BindingType::RenderTarget: return "ColorTarget";
|
|
case TextureCache::BindingType::DepthTarget: return "DepthTarget";
|
|
case TextureCache::BindingType::VideoOut: return "VideoOut";
|
|
}
|
|
return "Image";
|
|
}
|
|
|
|
void NameImageBinding(GraphicContext& graphics, Image& image, vk::ImageView view,
|
|
TextureCache::BindingType type, const ImageViewInfo& view_info) {
|
|
const auto* role = BindingTypeName(type);
|
|
SetVulkanObjectNameF(
|
|
graphics.device, image.backing.image,
|
|
"Kyty.{}.Image[guest=0x{:016x} size=0x{:x} extent={}x{}x{} format={} mips={} layers={} "
|
|
"samples={}]",
|
|
role, image.info.data.address, image.info.data.size, image.info.extent.width,
|
|
image.info.extent.height, image.info.extent.depth,
|
|
static_cast<uint32_t>(image.info.pixel_format), image.info.resources.levels,
|
|
image.info.resources.layers, image.info.samples);
|
|
SetVulkanObjectNameF(
|
|
graphics.device, view,
|
|
"Kyty.{}.View[guest=0x{:016x} format={} aspect=0x{:x} mip={}+{} layer={}+{}]", role,
|
|
image.info.data.address, static_cast<uint32_t>(view_info.format),
|
|
static_cast<vk::ImageAspectFlags::MaskType>(view_info.aspect), view_info.base_level,
|
|
view_info.level_count, view_info.base_layer, view_info.layer_count);
|
|
}
|
|
|
|
[[nodiscard]] std::vector<vk::BufferImageCopy> BuildDepthCopies(const ImageInfo& info,
|
|
uint64_t slice_stride) {
|
|
std::vector<vk::BufferImageCopy> copies(info.resources.layers);
|
|
for (uint32_t layer = 0; layer < info.resources.layers; ++layer) {
|
|
auto& copy = copies[layer];
|
|
copy.bufferOffset = slice_stride * layer;
|
|
copy.bufferRowLength = info.pitch;
|
|
copy.bufferImageHeight = info.extent.height;
|
|
copy.imageSubresource = {vk::ImageAspectFlagBits::eDepth, 0, layer, 1};
|
|
copy.imageExtent = {info.extent.width, info.extent.height, 1};
|
|
}
|
|
return copies;
|
|
}
|
|
|
|
[[nodiscard]] std::vector<GpuTileInfo> BuildDepthTiles(const ImageInfo& info) {
|
|
TileBlockLayout block {};
|
|
EXIT_NOT_IMPLEMENTED(
|
|
!TileGetBlockLayout(TileBlockFamily::Depth64KB, info.bytes_per_block, block));
|
|
const auto full_slice_size = info.data.size / info.resources.layers;
|
|
std::vector<GpuTileInfo> tiles;
|
|
tiles.reserve(info.resources.layers);
|
|
for (uint32_t layer = 0; layer < info.resources.layers; ++layer) {
|
|
const auto offset = full_slice_size * layer;
|
|
tiles.push_back({block.family, block.bytes_per_element, offset, full_slice_size, offset,
|
|
full_slice_size, 0, info.extent.width, info.extent.height, 1, info.pitch});
|
|
tiles.back().surface_z = layer;
|
|
}
|
|
return tiles;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TextureCache::TextureCache(GraphicContext& graphics, CommandScheduler& scheduler,
|
|
PageManager& page_manager, BufferCache& buffer_cache)
|
|
: m_graphics(graphics), m_scheduler(scheduler), m_page_manager(page_manager),
|
|
m_blit_helper(graphics, scheduler),
|
|
m_tiler(graphics, scheduler, buffer_cache.GetUtilityBuffer(MemoryUsage::Stream)),
|
|
m_buffer_cache(buffer_cache),
|
|
m_readback_linear_images(Config::ReadbackLinearImagesEnabled()) {
|
|
if (m_graphics.CanReportMemoryUsage()) {
|
|
constexpr int64_t GiB = 1024ll * 1024 * 1024;
|
|
const auto budget =
|
|
static_cast<int64_t>(std::min<uint64_t>(m_graphics.GetTotalMemoryBudget(), INT64_MAX));
|
|
const auto threshold = std::min<int64_t>(budget, 8 * GiB);
|
|
m_pressure_gc_memory = static_cast<uint64_t>(
|
|
std::max<int64_t>(std::min(budget - 6 * threshold / 10, budget - GiB), GiB + GiB / 2));
|
|
m_critical_gc_memory = static_cast<uint64_t>(
|
|
std::max<int64_t>(std::min(budget - 2 * threshold / 10, budget - GiB / 2), 3 * GiB));
|
|
m_trigger_gc_memory = static_cast<uint64_t>(std::max<int64_t>((budget - threshold) / 2, 0));
|
|
}
|
|
}
|
|
|
|
TextureCache::~TextureCache() {
|
|
m_slot_images.ForEach([&](ImageId id, const Image& image) {
|
|
if (image.registered) {
|
|
UnregisterImage(id);
|
|
}
|
|
});
|
|
}
|
|
|
|
bool TextureCache::SameBacking(const ImageInfo& cached, const ImageInfo& requested,
|
|
bool exact_format) {
|
|
if (cached.data.address != requested.data.address) {
|
|
return false;
|
|
}
|
|
if (cached.data.size != requested.data.size) {
|
|
return false;
|
|
}
|
|
if (cached.extent != requested.extent) {
|
|
return false;
|
|
}
|
|
if (cached.samples != requested.samples) {
|
|
return false;
|
|
}
|
|
if (cached.bytes_per_block != requested.bytes_per_block) {
|
|
return false;
|
|
}
|
|
if (cached.tile_mode != requested.tile_mode) {
|
|
return false;
|
|
}
|
|
if (!ImageViewOps::FormatsCompatible(cached.pixel_format, requested.pixel_format) ||
|
|
(cached.type != requested.type && requested.extent != vk::Extent3D {1, 1, 1})) {
|
|
return false;
|
|
}
|
|
if (exact_format && cached.pixel_format != requested.pixel_format) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
TextureCache::BindingType TextureCache::UploadBinding(const Image& image) {
|
|
if (image.info.IsDepth()) {
|
|
if (image.info.tile_mode == Prospero::TileMode::kDepth ||
|
|
image.info.tile_mode == Prospero::TileMode::kLinear) {
|
|
return BindingType::DepthTarget;
|
|
}
|
|
return BindingType::Texture;
|
|
}
|
|
if (image.usage.render_target) {
|
|
return BindingType::RenderTarget;
|
|
}
|
|
if (image.usage.video_out) {
|
|
return BindingType::VideoOut;
|
|
}
|
|
return image.usage.storage ? BindingType::Storage : BindingType::Texture;
|
|
}
|
|
|
|
bool TextureCache::SafeToDownload(const Image& image) {
|
|
if (!image.SafeToDownload()) {
|
|
return false;
|
|
}
|
|
const auto range = image.info.data;
|
|
return !m_buffer_cache.HasGpuDirtyBytes(range.address, range.size);
|
|
}
|
|
|
|
ImageId TextureCache::InsertImage(const ImageInfo& info) {
|
|
const auto id = m_slot_images.insert(m_graphics, m_scheduler, info);
|
|
if (!info.data.Empty()) {
|
|
RegisterImage(id);
|
|
}
|
|
return id;
|
|
}
|
|
|
|
void TextureCache::RegisterImage(ImageId id) {
|
|
auto& image = m_slot_images[id];
|
|
if (image.registered || image.info.data.Empty()) {
|
|
EXIT("TextureCache: invalid image registration\n");
|
|
}
|
|
ImagePageTable::PageRange pages {};
|
|
if (!ImagePageTable::TryGetPageRange(image.info.data.address, image.info.data.size, pages)) {
|
|
EXIT("TextureCache: image registration is outside the guest address space\n");
|
|
}
|
|
ForEachPage(image.info.data.address, image.info.data.size, [this, id](uint64_t page) {
|
|
m_image_page_table[page].push_back(id);
|
|
});
|
|
image.registered = true;
|
|
image.lru_id = m_lru_cache.Insert(id, m_gc_tick);
|
|
m_total_used_memory += image.AccountedSize();
|
|
}
|
|
|
|
void TextureCache::UnregisterImage(ImageId id) {
|
|
auto& image = m_slot_images[id];
|
|
if (!image.registered) {
|
|
return;
|
|
}
|
|
UntrackImage(id);
|
|
ImagePageTable::PageRange pages {};
|
|
if (!ImagePageTable::TryGetPageRange(image.info.data.address, image.info.data.size, pages)) {
|
|
EXIT("TextureCache: registered image is outside the guest address space\n");
|
|
}
|
|
ForEachPage(image.info.data.address, image.info.data.size, [this, id](uint64_t page) {
|
|
auto* owners = m_image_page_table.Find(page);
|
|
if (owners == nullptr || !owners->Erase(id)) {
|
|
EXIT("TextureCache: image missing from page owner index\n");
|
|
}
|
|
});
|
|
m_lru_cache.Free(image.lru_id);
|
|
const auto accounted = image.AccountedSize();
|
|
if (accounted > m_total_used_memory) {
|
|
EXIT("TextureCache: image accounting underflow\n");
|
|
}
|
|
m_total_used_memory -= accounted;
|
|
image.registered = false;
|
|
}
|
|
|
|
void TextureCache::DeleteImage(ImageId id) {
|
|
auto* image = m_slot_images.try_get(id);
|
|
if (image == nullptr || !image->registered) {
|
|
return;
|
|
}
|
|
if (!image->depth_id) {
|
|
std::vector<ImageId> associations;
|
|
m_slot_images.ForEach([&](ImageId candidate, const Image& associated) {
|
|
if (associated.depth_id == id) {
|
|
associations.push_back(candidate);
|
|
}
|
|
});
|
|
for (const auto association: associations) {
|
|
FreeImage(association);
|
|
}
|
|
}
|
|
if (image->IsGpuModified()) {
|
|
EXIT("TextureCache: deleting a GPU-modified image without resolving its contents\n");
|
|
}
|
|
m_download_images.erase(id);
|
|
if (image->info.HasMetadata()) {
|
|
const auto metadata = m_surface_metas.find(image->info.metadata.range.address);
|
|
if (metadata != m_surface_metas.end() &&
|
|
((image->info.metadata.kind == ImageMetadataKind::Dcc &&
|
|
metadata->second.type == MetaDataInfo::Type::Dcc) ||
|
|
(image->info.metadata.kind == ImageMetadataKind::Htile &&
|
|
metadata->second.type == MetaDataInfo::Type::HTile))) {
|
|
// A later binding may have reused this address for another metadata type.
|
|
m_surface_metas.erase(metadata);
|
|
}
|
|
}
|
|
UnregisterImage(id);
|
|
if (m_scheduler.Active()) {
|
|
m_scheduler.DeferOperation([this, id] { m_slot_images.erase(id); });
|
|
} else {
|
|
m_slot_images.erase(id);
|
|
}
|
|
}
|
|
|
|
void TextureCache::FreeImage(ImageId id) {
|
|
auto& image = m_slot_images[id];
|
|
if (image.IsGpuModified()) {
|
|
image.ClearGpuModified();
|
|
}
|
|
DeleteImage(id);
|
|
}
|
|
|
|
void TextureCache::TouchImage(Image& image) {
|
|
if (image.registered) {
|
|
m_lru_cache.Touch(image.lru_id, m_gc_tick);
|
|
}
|
|
}
|
|
|
|
void TextureCache::MarkAsMaybeDirty(ImageId id, Image& image) {
|
|
image.MarkMaybeCpuDirty();
|
|
if (image.NeedsMaybeCpuHash()) {
|
|
image.SetMaybeCpuHash(image.HashGuestEdges());
|
|
}
|
|
UntrackImage(id);
|
|
}
|
|
|
|
void TextureCache::TrackImage(ImageId id) {
|
|
auto& image = m_slot_images[id];
|
|
if (!image.registered) {
|
|
return;
|
|
}
|
|
const auto image_begin = image.info.data.address;
|
|
const auto image_end = image.info.data.End();
|
|
if (image_begin == image.track_addr && image_end == image.track_addr_end) {
|
|
return;
|
|
}
|
|
if (!image.IsTracked()) {
|
|
image.track_addr = image_begin;
|
|
image.track_addr_end = image_end;
|
|
m_page_manager.UpdatePageWatchers<true>(image_begin, image.info.data.size);
|
|
return;
|
|
}
|
|
if (image_begin < image.track_addr) {
|
|
TrackImageHead(id);
|
|
}
|
|
if (image.track_addr_end < image_end) {
|
|
TrackImageTail(id);
|
|
}
|
|
}
|
|
|
|
void TextureCache::TrackImageHead(ImageId id) {
|
|
auto& image = m_slot_images[id];
|
|
if (!image.registered) {
|
|
return;
|
|
}
|
|
const auto image_begin = image.info.data.address;
|
|
if (image_begin == image.track_addr) {
|
|
return;
|
|
}
|
|
if (!image.IsTracked() || image_begin > image.track_addr) {
|
|
EXIT("TextureCache: invalid image head tracking range\n");
|
|
}
|
|
const auto size = image.track_addr - image_begin;
|
|
image.track_addr = image_begin;
|
|
m_page_manager.UpdatePageWatchers<true>(image_begin, size);
|
|
}
|
|
|
|
void TextureCache::TrackImageTail(ImageId id) {
|
|
auto& image = m_slot_images[id];
|
|
if (!image.registered) {
|
|
return;
|
|
}
|
|
const auto image_end = image.info.data.End();
|
|
if (image_end == image.track_addr_end) {
|
|
return;
|
|
}
|
|
if (!image.IsTracked() || image.track_addr_end > image_end) {
|
|
EXIT("TextureCache: invalid image tail tracking range\n");
|
|
}
|
|
const auto address = image.track_addr_end;
|
|
const auto size = image_end - address;
|
|
image.track_addr_end = image_end;
|
|
m_page_manager.UpdatePageWatchers<true>(address, size);
|
|
}
|
|
|
|
void TextureCache::UntrackImage(ImageId id) {
|
|
auto& image = m_slot_images[id];
|
|
if (!image.IsTracked()) {
|
|
return;
|
|
}
|
|
const auto address = image.track_addr;
|
|
const auto size = image.track_addr_end - image.track_addr;
|
|
image.track_addr = 0;
|
|
image.track_addr_end = 0;
|
|
if (size != 0) {
|
|
m_page_manager.UpdatePageWatchers<false>(address, size);
|
|
}
|
|
}
|
|
|
|
void TextureCache::UntrackImageHead(ImageId id) {
|
|
auto& image = m_slot_images[id];
|
|
const auto begin = image.info.data.address;
|
|
if (!image.IsTracked() || begin < image.track_addr) {
|
|
return;
|
|
}
|
|
const auto address = Common::AlignDown(begin + TRACKER_PAGE_SIZE, TRACKER_PAGE_SIZE);
|
|
const auto size = address - begin;
|
|
image.track_addr = address;
|
|
if (image.track_addr == image.track_addr_end) {
|
|
MarkAsMaybeDirty(id, image);
|
|
}
|
|
if (size != 0) {
|
|
m_page_manager.UpdatePageWatchers<false>(begin, size);
|
|
}
|
|
}
|
|
|
|
void TextureCache::UntrackImageTail(ImageId id) {
|
|
auto& image = m_slot_images[id];
|
|
const auto end = image.info.data.End();
|
|
if (!image.IsTracked() || image.track_addr_end < end) {
|
|
return;
|
|
}
|
|
const auto address = Common::AlignDown(end, TRACKER_PAGE_SIZE);
|
|
const auto size = end - address;
|
|
image.track_addr_end = address;
|
|
if (image.track_addr == image.track_addr_end) {
|
|
MarkAsMaybeDirty(id, image);
|
|
}
|
|
if (size != 0) {
|
|
m_page_manager.UpdatePageWatchers<false>(address, size);
|
|
}
|
|
}
|
|
|
|
void TextureCache::TrackImageDownload(ImageId id, Image& image) {
|
|
if (m_readback_linear_images && !image.info.IsTiled() && !image.info.data.Empty()) {
|
|
if (!image.IsGpuModified()) {
|
|
EXIT("TextureCache: cannot enroll a non-GPU-owned image for download\n");
|
|
}
|
|
m_download_images.insert(id);
|
|
}
|
|
}
|
|
|
|
TextureCache::ImageIds TextureCache::FindImagesInRegion(uint64_t address, uint64_t size,
|
|
bool page_overlap) const {
|
|
ImagePageTable::PageRange pages {};
|
|
if (!ImagePageTable::TryGetPageRange(address, size, pages)) {
|
|
return {};
|
|
}
|
|
|
|
uint32_t query_epoch = ++m_image_query_epoch;
|
|
if (query_epoch == 0) {
|
|
m_slot_images.ForEach([](ImageId, const Image& image) { image.query_epoch = 0; });
|
|
query_epoch = ++m_image_query_epoch;
|
|
}
|
|
|
|
ImageIds result;
|
|
ForEachPage(address, size, [&](uint64_t page) {
|
|
const auto* owners = m_image_page_table.Find(page);
|
|
if (owners == nullptr) {
|
|
return;
|
|
}
|
|
owners->ForEach([&](ImageId id) {
|
|
auto* image = m_slot_images.try_get(id);
|
|
if (image == nullptr) {
|
|
return;
|
|
}
|
|
if (image->query_epoch == query_epoch) {
|
|
return;
|
|
}
|
|
image->query_epoch = query_epoch;
|
|
if (image->Overlaps(address, size, page_overlap)) {
|
|
result.push_back(id);
|
|
}
|
|
});
|
|
});
|
|
return result;
|
|
}
|
|
|
|
ImageId TextureCache::GetNullImage(const ImageDesc& desc) {
|
|
const auto format = desc.info.pixel_format;
|
|
if (const auto found = m_null_images.find(format); found != m_null_images.end()) {
|
|
return found->second;
|
|
}
|
|
ImageInfo info {};
|
|
info.pixel_format = desc.info.pixel_format;
|
|
info.guest_format = desc.info.guest_format;
|
|
info.type = Prospero::ImageType::kColor2D;
|
|
info.extent = {1, 1, 1};
|
|
info.resources = {1, 1};
|
|
info.pitch = 1;
|
|
info.bytes_per_block = std::max(desc.info.bytes_per_block, 1u);
|
|
info.samples = 1;
|
|
info.tile_mode = Prospero::TileMode::kLinear;
|
|
info.mip_layout[0] = {0, info.bytes_per_block, 1, 1};
|
|
const auto id = InsertImage(info);
|
|
m_null_images.emplace(format, id);
|
|
return id;
|
|
}
|
|
|
|
void TextureCache::ValidateImageDesc(const ImageDesc& desc) const {
|
|
ImageOps::Validate(desc.info);
|
|
if (desc.view_info.format == vk::Format::eUndefined || desc.view_info.level_count == 0 ||
|
|
desc.view_info.layer_count == 0 ||
|
|
desc.view_info.base_level >= desc.info.resources.levels ||
|
|
desc.view_info.level_count > desc.info.resources.levels - desc.view_info.base_level ||
|
|
(!desc.info.IsVolume() &&
|
|
(desc.view_info.base_layer >= desc.info.resources.layers ||
|
|
desc.view_info.layer_count > desc.info.resources.layers - desc.view_info.base_layer))) {
|
|
EXIT("TextureCache: invalid image view description\n");
|
|
}
|
|
if (desc.type == BindingType::DepthTarget && !IsSupportedDepthTargetFormat(desc.info)) {
|
|
EXIT("TextureCache: unsupported depth image description\n");
|
|
}
|
|
if (desc.type == BindingType::VideoOut && !IsSupportedVideoOutFormat(desc.info)) {
|
|
EXIT("TextureCache: unsupported video-out image description\n");
|
|
}
|
|
if (desc.type == BindingType::VideoOut &&
|
|
desc.info.metadata.compression == VideoOutCompression::Unsupported) {
|
|
EXIT("TextureCache: unsupported compressed video-out description\n");
|
|
}
|
|
}
|
|
|
|
void TextureCache::PrepareImageCopy(Image& image) {
|
|
if (image.IsCpuDirty()) {
|
|
image.RefreshComplete();
|
|
}
|
|
}
|
|
|
|
void TextureCache::RefreshCopySource(ImageId id) {
|
|
auto& image = m_slot_images[id];
|
|
RefreshImage(id);
|
|
if (image.IsDefinitelyCpuDirty()) {
|
|
EXIT("TextureCache: image copy source remained CPU-dirty after refresh\n");
|
|
}
|
|
}
|
|
|
|
bool TextureCache::CopyD16(Image& destination, Image& source) {
|
|
const bool source_depth = source.info.IsDepth();
|
|
const bool destination_depth = destination.info.IsDepth();
|
|
if (source_depth == destination_depth) {
|
|
return false;
|
|
}
|
|
auto& depth = source_depth ? source : destination;
|
|
auto& color = source_depth ? destination : source;
|
|
const auto transfer_bytes = DepthAspectTransferBytes(depth.backing.format);
|
|
if (depth.info.bytes_per_block != sizeof(uint16_t) ||
|
|
color.info.bytes_per_block != sizeof(uint16_t) || transfer_bytes != sizeof(uint32_t)) {
|
|
return false;
|
|
}
|
|
EXIT_IF(source.backing.samples != 1 || destination.backing.samples != 1 ||
|
|
source.info.resources.levels != 1 || destination.info.resources.levels != 1 ||
|
|
source.info.extent != destination.info.extent ||
|
|
source.info.resources.layers != destination.info.resources.layers);
|
|
|
|
const auto layers = depth.info.resources.layers;
|
|
const uint64_t depth_slice =
|
|
static_cast<uint64_t>(depth.info.pitch) * depth.info.extent.height * transfer_bytes;
|
|
const uint64_t color_slice =
|
|
static_cast<uint64_t>(color.info.pitch) * color.info.extent.height * sizeof(uint16_t);
|
|
EXIT_IF(layers == 0 || depth_slice > UINT64_MAX / layers || color_slice > UINT64_MAX / layers);
|
|
const auto depth_size = depth_slice * layers;
|
|
const auto color_size = color_slice * layers;
|
|
std::vector<vk::BufferImageCopy> depth_copies(layers);
|
|
std::vector<vk::BufferImageCopy> color_copies(layers);
|
|
for (uint32_t layer = 0; layer < layers; layer++) {
|
|
depth_copies[layer].bufferOffset = depth_slice * layer;
|
|
depth_copies[layer].bufferRowLength = depth.info.pitch;
|
|
depth_copies[layer].bufferImageHeight = depth.info.extent.height;
|
|
depth_copies[layer].imageSubresource = {vk::ImageAspectFlagBits::eDepth, 0, layer, 1};
|
|
depth_copies[layer].imageExtent = depth.info.extent;
|
|
color_copies[layer].bufferOffset = color_slice * layer;
|
|
color_copies[layer].bufferRowLength = color.info.pitch;
|
|
color_copies[layer].bufferImageHeight = color.info.extent.height;
|
|
color_copies[layer].imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, layer, 1};
|
|
color_copies[layer].imageExtent = color.info.extent;
|
|
}
|
|
|
|
auto depth_buffer = m_tiler.GetScratchBuffer(depth_size);
|
|
auto color_buffer = m_tiler.GetScratchBuffer(color_size);
|
|
const TileManager::D16Layout promote_layout {
|
|
.width = depth.info.extent.width,
|
|
.height = depth.info.extent.height,
|
|
.layers = layers,
|
|
.source_row_stride = static_cast<uint64_t>(color.info.pitch) * sizeof(uint16_t),
|
|
.target_row_stride = static_cast<uint64_t>(depth.info.pitch) * transfer_bytes,
|
|
.source_slice_stride = color_slice,
|
|
.target_slice_stride = depth_slice,
|
|
};
|
|
const bool d32 = DepthAspectTransferFormat(depth.backing.format) == vk::Format::eD32Sfloat;
|
|
if (source_depth) {
|
|
source.Download(depth_copies, depth_buffer.buffer, depth_buffer.offset, depth_buffer.size);
|
|
m_tiler.ConvertD16(depth_buffer, color_buffer, TileManager::D16Direction::Demote, d32,
|
|
{.width = promote_layout.width,
|
|
.height = promote_layout.height,
|
|
.layers = promote_layout.layers,
|
|
.source_row_stride = promote_layout.target_row_stride,
|
|
.target_row_stride = promote_layout.source_row_stride,
|
|
.source_slice_stride = promote_layout.target_slice_stride,
|
|
.target_slice_stride = promote_layout.source_slice_stride});
|
|
destination.Upload(color_copies, color_buffer.buffer, color_buffer.offset,
|
|
color_buffer.size);
|
|
} else {
|
|
source.Download(color_copies, color_buffer.buffer, color_buffer.offset, color_buffer.size);
|
|
m_tiler.ConvertD16(color_buffer, depth_buffer, TileManager::D16Direction::Promote, d32,
|
|
promote_layout);
|
|
destination.Upload(depth_copies, depth_buffer.buffer, depth_buffer.offset,
|
|
depth_buffer.size);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void TextureCache::CopyImage(ImageId destination_id, ImageId source_id) {
|
|
RefreshCopySource(source_id);
|
|
auto& destination = m_slot_images[destination_id];
|
|
auto& source = m_slot_images[source_id];
|
|
TrackImage(destination_id);
|
|
if (source.backing.samples != destination.backing.samples) {
|
|
EXIT("TextureCache: cannot issue an unequal-sample image copy\n");
|
|
}
|
|
PrepareImageCopy(destination);
|
|
if (source.IsBufferModified()) {
|
|
if (source.info.data == destination.info.data) {
|
|
destination.MarkBufferModified();
|
|
}
|
|
return;
|
|
}
|
|
const bool source_depth = source.info.IsDepth();
|
|
const bool dest_depth = destination.info.IsDepth();
|
|
const bool direct_copy =
|
|
source.backing.format == destination.backing.format ||
|
|
(!source_depth && !dest_depth &&
|
|
vk::blockSize(source.backing.format) == vk::blockSize(destination.backing.format));
|
|
if (direct_copy) {
|
|
destination.CopyImage(source);
|
|
} else if (!CopyD16(destination, source)) {
|
|
if (source.backing.samples != 1 || destination.backing.samples != 1) {
|
|
EXIT("TextureCache: cross-format multisample image copy is unsupported\n");
|
|
}
|
|
auto& copy_buffer = m_buffer_cache.GetUtilityBuffer(MemoryUsage::DeviceLocal);
|
|
destination.CopyImageWithBuffer(source, copy_buffer);
|
|
}
|
|
if (source.IsGpuModified()) {
|
|
destination.MarkGpuModified();
|
|
}
|
|
destination.ClearBufferModified();
|
|
}
|
|
|
|
void TextureCache::CopyImageMip(ImageId destination_id, ImageId source_id, uint32_t mip,
|
|
uint32_t layer) {
|
|
RefreshCopySource(source_id);
|
|
auto& destination = m_slot_images[destination_id];
|
|
auto& source = m_slot_images[source_id];
|
|
TrackImage(destination_id);
|
|
if (source.IsBufferModified() || source.backing.samples != destination.backing.samples) {
|
|
EXIT("TextureCache: invalid mip-copy ownership or sample count\n");
|
|
}
|
|
destination.CopyMip(source, mip, layer);
|
|
if (source.IsGpuModified()) {
|
|
destination.MarkGpuModified();
|
|
}
|
|
}
|
|
|
|
ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingType binding,
|
|
ImageId cached_id) {
|
|
auto& cached = m_slot_images[cached_id];
|
|
if (!cached.info.IsDepth() && !requested.IsDepth()) {
|
|
return {};
|
|
}
|
|
const bool stencil_match = requested.HasStencil() == cached.info.HasStencil();
|
|
const bool bpp_match = requested.bytes_per_block == cached.info.bytes_per_block;
|
|
// PPSA04264
|
|
const bool raw_d16_texture =
|
|
binding == BindingType::Texture && cached.info.IsDepth() &&
|
|
cached.info.guest_format == Prospero::BufferFormat::k16UNorm &&
|
|
requested.guest_format == Prospero::BufferFormat::k16UInt &&
|
|
requested.pixel_format == vk::Format::eR16Uint && cached.backing.samples == 1 &&
|
|
requested.samples == 1 && requested.data == cached.info.data &&
|
|
requested.extent == cached.info.extent && requested.resources == cached.info.resources &&
|
|
requested.type == cached.info.type && requested.pitch == cached.info.pitch &&
|
|
requested.tile_mode == cached.info.tile_mode && !requested.HasStencil() &&
|
|
!cached.info.HasStencil() && !requested.HasMetadata() && !cached.info.HasMetadata();
|
|
// PPSA04264
|
|
const bool retain_cached_layout =
|
|
requested.samples == 1 && cached.info.samples == 1 && cached.backing.samples == 1 &&
|
|
requested.bytes_per_block == cached.info.bytes_per_block &&
|
|
requested.data.address == cached.info.data.address &&
|
|
requested.data.size < cached.info.data.size && requested.extent == cached.info.extent &&
|
|
requested.resources.levels == 1 && cached.info.resources.levels == 1 &&
|
|
requested.resources.layers != 0 && cached.info.resources.layers != 0 &&
|
|
requested.resources.layers < cached.info.resources.layers &&
|
|
requested.type == cached.info.type && requested.pitch == cached.info.pitch &&
|
|
requested.tile_mode == cached.info.tile_mode && requested.mip_layout[0].offset == 0 &&
|
|
cached.info.mip_layout[0].offset == 0 &&
|
|
requested.mip_layout[0].size == requested.data.size &&
|
|
cached.info.mip_layout[0].size == cached.info.data.size &&
|
|
requested.data.size % requested.resources.layers == 0 &&
|
|
cached.info.data.size % cached.info.resources.layers == 0 &&
|
|
requested.data.size / requested.resources.layers ==
|
|
cached.info.data.size / cached.info.resources.layers &&
|
|
!requested.HasStencil() && !cached.info.HasStencil() && !requested.HasMetadata() &&
|
|
!cached.info.HasMetadata();
|
|
bool recreate = cached.info.resources < requested.resources;
|
|
switch (binding) {
|
|
case BindingType::Texture:
|
|
recreate |= requested.IsDepth() && !cached.info.IsDepth();
|
|
recreate |= raw_d16_texture;
|
|
break;
|
|
case BindingType::Storage: recreate |= cached.info.IsDepth(); break;
|
|
case BindingType::RenderTarget: recreate |= cached.info.IsDepth(); break;
|
|
case BindingType::DepthTarget:
|
|
recreate |= !cached.info.IsDepth();
|
|
recreate |= cached.info.IsDepth() && !(stencil_match && bpp_match);
|
|
break;
|
|
case BindingType::VideoOut: recreate |= cached.info.IsDepth(); break;
|
|
}
|
|
if (!recreate) {
|
|
return cached_id;
|
|
}
|
|
RefreshImage(cached_id);
|
|
auto info = requested;
|
|
if (retain_cached_layout) {
|
|
info.data = cached.info.data;
|
|
info.resources = cached.info.resources;
|
|
info.mip_layout = cached.info.mip_layout;
|
|
} else {
|
|
info.resources = std::max(requested.resources, cached.info.resources);
|
|
}
|
|
info.htile_clear_mask = 0;
|
|
const auto replacement_id = InsertImage(info);
|
|
auto& replacement = m_slot_images[replacement_id];
|
|
replacement.usage = cached.usage;
|
|
if (cached.binding.is_bound || cached.binding.is_target) {
|
|
cached.binding.needs_rebind = true;
|
|
}
|
|
if (cached.backing.samples == replacement.backing.samples) {
|
|
const bool copy_supported =
|
|
cached.backing.samples == 1 || cached.backing.format == replacement.backing.format ||
|
|
(!cached.info.IsDepth() && !replacement.info.IsDepth() &&
|
|
ImageViewOps::FormatsCompatible(cached.backing.format, replacement.backing.format));
|
|
if (copy_supported) {
|
|
CopyImage(replacement_id, cached_id);
|
|
} else {
|
|
LOGF_COLOR(Log::Color::BrightYellow,
|
|
"TextureCache: unsupported cross-format multisample depth copy\n");
|
|
}
|
|
} else if (cached.backing.samples == 1 && replacement.backing.samples > 1 &&
|
|
replacement.info.IsDepth()) {
|
|
RefreshCopySource(cached_id);
|
|
if (cached.IsBufferModified() || cached.IsDefinitelyCpuDirty()) {
|
|
EXIT("TextureCache: multisample depth conversion source is not native-current\n");
|
|
}
|
|
PrepareImageCopy(replacement);
|
|
m_blit_helper.ReinterpretColorAsMsDepth(cached, replacement);
|
|
CommitGpuWrite(replacement);
|
|
} else {
|
|
LOGF_COLOR(Log::Color::BrightYellow,
|
|
"TextureCache: unsupported unequal-sample depth overlap copy (%u -> %u)\n",
|
|
cached.backing.samples, replacement.backing.samples);
|
|
}
|
|
FreeImage(cached_id);
|
|
return replacement_id;
|
|
}
|
|
|
|
TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& requested,
|
|
BindingType binding, ImageId cached_id,
|
|
ImageId merged_id) {
|
|
auto owner = m_slot_images.try_get(cached_id);
|
|
if (owner == nullptr) {
|
|
return {merged_id};
|
|
}
|
|
auto& cached = *owner;
|
|
const auto current_tick = m_scheduler.CurrentTick();
|
|
const bool safe_to_delete =
|
|
current_tick - std::min(current_tick, cached.tick_accessed_last) > NumFramesBeforeRemoval;
|
|
|
|
const uint32_t requested_block = requested.bytes_per_block * requested.samples;
|
|
const uint32_t cached_block = cached.info.bytes_per_block * cached.info.samples;
|
|
if (requested.data.address == cached.info.data.address &&
|
|
requested.BlockExtent() == cached.info.BlockExtent() && requested_block == cached_block) {
|
|
if (const auto depth_id = ResolveDepthOverlap(requested, binding, cached_id)) {
|
|
return {depth_id};
|
|
}
|
|
if (requested.IsBlock() && !cached.info.IsBlock()) {
|
|
return {ExpandImage(requested, cached_id)};
|
|
}
|
|
if (requested.data.size == cached.info.data.size &&
|
|
(requested.IsVolume() || cached.info.IsVolume())) {
|
|
return {ExpandImage(requested, cached_id)};
|
|
}
|
|
// Equal pitch does not imply equal mip placement: a changed extent can move
|
|
// a level into or out of the mip tail. These are separate guest layouts.
|
|
if (requested.tile_mode != cached.info.tile_mode ||
|
|
(requested.resources == cached.info.resources &&
|
|
requested.mip_layout != cached.info.mip_layout)) {
|
|
if (safe_to_delete) {
|
|
FreeImage(cached_id);
|
|
}
|
|
return {merged_id};
|
|
}
|
|
// PPSA08394
|
|
if (requested.data.size == cached.info.data.size &&
|
|
requested.resources == cached.info.resources && requested.type == cached.info.type &&
|
|
requested.extent.width > cached.info.extent.width &&
|
|
requested.extent.height >= cached.info.extent.height &&
|
|
requested.extent.depth >= cached.info.extent.depth &&
|
|
ImageViewOps::FormatsCompatible(cached.info.pixel_format, requested.pixel_format)) {
|
|
return {ExpandImage(requested, cached_id)};
|
|
}
|
|
if (requested.pixel_format != cached.info.pixel_format ||
|
|
requested.data.size <= cached.info.data.size) {
|
|
const auto result_id = merged_id ? merged_id : cached_id;
|
|
const auto result = m_slot_images.try_get(result_id);
|
|
return {result != nullptr && ImageViewOps::FormatsCompatible(result->info.pixel_format,
|
|
requested.pixel_format)
|
|
? result_id
|
|
: ImageId {}};
|
|
}
|
|
if (requested.type == cached.info.type && requested.resources > cached.info.resources) {
|
|
return {ExpandImage(requested, cached_id)};
|
|
}
|
|
EXIT("TextureCache: unresolvable equal-address image overlap, address=0x%016" PRIx64
|
|
" requested=%ux%u "
|
|
"cached=%ux%u requested_size=0x%016" PRIx64 " cached_size=0x%016" PRIx64
|
|
" type=%u/%u tile=%u/%u\n",
|
|
requested.data.address, requested.resources.levels, requested.resources.layers,
|
|
cached.info.resources.levels, cached.info.resources.layers, requested.data.size,
|
|
cached.info.data.size, static_cast<uint32_t>(requested.type),
|
|
static_cast<uint32_t>(cached.info.type), static_cast<uint32_t>(requested.tile_mode),
|
|
static_cast<uint32_t>(cached.info.tile_mode));
|
|
}
|
|
|
|
const int32_t requested_mip = requested.MipOf(cached.info);
|
|
if (requested_mip >= 0) {
|
|
const int32_t layer = requested.SliceOf(cached.info, requested_mip);
|
|
return {cached_id, requested_mip, layer};
|
|
}
|
|
|
|
const int32_t mip = cached.info.MipOf(requested);
|
|
if (mip >= 0) {
|
|
const int32_t layer = cached.info.SliceOf(requested, mip);
|
|
if (!merged_id) {
|
|
return {ExpandImage(requested, cached_id)};
|
|
}
|
|
cached.binding.needs_rebind |= cached.binding.is_bound || cached.binding.is_target;
|
|
m_slot_images[merged_id].binding.is_target |= cached.binding.is_target;
|
|
CopyImageMip(merged_id, cached_id, static_cast<uint32_t>(mip),
|
|
static_cast<uint32_t>(layer));
|
|
FreeImage(cached_id);
|
|
return {merged_id};
|
|
}
|
|
if (requested.data.address >= cached.info.data.address && safe_to_delete) {
|
|
FreeImage(cached_id);
|
|
}
|
|
return {merged_id};
|
|
}
|
|
|
|
ImageId TextureCache::ExpandImage(const ImageInfo& info, ImageId source_id) {
|
|
RefreshCopySource(source_id);
|
|
const auto expanded_id = InsertImage(info);
|
|
auto& expanded = m_slot_images[expanded_id];
|
|
auto& source = m_slot_images[source_id];
|
|
expanded.usage = source.usage;
|
|
if (source.binding.is_bound || source.binding.is_target) {
|
|
source.binding.needs_rebind = true;
|
|
}
|
|
InitializeImage(expanded_id);
|
|
const int32_t mip = source.info.MipOf(info);
|
|
const int32_t layer = source.info.SliceOf(info, mip);
|
|
if (layer >= 0) {
|
|
CopyImageMip(expanded_id, source_id, static_cast<uint32_t>(mip),
|
|
static_cast<uint32_t>(layer));
|
|
} else {
|
|
CopyImage(expanded_id, source_id);
|
|
}
|
|
FreeImage(source_id);
|
|
return expanded_id;
|
|
}
|
|
|
|
struct TextureCache::TextureTransfer {
|
|
TextureUploadLayout layout;
|
|
std::vector<vk::BufferImageCopy> regions;
|
|
std::vector<GpuTileInfo> tiles;
|
|
bool swap_bgra16 = false;
|
|
bool valid = false;
|
|
|
|
[[nodiscard]] uint64_t LinearSize() const {
|
|
uint64_t size = 0;
|
|
for (const auto& tile: tiles) {
|
|
size = std::max(size, tile.linear_offset + tile.linear_size);
|
|
}
|
|
return size;
|
|
}
|
|
};
|
|
|
|
struct TextureCache::ImageDownload {
|
|
TextureTransfer texture;
|
|
bool depth_target = false;
|
|
bool valid = false;
|
|
};
|
|
|
|
TextureCache::TextureTransfer
|
|
TextureCache::BuildTextureTransfer(const Image& image, BindingType binding,
|
|
TransferDirection direction) const {
|
|
const auto& info = image.info;
|
|
const bool upload = direction == TransferDirection::Upload;
|
|
const bool render_target = binding == BindingType::RenderTarget;
|
|
const bool video_out = binding == BindingType::VideoOut;
|
|
auto format = info.guest_format;
|
|
uint32_t layers = info.TransferLayers();
|
|
bool volume = info.IsVolume();
|
|
bool allow_depth_tile = upload;
|
|
const char* owner = "TextureCache readback";
|
|
|
|
TextureTransfer transfer;
|
|
transfer.swap_bgra16 = info.bgra16 && (!upload || render_target || video_out);
|
|
if (render_target) {
|
|
format = ImageOps::RenderTargetTransferFormat(info.bytes_per_block);
|
|
}
|
|
if (video_out) {
|
|
allow_depth_tile = false;
|
|
} else if (render_target || binding == BindingType::Storage) {
|
|
allow_depth_tile = true;
|
|
}
|
|
if (upload) {
|
|
if ((render_target || video_out) &&
|
|
(info.resources.layers == 0 || info.data.size % info.resources.layers != 0 ||
|
|
info.samples != 1 || image.backing.samples != 1)) {
|
|
EXIT("TextureCache: invalid color-attachment upload\n");
|
|
}
|
|
owner = "TextureCache";
|
|
if (render_target) {
|
|
owner = "RenderTarget";
|
|
} else if (binding == BindingType::Storage) {
|
|
owner = "StorageTextureCache";
|
|
} else if (video_out) {
|
|
if (info.metadata.compression != VideoOutCompression::Uncompressed) {
|
|
EXIT("TextureCache: invalid color-attachment upload\n");
|
|
}
|
|
layers = info.resources.layers;
|
|
volume = false;
|
|
owner = "VideoOut";
|
|
}
|
|
}
|
|
|
|
transfer.layout = TextureCalcUploadLayout(format, info.extent.width, info.extent.height,
|
|
info.resources.levels, layers, info.tile_mode,
|
|
info.data.size, allow_depth_tile, volume, owner);
|
|
transfer.regions = TextureBuildImageCopies(transfer.layout);
|
|
if (info.IsDepth()) {
|
|
for (auto& region: transfer.regions) {
|
|
region.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eDepth;
|
|
}
|
|
}
|
|
if (transfer.layout.surface.description.tile_mode != Prospero::TileMode::kLinear) {
|
|
if (!TextureBuildGpuTileInfos(info.data.size, transfer.regions, transfer.layout,
|
|
info.resources.levels, transfer.tiles)) {
|
|
return transfer;
|
|
}
|
|
}
|
|
transfer.valid = true;
|
|
return transfer;
|
|
}
|
|
|
|
TextureCache::ImageDownload TextureCache::BuildDownload(const Image& image) const {
|
|
const auto& info = image.info;
|
|
const auto binding = UploadBinding(image);
|
|
ImageDownload transfer {.depth_target = binding == BindingType::DepthTarget};
|
|
if (info.samples != 1 || image.backing.samples != 1) {
|
|
return transfer;
|
|
}
|
|
if (transfer.depth_target) {
|
|
transfer.valid = IsSupportedDepthPlaneReadback(info) && info.resources.layers != 0 &&
|
|
info.data.size % info.resources.layers == 0 &&
|
|
Prospero::NumBytesPerElement(info.guest_format) == info.bytes_per_block;
|
|
return transfer;
|
|
}
|
|
if (info.metadata.compression != VideoOutCompression::Uncompressed) {
|
|
return transfer;
|
|
}
|
|
transfer.texture = BuildTextureTransfer(image, binding, TransferDirection::Download);
|
|
transfer.valid = transfer.texture.valid;
|
|
return transfer;
|
|
}
|
|
|
|
void TextureCache::UploadImage(Image& image, Buffer& source, uint64_t source_offset) {
|
|
const auto& info = image.info;
|
|
const auto binding = UploadBinding(image);
|
|
const auto upload = [&](std::vector<vk::BufferImageCopy>& copies, TileManager::Result linear) {
|
|
for (auto& copy: copies) {
|
|
copy.bufferOffset += linear.offset;
|
|
}
|
|
image.Upload(copies, linear.buffer, linear.offset, linear.size);
|
|
};
|
|
|
|
if (binding != BindingType::DepthTarget) {
|
|
auto transfer = BuildTextureTransfer(image, binding, TransferDirection::Upload);
|
|
if (!transfer.valid) {
|
|
EXIT("TextureCache: invalid texture upload: binding=%u addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " format=%u tile=%u family=%u extent=%ux%ux%u "
|
|
"pitch=%u levels=%u layers=%u samples=%u\n",
|
|
static_cast<uint32_t>(binding), info.data.address, info.data.size,
|
|
static_cast<uint32_t>(info.guest_format), static_cast<uint32_t>(info.tile_mode),
|
|
static_cast<uint32_t>(transfer.layout.surface.texture.block.family), info.extent.width,
|
|
info.extent.height, info.extent.depth, info.pitch, info.resources.levels,
|
|
info.resources.layers, info.samples);
|
|
}
|
|
TileManager::Result linear {source.Handle(), source_offset, info.data.size};
|
|
if (!transfer.tiles.empty()) {
|
|
linear = m_tiler.Detile(source.Handle(), source_offset, info.data.size,
|
|
transfer.LinearSize(), transfer.tiles);
|
|
}
|
|
if (transfer.swap_bgra16) {
|
|
linear = m_tiler.SwapBgra16(linear);
|
|
}
|
|
upload(transfer.regions, linear);
|
|
return;
|
|
}
|
|
|
|
if (info.samples != 1 || image.backing.samples != 1 ||
|
|
info.resources.layers == 0 || info.data.size % info.resources.layers != 0 ||
|
|
Prospero::NumBytesPerElement(info.guest_format) != info.bytes_per_block) {
|
|
EXIT("TextureCache: invalid depth upload\n");
|
|
}
|
|
const auto layers = info.resources.layers;
|
|
const auto full_slice_size = info.data.size / layers;
|
|
auto copies = BuildDepthCopies(info, full_slice_size);
|
|
TileManager::Result linear {source.Handle(), source_offset, source.Size() - source_offset};
|
|
if (info.IsTiled()) {
|
|
const auto tiles = BuildDepthTiles(info);
|
|
linear =
|
|
m_tiler.Detile(source.Handle(), source_offset, info.data.size, info.data.size, tiles);
|
|
}
|
|
const auto transfer_bytes = DepthAspectTransferBytes(info.pixel_format);
|
|
if (transfer_bytes != info.bytes_per_block) {
|
|
const uint64_t texels_per_slice = static_cast<uint64_t>(info.pitch) * info.extent.height;
|
|
EXIT_NOT_IMPLEMENTED(info.bytes_per_block != sizeof(uint16_t) ||
|
|
transfer_bytes != sizeof(uint32_t) || texels_per_slice > UINT32_MAX ||
|
|
texels_per_slice > UINT64_MAX / transfer_bytes);
|
|
const uint64_t transfer_slice = texels_per_slice * transfer_bytes;
|
|
EXIT_NOT_IMPLEMENTED(transfer_slice > UINT64_MAX / layers);
|
|
auto promoted = m_tiler.GetScratchBuffer(transfer_slice * layers);
|
|
m_tiler.ConvertD16(
|
|
linear, promoted, TileManager::D16Direction::Promote,
|
|
info.pixel_format == vk::Format::eD32SfloatS8Uint,
|
|
{.width = info.extent.width,
|
|
.height = info.extent.height,
|
|
.layers = layers,
|
|
.source_row_stride = static_cast<uint64_t>(info.pitch) * sizeof(uint16_t),
|
|
.target_row_stride = static_cast<uint64_t>(info.pitch) * sizeof(uint32_t),
|
|
.source_slice_stride = full_slice_size,
|
|
.target_slice_stride = transfer_slice});
|
|
linear = promoted;
|
|
for (uint32_t layer = 0; layer < layers; layer++) {
|
|
copies[layer].bufferOffset = transfer_slice * layer;
|
|
}
|
|
}
|
|
upload(copies, linear);
|
|
}
|
|
|
|
void TextureCache::InitializeImage(ImageId id) {
|
|
auto& image = m_slot_images[id];
|
|
if (image.info.data.Empty()) {
|
|
return;
|
|
}
|
|
TrackImage(id);
|
|
if (image.info.metadata.compression != VideoOutCompression::Uncompressed) {
|
|
if (image.IsCpuDirty()) {
|
|
image.RefreshComplete();
|
|
}
|
|
return;
|
|
}
|
|
if (image.info.samples > 1) {
|
|
return;
|
|
}
|
|
const bool upload = image.IsBufferModified() || image.IsCpuDirty();
|
|
if (upload) {
|
|
const auto [source, source_offset] =
|
|
m_buffer_cache.ObtainBufferForImage(image.info.data.address, image.info.data.size);
|
|
if (source == nullptr) {
|
|
EXIT("TextureCache: failed to obtain image upload source\n");
|
|
}
|
|
UploadImage(image, *source, source_offset);
|
|
image.ClearBufferModified();
|
|
}
|
|
if (image.IsCpuDirty()) {
|
|
image.RefreshComplete();
|
|
}
|
|
}
|
|
|
|
void TextureCache::PrepareDccClear(ImageId id, const ImageDesc& desc) {
|
|
if (desc.info.metadata.kind != ImageMetadataKind::Dcc) {
|
|
return;
|
|
}
|
|
auto& image = m_slot_images[id];
|
|
image.info.metadata = desc.info.metadata;
|
|
auto [entry, inserted] = m_surface_metas.try_emplace(
|
|
desc.info.metadata.range.address, MetaDataInfo {.type = MetaDataInfo::Type::Dcc});
|
|
auto& metadata = entry->second;
|
|
if (!inserted && metadata.type == MetaDataInfo::Type::PendingDcc) {
|
|
metadata.type = MetaDataInfo::Type::Dcc;
|
|
} else if (metadata.type != MetaDataInfo::Type::Dcc) {
|
|
EXIT("TextureCache: image reuses non-DCC metadata\n");
|
|
}
|
|
if (metadata.clear_mask == 0 || image.info.resources.levels != 1 ||
|
|
desc.info.metadata.range.size == 0 || metadata.fill_size < desc.info.metadata.range.size) {
|
|
return;
|
|
}
|
|
vk::ClearValue clear {};
|
|
if (!DecodeDccClear(desc, image.backing.format, metadata.fill_value, clear.color)) {
|
|
return;
|
|
}
|
|
const auto& view = desc.view_info;
|
|
const bool volume_texture = image.info.IsVolume() && view.type == vk::ImageViewType::e3D;
|
|
const auto first = volume_texture ? 0u : view.base_layer;
|
|
const auto count = volume_texture ? std::max(image.info.extent.depth >> view.base_level, 1u)
|
|
: view.layer_count;
|
|
if (first >= 32 || count > 32 - first) {
|
|
return;
|
|
}
|
|
// The metadata fill covers the complete allocation. Consume each layer only after its
|
|
// native image contents exist; already materialized layers may have been rendered since.
|
|
for (uint32_t layer = first; layer < first + count;) {
|
|
if ((metadata.clear_mask & (1u << layer)) == 0) {
|
|
layer++;
|
|
continue;
|
|
}
|
|
const auto start = layer;
|
|
uint32_t mask = 0;
|
|
do {
|
|
mask |= 1u << layer++;
|
|
} while (layer < first + count && (metadata.clear_mask & (1u << layer)) != 0);
|
|
ClearImage(m_scheduler.Current(), id,
|
|
{vk::ImageAspectFlagBits::eColor, view.base_level, view.level_count, start,
|
|
layer - start},
|
|
clear);
|
|
metadata.clear_mask &= ~mask;
|
|
}
|
|
}
|
|
|
|
void TextureCache::RefreshImage(ImageId id) {
|
|
TrackImage(id);
|
|
auto& image = m_slot_images[id];
|
|
if (image.IsMaybeCpuDirty()) {
|
|
const auto hash = image.HashGuestEdges();
|
|
if (image.NeedsMaybeCpuHash()) {
|
|
image.SetMaybeCpuHash(hash);
|
|
return;
|
|
}
|
|
(void)image.ResolveMaybeCpuHash(hash);
|
|
}
|
|
bool cpu_dirty = image.IsBufferModified() || image.IsDefinitelyCpuDirty();
|
|
if (image.info.metadata.compression != VideoOutCompression::Uncompressed) {
|
|
if (cpu_dirty) {
|
|
EXIT("TextureCache: compressed guest image refresh is unsupported\n");
|
|
}
|
|
return;
|
|
}
|
|
if (!cpu_dirty) {
|
|
return;
|
|
}
|
|
InitializeImage(id);
|
|
}
|
|
|
|
void TextureCache::AssociateStencil(ImageId depth_id, GuestRange stencil) {
|
|
if (!stencil.Valid()) {
|
|
EXIT("TextureCache: invalid stencil association range\n");
|
|
}
|
|
auto& depth = m_slot_images[depth_id];
|
|
if (!depth.info.IsDepth() || !depth.info.HasStencil()) {
|
|
EXIT("TextureCache: stencil association requires a depth/stencil image\n");
|
|
}
|
|
|
|
ImageId association {};
|
|
for (const auto id: FindImagesInRegion(stencil.address, stencil.size, false)) {
|
|
const auto owner = m_slot_images.try_get(id);
|
|
if (owner != nullptr && owner->info.data.address == stencil.address) {
|
|
association = id;
|
|
}
|
|
}
|
|
if (!association) {
|
|
ImageInfo info {};
|
|
info.data = stencil;
|
|
info.extent = depth.info.extent;
|
|
association = InsertImage(info);
|
|
}
|
|
auto& record = m_slot_images[association];
|
|
TouchImage(record);
|
|
record.depth_id = depth_id;
|
|
}
|
|
|
|
ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
|
|
auto& command = m_scheduler.Current();
|
|
if (command.IsInvalid()) {
|
|
EXIT("TextureCache: image lookup requires a valid command buffer\n");
|
|
}
|
|
ValidateImageDesc(desc);
|
|
if (desc.info.data.Empty()) {
|
|
std::scoped_lock lock {m_lock};
|
|
return GetNullImage(desc);
|
|
}
|
|
|
|
ImageId result {};
|
|
{
|
|
std::scoped_lock lock {m_lock};
|
|
const auto candidates =
|
|
FindImagesInRegion(desc.info.data.address, desc.info.data.size, false);
|
|
|
|
for (const auto id: candidates) {
|
|
const auto& image = m_slot_images[id];
|
|
if (SameBacking(image.info, desc.info, exact_format)) {
|
|
result = id;
|
|
}
|
|
}
|
|
|
|
int32_t view_mip = -1;
|
|
int32_t view_layer = -1;
|
|
if (!result) {
|
|
for (const auto candidate: candidates) {
|
|
view_mip = -1;
|
|
view_layer = -1;
|
|
const auto& merged_info = result ? m_slot_images[result].info : desc.info;
|
|
const auto overlap = ResolveOverlap(merged_info, desc.type, candidate, result);
|
|
if (overlap.image) {
|
|
result = overlap.image;
|
|
view_mip = overlap.mip;
|
|
view_layer = overlap.layer;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (result) {
|
|
auto& resolved = m_slot_images[result];
|
|
if (exact_format && resolved.info.pixel_format != desc.info.pixel_format) {
|
|
result = {};
|
|
} else if (resolved.info.resources < desc.info.resources) {
|
|
FreeImage(result);
|
|
result = {};
|
|
}
|
|
}
|
|
if (!result) {
|
|
result = InsertImage(desc.info);
|
|
auto& inserted = m_slot_images[result];
|
|
if (m_buffer_cache.HasGpuDirtyBytes(inserted.info.data.address,
|
|
inserted.info.data.size)) {
|
|
inserted.MarkBufferModified();
|
|
}
|
|
}
|
|
auto& image = m_slot_images[result];
|
|
if (desc.type == BindingType::VideoOut &&
|
|
desc.info.metadata.compression != VideoOutCompression::Uncompressed) {
|
|
const bool guest_dirty = image.IsBufferModified() || image.IsCpuDirty();
|
|
const bool native_current =
|
|
(image.usage.render_target || image.IsGpuModified()) && !guest_dirty;
|
|
if (!native_current) {
|
|
EXIT("TextureCache: compressed video-out read requires clean native GPU "
|
|
"contents\n");
|
|
}
|
|
}
|
|
if (view_mip >= 0) {
|
|
desc.view_info.base_level = static_cast<uint32_t>(view_mip);
|
|
}
|
|
if (view_layer >= 0) {
|
|
desc.view_info.base_layer = static_cast<uint32_t>(view_layer);
|
|
}
|
|
image.tick_accessed_last = m_scheduler.CurrentTick();
|
|
TouchImage(image);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void TextureCache::UpdateImage(ImageId id) {
|
|
std::scoped_lock lock {m_lock};
|
|
auto& image = m_slot_images[id];
|
|
TouchImage(image);
|
|
RefreshImage(id);
|
|
}
|
|
|
|
ImageId TextureCache::FindImageFromRange(uint64_t address, uint64_t size, bool ensure_valid) {
|
|
if (!GuestRange {address, size}.Valid()) {
|
|
return {};
|
|
}
|
|
std::scoped_lock lock {m_lock};
|
|
ImageIds matches;
|
|
for (const auto id: FindImagesInRegion(address, size, false)) {
|
|
auto owner = m_slot_images.try_get(id);
|
|
if (owner == nullptr || owner->info.data.address != address) {
|
|
continue;
|
|
}
|
|
if (ensure_valid && owner->depth_id) {
|
|
owner = m_slot_images.try_get(owner->depth_id);
|
|
}
|
|
if (owner == nullptr || (ensure_valid && !SafeToDownload(*owner))) {
|
|
continue;
|
|
}
|
|
matches.push_back(id);
|
|
}
|
|
ImageId selected {};
|
|
if (matches.size() == 1) {
|
|
selected = matches.front();
|
|
} else {
|
|
for (const auto id: matches) {
|
|
const auto& image = m_slot_images[id];
|
|
if (image.info.data.size == size) {
|
|
selected = id;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (selected && ensure_valid) {
|
|
const auto owner = m_slot_images.try_get(selected);
|
|
if (owner != nullptr && owner->depth_id) {
|
|
selected = owner->depth_id;
|
|
}
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
vk::ImageView TextureCache::FindTexture(ImageId id, const ImageDesc& desc) {
|
|
std::scoped_lock lock {m_lock};
|
|
auto& image = m_slot_images[id];
|
|
TouchImage(image);
|
|
if (!image.info.data.Empty()) {
|
|
if (!image.registered || image.depth_id || image.binding.needs_rebind) {
|
|
EXIT("TextureCache: texture requires rediscovery before final acquisition\n");
|
|
}
|
|
}
|
|
if (desc.type == BindingType::Storage) {
|
|
image.MarkGpuModified();
|
|
}
|
|
if (!image.info.data.Empty()) {
|
|
PrepareDccClear(id, desc);
|
|
RefreshImage(id);
|
|
}
|
|
switch (desc.type) {
|
|
case BindingType::Texture: break;
|
|
case BindingType::Storage:
|
|
if (!image.info.data.Empty()) {
|
|
CommitGpuWrite(image);
|
|
}
|
|
TrackImageDownload(id, image);
|
|
break;
|
|
default: EXIT("TextureCache: invalid texture binding\n");
|
|
}
|
|
const auto view = image.FindView(desc.view_info);
|
|
NameImageBinding(m_graphics, image, view, desc.type, desc.view_info);
|
|
return view;
|
|
}
|
|
|
|
vk::ImageView TextureCache::FindRenderTarget(ImageId id, const ImageDesc& desc) {
|
|
if (desc.type != BindingType::RenderTarget) {
|
|
EXIT("TextureCache: invalid color-target binding\n");
|
|
}
|
|
std::scoped_lock lock {m_lock};
|
|
auto& image = m_slot_images[id];
|
|
if (!image.registered || image.depth_id || image.binding.needs_rebind) {
|
|
EXIT("TextureCache: color target requires rediscovery before final acquisition\n");
|
|
}
|
|
TouchImage(image);
|
|
image.MarkGpuModified();
|
|
image.usage.render_target = true;
|
|
PrepareDccClear(id, desc);
|
|
RefreshImage(id);
|
|
CommitGpuWrite(image);
|
|
TrackImageDownload(id, image);
|
|
const auto view = image.FindView(desc.view_info);
|
|
NameImageBinding(m_graphics, image, view, desc.type, desc.view_info);
|
|
return view;
|
|
}
|
|
|
|
vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) {
|
|
if (desc.type != BindingType::DepthTarget) {
|
|
EXIT("TextureCache: invalid depth-target binding\n");
|
|
}
|
|
std::scoped_lock lock {m_lock};
|
|
auto& image = m_slot_images[id];
|
|
if (!image.registered || image.depth_id || image.binding.needs_rebind) {
|
|
EXIT("TextureCache: depth target requires rediscovery before final acquisition\n");
|
|
}
|
|
TouchImage(image);
|
|
image.MarkGpuModified();
|
|
image.usage.depth_target = true;
|
|
RefreshImage(id);
|
|
if (desc.info.HasMetadata()) {
|
|
image.info.metadata = desc.info.metadata;
|
|
auto [metadata, inserted] =
|
|
m_surface_metas.try_emplace(desc.info.metadata.range.address,
|
|
MetaDataInfo {.type = MetaDataInfo::Type::HTile,
|
|
.clear_mask = image.info.htile_clear_mask});
|
|
if (!inserted && metadata->second.type != MetaDataInfo::Type::HTile) {
|
|
// PS5 allocations can reuse DCC storage as HTile while the old color image is cached.
|
|
// The depth binding defines the new type; incompatible fill state cannot carry over.
|
|
metadata->second = {.type = MetaDataInfo::Type::HTile,
|
|
.clear_mask = image.info.htile_clear_mask};
|
|
}
|
|
}
|
|
CommitGpuWrite(image);
|
|
if (desc.info.HasStencil()) {
|
|
AssociateStencil(id, desc.info.stencil);
|
|
}
|
|
const auto view = image.FindView(desc.view_info);
|
|
NameImageBinding(m_graphics, image, view, desc.type, desc.view_info);
|
|
return view;
|
|
}
|
|
|
|
void TextureCache::MarkGpuWritten(ImageId id) {
|
|
std::scoped_lock lock {m_lock};
|
|
auto& image = m_slot_images[id];
|
|
if (!image.registered || image.depth_id) {
|
|
EXIT("TextureCache: cannot mark an unavailable image GPU-written\n");
|
|
}
|
|
TrackImage(id);
|
|
CommitGpuWrite(image);
|
|
}
|
|
|
|
void TextureCache::CommitGpuWrite(Image& image) {
|
|
if (image.depth_id || image.backing.image == nullptr) {
|
|
EXIT("TextureCache: stencil association cannot own image contents\n");
|
|
}
|
|
image.ClearBufferModified();
|
|
if (image.IsCpuDirty()) {
|
|
image.RefreshComplete();
|
|
}
|
|
image.MarkGpuModified();
|
|
}
|
|
|
|
bool TextureCache::ClearImageFromBuffer(CommandBuffer& command, uint64_t address, uint64_t size,
|
|
uint32_t packed_clear) {
|
|
if (command.IsInvalid() || !GuestRange {address, size}.Valid()) {
|
|
EXIT("TextureCache: invalid image clear\n");
|
|
}
|
|
std::scoped_lock lock {m_lock};
|
|
ImageId selected {};
|
|
vk::ImageAspectFlags aspect {};
|
|
for (const auto id: FindImagesInRegion(address, size, false)) {
|
|
auto owner = m_slot_images.try_get(id);
|
|
if (owner == nullptr) {
|
|
continue;
|
|
}
|
|
vk::ImageAspectFlags candidate {};
|
|
ImageId candidate_id = id;
|
|
if (owner->depth_id && owner->info.data.address == address &&
|
|
owner->info.data.size == size) {
|
|
candidate = vk::ImageAspectFlagBits::eStencil;
|
|
candidate_id = owner->depth_id;
|
|
owner = m_slot_images.try_get(candidate_id);
|
|
if (owner == nullptr || owner->backing.image == nullptr || !owner->info.HasStencil()) {
|
|
continue;
|
|
}
|
|
} else if (!owner->depth_id && owner->info.data.address == address &&
|
|
owner->info.data.size == size) {
|
|
candidate = owner->info.IsDepth() ? vk::ImageAspectFlagBits::eDepth
|
|
: vk::ImageAspectFlagBits::eColor;
|
|
}
|
|
if (!candidate) {
|
|
continue;
|
|
}
|
|
if (selected && selected != candidate_id) {
|
|
return false;
|
|
}
|
|
selected = candidate_id;
|
|
aspect = candidate;
|
|
}
|
|
if (!selected) {
|
|
return false;
|
|
}
|
|
auto& image = m_slot_images[selected];
|
|
vk::ClearValue clear {};
|
|
if (aspect == vk::ImageAspectFlagBits::eColor) {
|
|
if (!DecodePackedColorClear(image.info.pixel_format, packed_clear, clear.color)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
uint8_t stencil_clear = 0;
|
|
if ((aspect == vk::ImageAspectFlagBits::eDepth &&
|
|
!DecodePackedDepthClear(image.info.pixel_format, packed_clear, clear.depthStencil.depth)) ||
|
|
(aspect == vk::ImageAspectFlagBits::eStencil &&
|
|
!DecodePackedStencilClear(packed_clear, stencil_clear))) {
|
|
return false;
|
|
}
|
|
clear.depthStencil.stencil = stencil_clear;
|
|
}
|
|
ClearImage(command, selected,
|
|
{aspect, 0, image.info.resources.levels, 0, image.info.TransferLayers()}, clear);
|
|
return true;
|
|
}
|
|
|
|
void TextureCache::ClearImage(CommandBuffer& command, ImageId id,
|
|
const vk::ImageSubresourceRange& range, const vk::ClearValue& clear) {
|
|
auto& image = m_slot_images[id];
|
|
const auto aspects = image.info.IsDepth() ? ImageViewOps::DepthAspectMask(image.backing.format)
|
|
: vk::ImageAspectFlagBits::eColor;
|
|
EXIT_IF(range.baseMipLevel >= image.info.resources.levels);
|
|
const auto layers = image.info.IsVolume()
|
|
? std::max(image.info.extent.depth >> range.baseMipLevel, 1u)
|
|
: image.backing.layers;
|
|
EXIT_IF(command.IsInvalid() || image.depth_id || !range.aspectMask || range.levelCount == 0 ||
|
|
range.levelCount > image.info.resources.levels - range.baseMipLevel ||
|
|
range.layerCount == 0 || range.baseArrayLayer >= layers ||
|
|
range.layerCount > layers - range.baseArrayLayer ||
|
|
(range.aspectMask & aspects) != range.aspectMask);
|
|
const bool full_image = range.aspectMask == aspects && range.baseMipLevel == 0 &&
|
|
range.levelCount == image.info.resources.levels &&
|
|
range.baseArrayLayer == 0 && range.layerCount == layers;
|
|
TrackImage(id);
|
|
if (!full_image && (image.IsBufferModified() || image.IsCpuDirty())) {
|
|
InitializeImage(id);
|
|
if (image.info.samples == 1 && (image.IsBufferModified() || image.IsCpuDirty())) {
|
|
EXIT("TextureCache: image clear retained guest ownership\n");
|
|
}
|
|
}
|
|
command.EndRendering();
|
|
if (image.info.IsVolume() && !full_image) {
|
|
EXIT_NOT_IMPLEMENTED(range.aspectMask != vk::ImageAspectFlagBits::eColor ||
|
|
range.levelCount != 1);
|
|
ImageViewInfo view {};
|
|
view.format = image.backing.format;
|
|
view.type = range.layerCount == 1 ? vk::ImageViewType::e2D : vk::ImageViewType::e2DArray;
|
|
view.base_level = range.baseMipLevel;
|
|
view.base_layer = range.baseArrayLayer;
|
|
view.layer_count = range.layerCount;
|
|
view.usage = vk::ImageUsageFlagBits::eColorAttachment;
|
|
image.Transit(vk::ImageLayout::eColorAttachmentOptimal,
|
|
vk::AccessFlagBits2::eColorAttachmentWrite, {}, command.Handle());
|
|
vk::RenderingAttachmentInfo attachment {};
|
|
attachment.imageView = image.FindView(view);
|
|
attachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal;
|
|
attachment.loadOp = vk::AttachmentLoadOp::eClear;
|
|
attachment.storeOp = vk::AttachmentStoreOp::eStore;
|
|
attachment.clearValue = clear;
|
|
vk::RenderingInfo rendering {};
|
|
rendering.renderArea.extent = {
|
|
std::max(image.info.extent.width >> range.baseMipLevel, 1u),
|
|
std::max(image.info.extent.height >> range.baseMipLevel, 1u)};
|
|
rendering.layerCount = range.layerCount;
|
|
rendering.colorAttachmentCount = 1;
|
|
rendering.pColorAttachments = &attachment;
|
|
command.Handle().beginRendering(&rendering);
|
|
command.Handle().endRendering();
|
|
CommitGpuWrite(image);
|
|
return;
|
|
}
|
|
image.Transit(vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, {},
|
|
command.Handle());
|
|
auto native_range = range;
|
|
if (image.info.IsVolume()) {
|
|
native_range.baseArrayLayer = 0;
|
|
native_range.layerCount = 1;
|
|
}
|
|
if (range.aspectMask == vk::ImageAspectFlagBits::eColor) {
|
|
command.Handle().clearColorImage(image.backing.image, vk::ImageLayout::eTransferDstOptimal,
|
|
&clear.color, 1, &native_range);
|
|
} else {
|
|
command.Handle().clearDepthStencilImage(image.backing.image,
|
|
vk::ImageLayout::eTransferDstOptimal,
|
|
&clear.depthStencil, 1, &native_range);
|
|
}
|
|
CommitGpuWrite(image);
|
|
}
|
|
|
|
void TextureCache::InvalidateMemory(uint64_t address, uint64_t size) {
|
|
if (!GuestRange {address, size}.Valid()) {
|
|
EXIT("TextureCache: invalid memory-invalidation range\n");
|
|
}
|
|
std::scoped_lock lock {m_lock};
|
|
InvalidateCpuAliases(address, size);
|
|
}
|
|
|
|
void TextureCache::DownloadDepth(Image& image, Buffer& destination, uint64_t destination_offset) {
|
|
const auto& info = image.info;
|
|
const auto layers = info.resources.layers;
|
|
const auto full_slice_size = info.data.size / layers;
|
|
const auto transfer_bytes = DepthAspectTransferBytes(info.pixel_format);
|
|
const uint64_t texels_per_slice = static_cast<uint64_t>(info.pitch) * info.extent.height;
|
|
EXIT_NOT_IMPLEMENTED(transfer_bytes == 0 || texels_per_slice > UINT32_MAX ||
|
|
texels_per_slice > UINT64_MAX / transfer_bytes ||
|
|
texels_per_slice > UINT64_MAX / info.bytes_per_block);
|
|
const uint64_t transfer_slice = texels_per_slice * transfer_bytes;
|
|
const uint64_t guest_slice = texels_per_slice * info.bytes_per_block;
|
|
EXIT_NOT_IMPLEMENTED(transfer_slice > UINT64_MAX / layers);
|
|
const uint64_t transfer_size = transfer_slice * layers;
|
|
EXIT_NOT_IMPLEMENTED(guest_slice > full_slice_size);
|
|
auto copies = BuildDepthCopies(info, full_slice_size);
|
|
if (transfer_bytes == info.bytes_per_block) {
|
|
if (!info.IsTiled()) {
|
|
for (auto& copy: copies) {
|
|
copy.bufferOffset += destination_offset;
|
|
}
|
|
image.Download(copies, destination.Handle(), destination_offset, info.data.size);
|
|
return;
|
|
}
|
|
const auto tiles = BuildDepthTiles(info);
|
|
m_tiler.TileImage(image, copies, destination.Handle(), destination_offset, info.data.size,
|
|
info.data.size, tiles);
|
|
return;
|
|
}
|
|
EXIT_NOT_IMPLEMENTED(info.bytes_per_block != sizeof(uint16_t) ||
|
|
transfer_bytes != sizeof(uint32_t));
|
|
for (uint32_t layer = 0; layer < layers; layer++) {
|
|
copies[layer].bufferOffset = transfer_slice * layer;
|
|
}
|
|
auto host_linear = m_tiler.GetScratchBuffer(transfer_size);
|
|
image.Download(copies, host_linear.buffer, 0, host_linear.size);
|
|
const bool tiled = info.IsTiled();
|
|
auto guest_linear = tiled ? m_tiler.GetScratchBuffer(info.data.size)
|
|
: TileManager::Result {destination.Handle(), destination_offset,
|
|
destination.Size() - destination_offset};
|
|
m_tiler.ConvertD16(host_linear, guest_linear, TileManager::D16Direction::Demote,
|
|
DepthAspectTransferFormat(info.pixel_format) == vk::Format::eD32Sfloat,
|
|
{.width = info.extent.width,
|
|
.height = info.extent.height,
|
|
.layers = layers,
|
|
.source_row_stride = static_cast<uint64_t>(info.pitch) * sizeof(uint32_t),
|
|
.target_row_stride = static_cast<uint64_t>(info.pitch) * sizeof(uint16_t),
|
|
.source_slice_stride = transfer_slice,
|
|
.target_slice_stride = full_slice_size});
|
|
if (!tiled) {
|
|
return;
|
|
}
|
|
const auto tiles = BuildDepthTiles(info);
|
|
m_tiler.Tile(guest_linear.buffer, guest_linear.offset, info.data.size, destination.Handle(),
|
|
destination_offset, info.data.size, tiles);
|
|
}
|
|
|
|
void TextureCache::DownloadImage(Image& image, Buffer& destination, uint64_t destination_offset,
|
|
uint64_t destination_size, ImageDownload transfer) {
|
|
if (!transfer.valid) {
|
|
EXIT("TextureCache: invalid image download transfer\n");
|
|
}
|
|
if (transfer.depth_target) {
|
|
if (destination_size != image.info.data.size) {
|
|
EXIT("TextureCache: partial depth image download is unsupported\n");
|
|
}
|
|
DownloadDepth(image, destination, destination_offset);
|
|
return;
|
|
}
|
|
|
|
auto& texture = transfer.texture;
|
|
const auto transform = texture.swap_bgra16 ? TileManager::ColorTransform::SwapBgra16
|
|
: TileManager::ColorTransform::None;
|
|
if (texture.tiles.empty()) {
|
|
if (transform == TileManager::ColorTransform::SwapBgra16) {
|
|
auto linear = m_tiler.GetScratchBuffer(destination_size);
|
|
image.Download(texture.regions, linear.buffer, 0, linear.size);
|
|
m_tiler.SwapBgra16(linear,
|
|
{destination.Handle(), destination_offset, destination_size});
|
|
return;
|
|
}
|
|
for (auto& copy: texture.regions) {
|
|
copy.bufferOffset += destination_offset;
|
|
}
|
|
image.Download(texture.regions, destination.Handle(), destination_offset, destination_size);
|
|
return;
|
|
}
|
|
|
|
m_tiler.TileImage(image, texture.regions, destination.Handle(), destination_offset,
|
|
destination_size, texture.LinearSize(), texture.tiles, transform);
|
|
}
|
|
|
|
bool BufferCache::SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uint64_t size) {
|
|
const auto selected = m_texture_cache.FindImageFromRange(vaddr, size);
|
|
if (!selected) {
|
|
return false;
|
|
}
|
|
|
|
std::scoped_lock lock {m_texture_cache.m_lock};
|
|
auto& image = m_texture_cache.m_slot_images[selected];
|
|
// The GPU thread owns image retirement; CPU invalidation can dirty this image after lookup.
|
|
if (!m_texture_cache.SafeToDownload(image)) {
|
|
return false;
|
|
}
|
|
if (!buffer.IsInBounds(image.info.data.address, 1)) {
|
|
return false;
|
|
}
|
|
const auto buf_offset = buffer.Offset(image.info.data.address);
|
|
const auto available = buffer.Size() - buf_offset;
|
|
uint32_t levels = 0;
|
|
uint64_t copy_size = 0;
|
|
if (image.info.IsVolume()) {
|
|
// Volume mips contain strided block slices, so a mip's linear span cannot prove that
|
|
// every retained slice fits. Keep volume synchronization whole-image only.
|
|
if (!buffer.IsInBounds(image.info.data.address, image.info.data.size)) {
|
|
return false;
|
|
}
|
|
levels = image.info.resources.levels;
|
|
copy_size = image.info.data.size;
|
|
} else {
|
|
for (; levels < image.info.resources.levels; ++levels) {
|
|
const auto& mip = image.info.mip_layout[levels];
|
|
if (mip.size == 0 || mip.offset > available || mip.size > available - mip.offset) {
|
|
break;
|
|
}
|
|
copy_size = std::max(copy_size, mip.offset + mip.size);
|
|
}
|
|
}
|
|
if (copy_size == 0) {
|
|
return false;
|
|
}
|
|
auto transfer = m_texture_cache.BuildDownload(image);
|
|
if (!transfer.valid) {
|
|
return false;
|
|
}
|
|
if (transfer.depth_target && copy_size != image.info.data.size) {
|
|
return false;
|
|
}
|
|
if (!transfer.depth_target && levels < image.info.resources.levels) {
|
|
auto& texture = transfer.texture;
|
|
std::erase_if(texture.regions, [levels](const vk::BufferImageCopy& region) {
|
|
return region.imageSubresource.mipLevel >= levels;
|
|
});
|
|
if (texture.regions.empty()) {
|
|
return false;
|
|
}
|
|
if (!texture.tiles.empty()) {
|
|
texture.tiles.clear();
|
|
if (!TextureBuildGpuTileInfos(copy_size, texture.regions, texture.layout, levels,
|
|
texture.tiles)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
m_texture_cache.DownloadImage(image, buffer, buf_offset, copy_size, std::move(transfer));
|
|
return true;
|
|
}
|
|
|
|
bool TextureCache::DownloadImageMemory(ImageId id) {
|
|
auto& image = m_slot_images[id];
|
|
if (image.depth_id) {
|
|
return false;
|
|
}
|
|
auto transfer = BuildDownload(image);
|
|
if (!transfer.valid || !SafeToDownload(image)) {
|
|
return false;
|
|
}
|
|
const auto range = image.info.data;
|
|
auto& download = m_buffer_cache.GetUtilityBuffer(MemoryUsage::Download);
|
|
auto [mapped, offset] =
|
|
download.Map(range.size, std::max<uint64_t>(image.info.bytes_per_block, 4));
|
|
if (mapped == nullptr) {
|
|
EXIT("TextureCache: failed to map reusable download buffer\n");
|
|
}
|
|
download.Commit();
|
|
if (!LibKernel::Memory::TryReadBacking(range.address, mapped, range.size)) {
|
|
return false;
|
|
}
|
|
download.Flush(offset, range.size);
|
|
|
|
DownloadImage(image, download, offset, range.size, std::move(transfer));
|
|
vk::BufferMemoryBarrier barrier {};
|
|
barrier.srcAccessMask = vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eTransferWrite |
|
|
vk::AccessFlagBits::eShaderWrite;
|
|
barrier.dstAccessMask = vk::AccessFlagBits::eHostRead;
|
|
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
barrier.buffer = download.Handle();
|
|
barrier.offset = offset;
|
|
barrier.size = range.size;
|
|
m_scheduler.EndRendering();
|
|
m_scheduler.Current().Handle().pipelineBarrier(vk::PipelineStageFlagBits::eAllCommands,
|
|
vk::PipelineStageFlagBits::eHost, {}, 0, nullptr,
|
|
1, &barrier, 0, nullptr);
|
|
m_scheduler.DeferPriorityOperation([&download, range, mapped, offset] {
|
|
download.Invalidate(offset, range.size);
|
|
LibKernel::Memory::WriteBacking(range.address, mapped, range.size);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
void TextureCache::InvalidateMemoryFromGPU(uint64_t address, uint64_t size) {
|
|
if (!GuestRange {address, size}.Valid()) {
|
|
return;
|
|
}
|
|
std::scoped_lock lock {m_lock};
|
|
for (const auto id: FindImagesInRegion(address, size, true)) {
|
|
auto& image = m_slot_images[id];
|
|
if (image.depth_id || !image.Overlaps(address, size)) {
|
|
continue;
|
|
}
|
|
if (image.IsGpuModified()) {
|
|
image.ClearGpuModified();
|
|
}
|
|
image.MarkBufferModified();
|
|
}
|
|
}
|
|
|
|
bool TextureCache::IsRegionGpuModified(uint64_t address, uint64_t size) {
|
|
if (!GuestRange {address, size}.Valid()) {
|
|
return false;
|
|
}
|
|
std::scoped_lock lock {m_lock};
|
|
for (const auto id: FindImagesInRegion(address, size, false)) {
|
|
const auto& image = m_slot_images[id];
|
|
if (!image.depth_id && image.IsGpuModified()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void TextureCache::InvalidateCpuAliases(uint64_t address, uint64_t size) {
|
|
const auto page_begin = Common::AlignDown(address, TRACKER_PAGE_SIZE);
|
|
const auto page_end = Common::AlignUp(address + size, TRACKER_PAGE_SIZE);
|
|
for (const auto id: FindImagesInRegion(address, size, true)) {
|
|
auto owner = m_slot_images.try_get(id);
|
|
if (owner == nullptr) {
|
|
continue;
|
|
}
|
|
if (owner->Overlaps(address, size)) {
|
|
owner->InvalidateCpuWrite(address, size);
|
|
UntrackImage(id);
|
|
continue;
|
|
}
|
|
const auto image_begin = owner->info.data.address;
|
|
const auto image_end = owner->info.data.End();
|
|
if (page_end < image_end) {
|
|
UntrackImageHead(id);
|
|
} else if (image_begin < page_begin) {
|
|
UntrackImageTail(id);
|
|
} else {
|
|
MarkAsMaybeDirty(id, *owner);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool TextureCache::IsMeta(uint64_t address) {
|
|
std::scoped_lock lock {m_lock};
|
|
const auto found = m_surface_metas.find(address);
|
|
return found != m_surface_metas.end() && found->second.type != MetaDataInfo::Type::PendingDcc;
|
|
}
|
|
|
|
bool TextureCache::IsMetaCleared(uint64_t address, uint32_t slice, uint32_t* fill_value) {
|
|
std::scoped_lock lock {m_lock};
|
|
const auto found = m_surface_metas.find(address);
|
|
if (found == m_surface_metas.end() || found->second.type == MetaDataInfo::Type::PendingDcc ||
|
|
slice >= 32) {
|
|
return false;
|
|
}
|
|
if (fill_value != nullptr) {
|
|
*fill_value = found->second.fill_value;
|
|
}
|
|
return (found->second.clear_mask & (1u << slice)) != 0;
|
|
}
|
|
|
|
bool TextureCache::ClearMeta(uint64_t address) {
|
|
std::scoped_lock lock {m_lock};
|
|
const auto found = m_surface_metas.find(address);
|
|
if (found == m_surface_metas.end() || found->second.type == MetaDataInfo::Type::PendingDcc ||
|
|
found->second.type == MetaDataInfo::Type::Dcc) {
|
|
// Preserve the broad metadata-clear operation for CMask/FMask/HTile. DCC requires a
|
|
// validated fill value, so an arbitrary compute write must not clear it.
|
|
return false;
|
|
}
|
|
found->second.clear_mask = UINT32_MAX;
|
|
return true;
|
|
}
|
|
|
|
void TextureCache::TrackDccFill(uint64_t address, uint64_t size, uint32_t fill_value) {
|
|
if (!GuestRange {address, size}.Valid()) {
|
|
EXIT("TextureCache: invalid DCC fill range\n");
|
|
}
|
|
// DCC fills use a repeated byte code. Require all four bytes of the detected dword to agree,
|
|
// and mark only recognized deferred-clear encodings as logically clear.
|
|
const auto dcc_clear_mask = [fill_value] {
|
|
const auto code = static_cast<uint8_t>(fill_value);
|
|
if (fill_value != static_cast<uint32_t>(code) * 0x01010101u) {
|
|
return 0u;
|
|
}
|
|
switch (code) {
|
|
case 0x00:
|
|
case 0x20:
|
|
case 0x40:
|
|
case 0x80:
|
|
case 0xc0: return UINT32_MAX;
|
|
default: return 0u;
|
|
}
|
|
}();
|
|
std::scoped_lock lock {m_lock};
|
|
// The guest dispatch still writes metadata. An unknown address remains PendingDcc until an
|
|
// image descriptor confirms its role; never reinterpret CMask/FMask/HTile as DCC.
|
|
const auto found = m_surface_metas.try_emplace(address).first;
|
|
if (found->second.type == MetaDataInfo::Type::PendingDcc ||
|
|
found->second.type == MetaDataInfo::Type::Dcc) {
|
|
found->second.clear_mask = dcc_clear_mask;
|
|
found->second.fill_value = fill_value;
|
|
found->second.fill_size = size;
|
|
}
|
|
}
|
|
|
|
bool TextureCache::TouchMeta(uint64_t address, uint32_t slice, bool is_clear) {
|
|
std::scoped_lock lock {m_lock};
|
|
const auto found = m_surface_metas.find(address);
|
|
if (found == m_surface_metas.end() || found->second.type == MetaDataInfo::Type::PendingDcc ||
|
|
slice >= 32) {
|
|
return false;
|
|
}
|
|
if (is_clear) {
|
|
found->second.clear_mask |= 1u << slice;
|
|
} else {
|
|
found->second.clear_mask &= ~(1u << slice);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void TextureCache::UnmapMemory(uint64_t address, uint64_t size) {
|
|
if (!GuestRange {address, size}.Valid()) {
|
|
EXIT("TextureCache: invalid unmap range\n");
|
|
}
|
|
std::scoped_lock lock {m_lock};
|
|
const auto end = address + size;
|
|
for (auto metadata = m_surface_metas.lower_bound(address);
|
|
metadata != m_surface_metas.end() && metadata->first < end;) {
|
|
metadata = m_surface_metas.erase(metadata);
|
|
}
|
|
auto images = FindImagesInRegion(address, size, false);
|
|
for (const auto id: images) {
|
|
auto owner = m_slot_images.try_get(id);
|
|
if (owner == nullptr) {
|
|
continue;
|
|
}
|
|
FreeImage(id);
|
|
}
|
|
}
|
|
|
|
void TextureCache::RunGarbageCollector() {
|
|
std::scoped_lock lock {m_lock};
|
|
const uint64_t tick = m_gc_tick++;
|
|
if (m_graphics.CanReportMemoryUsage()) {
|
|
m_total_used_memory = m_graphics.GetDeviceMemoryUsage();
|
|
}
|
|
if (m_total_used_memory < m_trigger_gc_memory) {
|
|
return;
|
|
}
|
|
const auto collect = [&](bool allow_aggressive) {
|
|
bool pressured = m_total_used_memory >= m_pressure_gc_memory;
|
|
bool aggressive = allow_aggressive && m_total_used_memory >= m_critical_gc_memory;
|
|
const uint64_t age = std::min<uint64_t>(aggressive ? 160 : pressured ? 80 : 16, tick);
|
|
size_t deletions = aggressive ? 40 : pressured ? 20 : 10;
|
|
std::vector<ImageId> candidates;
|
|
candidates.reserve(deletions);
|
|
// Deleting depth recursively deletes its stencil association, so finish LRU traversal
|
|
// first.
|
|
m_lru_cache.ForEachItemBelow(tick - age, [&](ImageId id) {
|
|
candidates.push_back(id);
|
|
return candidates.size() == deletions;
|
|
});
|
|
for (const auto id: candidates) {
|
|
if (deletions == 0) {
|
|
break;
|
|
}
|
|
--deletions;
|
|
auto owner = m_slot_images.try_get(id);
|
|
if (owner == nullptr || !owner->registered || owner->depth_id) {
|
|
continue;
|
|
}
|
|
if (owner->IsGpuModified()) {
|
|
const bool safe = SafeToDownload(*owner);
|
|
if (safe && owner->info.IsTiled()) {
|
|
continue;
|
|
}
|
|
if (safe && !pressured) {
|
|
continue;
|
|
}
|
|
if (safe && !DownloadImageMemory(id)) {
|
|
continue;
|
|
}
|
|
}
|
|
FreeImage(id);
|
|
if (m_total_used_memory < m_critical_gc_memory && aggressive) {
|
|
deletions >>= 2;
|
|
aggressive = false;
|
|
}
|
|
if (m_total_used_memory < m_pressure_gc_memory && pressured) {
|
|
deletions >>= 1;
|
|
pressured = false;
|
|
}
|
|
}
|
|
};
|
|
collect(false);
|
|
if (m_total_used_memory >= m_critical_gc_memory) {
|
|
collect(true);
|
|
}
|
|
}
|
|
|
|
void TextureCache::ProcessDownloadImages() {
|
|
std::scoped_lock lock {m_lock};
|
|
for (const auto id: m_download_images) {
|
|
const auto owner = m_slot_images.try_get(id);
|
|
if (owner != nullptr && owner->registered && owner->IsGpuModified()) {
|
|
(void)DownloadImageMemory(id);
|
|
}
|
|
}
|
|
m_download_images.clear();
|
|
}
|
|
|
|
} // namespace Libs::Graphics
|