mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-08-03 11:23:49 +00:00
3847 lines
178 KiB
C++
3847 lines
178 KiB
C++
#include "graphics/host_gpu/renderer/textureCache.h"
|
|
|
|
#include "common/assert.h"
|
|
#include "common/logging/log.h"
|
|
#include "common/profiler.h"
|
|
#include "graphics/guest_gpu/gpu_defs.h"
|
|
#include "graphics/guest_gpu/gpu_format.h"
|
|
#include "graphics/guest_gpu/graphicsRun.h"
|
|
#include "graphics/guest_gpu/tile.h"
|
|
#include "graphics/host_gpu/graphicContext.h"
|
|
#include "graphics/host_gpu/objects/label.h"
|
|
#include "graphics/host_gpu/objects/textureCommon.h"
|
|
#include "graphics/host_gpu/renderer/bufferCache.h"
|
|
#include "graphics/host_gpu/renderer/dummyTextureCache.h"
|
|
#include "graphics/host_gpu/renderer/image.h"
|
|
#include "graphics/host_gpu/renderer/imageView.h"
|
|
#include "graphics/host_gpu/renderer/render.h"
|
|
#include "graphics/host_gpu/renderer/renderContext.h"
|
|
#include "graphics/host_gpu/renderer/renderTargetBarriers.h"
|
|
#include "graphics/host_gpu/renderer/resourceMutex.h"
|
|
#include "graphics/host_gpu/transfer.h"
|
|
#include "graphics/host_gpu/vulkanCommon.h"
|
|
#include "kernel/memory.h"
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <bit>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <thread>
|
|
#include <xxhash.h>
|
|
|
|
namespace Libs::Graphics {
|
|
|
|
namespace {
|
|
|
|
thread_local const void* g_texture_cache_lock_owner = nullptr;
|
|
thread_local const void* g_texture_fault_owner = nullptr;
|
|
|
|
[[nodiscard]] bool GuestRangeIsZero(uint64_t address, uint64_t size) noexcept {
|
|
const auto* bytes = reinterpret_cast<const uint8_t*>(address);
|
|
while (size >= sizeof(uint64_t)) {
|
|
uint64_t word = 0;
|
|
std::memcpy(&word, bytes, sizeof(word));
|
|
if (word != 0) {
|
|
return false;
|
|
}
|
|
bytes += sizeof(word);
|
|
size -= sizeof(word);
|
|
}
|
|
while (size-- != 0) {
|
|
if (*bytes++ != 0) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
[[nodiscard]] uint64_t HashSampledImageEdges(const Image& image) {
|
|
constexpr uint64_t page_mask = TRACKER_PAGE_SIZE - 1;
|
|
const uint64_t begin = image.address;
|
|
const uint64_t end = image.address + image.size;
|
|
const uint64_t head_end = std::min(end, (begin + page_mask) & ~page_mask);
|
|
const uint64_t tail_begin = std::max(begin, end & ~page_mask);
|
|
const uint64_t head_size = head_end - begin;
|
|
const uint64_t tail_size = tail_begin < head_end ? end - head_end : end - tail_begin;
|
|
std::array<uint8_t, TRACKER_PAGE_SIZE * 2> bytes {};
|
|
if (head_size + tail_size > bytes.size()) {
|
|
EXIT("TextureCache: sampled-image edge hash range overflow\n");
|
|
}
|
|
if ((head_size != 0 && !LibKernel::Memory::TryReadBacking(begin, bytes.data(), head_size)) ||
|
|
(tail_size != 0 &&
|
|
!LibKernel::Memory::TryReadBacking(tail_begin < head_end ? head_end : tail_begin,
|
|
bytes.data() + head_size, tail_size))) {
|
|
EXIT("TextureCache: failed to hash sampled-image edge backing, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 "\n",
|
|
image.address, image.size);
|
|
}
|
|
return XXH3_64bits(bytes.data(), static_cast<size_t>(head_size + tail_size));
|
|
}
|
|
|
|
class FaultSafeTextureLock final {
|
|
public:
|
|
FaultSafeTextureLock(const void* owner, TrackingSpinLock& mutex): m_mutex(mutex) {
|
|
if (g_texture_cache_lock_owner != nullptr) {
|
|
EXIT("TextureCache: recursive cache lock acquisition, current=%p\n",
|
|
g_texture_cache_lock_owner);
|
|
}
|
|
g_texture_cache_lock_owner = owner;
|
|
m_mutex.lock();
|
|
}
|
|
~FaultSafeTextureLock() {
|
|
m_mutex.unlock();
|
|
g_texture_cache_lock_owner = nullptr;
|
|
}
|
|
|
|
private:
|
|
TrackingSpinLock& m_mutex;
|
|
};
|
|
|
|
enum class ImageRangeOverlap : uint8_t { None, PageOnly, Bytes };
|
|
|
|
ImageRangeOverlap ClassifyImageRangeOverlap(uint64_t left, uint64_t left_size, uint64_t right,
|
|
uint64_t right_size) {
|
|
if (ImageRangeOverlaps(left, left_size, right, right_size)) {
|
|
return ImageRangeOverlap::Bytes;
|
|
}
|
|
return ImagePageRangesOverlap(left, left_size, right, right_size) ? ImageRangeOverlap::PageOnly
|
|
: ImageRangeOverlap::None;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool IsExactRenderTargetMipStorage(const ImageInfo& sampled, const ImageInfo& storage,
|
|
vk::Format sampled_view_format,
|
|
vk::Format storage_image_format) noexcept {
|
|
const auto render_target = Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget);
|
|
const auto image_2d = Prospero::GpuEnumValue(Prospero::ImageType::kColor2D);
|
|
if (sampled.address == 0 || sampled.size == 0 || sampled.width == 0 || sampled.height == 0 ||
|
|
(sampled.address & 0xffffu) != 0 || sampled.levels <= 1 || sampled.levels > 16 ||
|
|
sampled.base_level != 0 || sampled.view_levels != sampled.levels ||
|
|
sampled.tile != render_target || sampled.depth != 1 || sampled.type != image_2d ||
|
|
sampled.base_array != 0 || storage.address == 0 || storage.size == 0 ||
|
|
(storage.address & 0xffffu) != 0 || storage.width == 0 || storage.height == 0 ||
|
|
storage.base_level != 0 || storage.levels != 1 || storage.view_levels != 1 ||
|
|
storage.tile != render_target || storage.depth != 1 || storage.type != image_2d ||
|
|
storage.base_array != 0 || sampled.format != storage.format ||
|
|
sampled_view_format != storage_image_format) {
|
|
return false;
|
|
}
|
|
const auto bytes_per_element = Prospero::NumBytesPerElement(sampled.format);
|
|
if (bytes_per_element == 0) {
|
|
return false;
|
|
}
|
|
TileSizeAlign sampled_layout {};
|
|
std::array<TileSizeOffset, 16> level_layouts {};
|
|
std::array<TilePaddedSize, 16> level_padded {};
|
|
if (!TileGetRenderTargetMipLayout(sampled.width, sampled.height, sampled.pitch,
|
|
bytes_per_element, sampled.levels, &sampled_layout,
|
|
level_layouts.data(), level_padded.data()) ||
|
|
sampled_layout.align != 65536 || sampled_layout.size != sampled.size) {
|
|
return false;
|
|
}
|
|
TileSizeAlign storage_layout {};
|
|
if (!TileGetRenderTargetSize(storage.width, storage.height, storage.pitch, bytes_per_element,
|
|
&storage_layout) ||
|
|
storage_layout.align != 65536 || storage_layout.size != storage.size) {
|
|
return false;
|
|
}
|
|
for (uint32_t level = 0; level < sampled.levels; level++) {
|
|
const uint64_t divisor = 1ull << level;
|
|
const auto level_width =
|
|
static_cast<uint32_t>((static_cast<uint64_t>(sampled.width) + divisor - 1) / divisor);
|
|
const auto level_height =
|
|
static_cast<uint32_t>((static_cast<uint64_t>(sampled.height) + divisor - 1) / divisor);
|
|
const auto& layout = level_layouts[level];
|
|
const bool dedicated_allocation = layout.size == layout.src_size &&
|
|
layout.offset == layout.src_offset && layout.x == 0 &&
|
|
layout.y == 0;
|
|
const auto& padded = level_padded[level];
|
|
const bool packed_tail_allocation =
|
|
layout.src_size == 65536 && !dedicated_allocation && padded.width != 0 &&
|
|
padded.height != 0 && storage.width == padded.width &&
|
|
storage.height == padded.height && storage.pitch == padded.width &&
|
|
storage.size == layout.src_size && storage_layout.size == layout.src_size &&
|
|
layout.src_offset <= sampled.size &&
|
|
layout.src_size <= sampled.size - layout.src_offset &&
|
|
sampled.address <= UINT64_MAX - layout.src_offset &&
|
|
storage.address == sampled.address + layout.src_offset;
|
|
if (packed_tail_allocation) {
|
|
// A PS5 mip tail is one physical thin-64 KiB tile. The storage descriptor exposes
|
|
// that complete padded block so its shader can address the packed mip locations.
|
|
return true;
|
|
}
|
|
if (!dedicated_allocation || storage.width != level_width ||
|
|
storage.height != level_height ||
|
|
storage.pitch != TileGetRenderTargetPitch(level_width, bytes_per_element) ||
|
|
storage.size != layout.size || storage_layout.size != layout.size ||
|
|
sampled.address > UINT64_MAX - layout.offset ||
|
|
storage.address != sampled.address + layout.offset) {
|
|
continue;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
struct TextureCache::CachedImage {
|
|
enum class Kind {
|
|
Texture,
|
|
StorageTexture,
|
|
RenderTarget,
|
|
DepthTarget,
|
|
VideoOut
|
|
} kind = Kind::Texture;
|
|
Image info;
|
|
RenderTargetInfo target;
|
|
DepthTargetInfo depth;
|
|
VideoOutInfo video_out;
|
|
GraphicContext* ctx = nullptr;
|
|
VulkanImage* image = nullptr;
|
|
bool gpu_modified = false;
|
|
bool buffer_modified = false;
|
|
bool stencil_initialized = false;
|
|
bool registered = false;
|
|
|
|
~CachedImage() {
|
|
if (ctx == nullptr || image == nullptr || registered) {
|
|
EXIT("TextureCache: cached image destroyed with invalid resources, ctx=%p image=%p "
|
|
"kind=%u registered=%d\n",
|
|
static_cast<const void*>(ctx), static_cast<const void*>(image),
|
|
static_cast<uint32_t>(kind), registered);
|
|
}
|
|
ImageOps::Destroy(ctx, image);
|
|
image = nullptr;
|
|
}
|
|
|
|
[[nodiscard]] uint32_t RangeCount() const {
|
|
return kind == Kind::DepthTarget && depth.stencil_address != 0 ? 2u : 1u;
|
|
}
|
|
[[nodiscard]] BufferImageBinding BufferBinding() const {
|
|
switch (kind) {
|
|
case Kind::Texture: return BufferImageBinding::Texture;
|
|
case Kind::RenderTarget: return BufferImageBinding::RenderTarget;
|
|
case Kind::VideoOut: return BufferImageBinding::VideoOut;
|
|
case Kind::StorageTexture: return BufferImageBinding::StorageTexture;
|
|
case Kind::DepthTarget: return BufferImageBinding::DepthTarget;
|
|
}
|
|
return BufferImageBinding::Unsupported;
|
|
}
|
|
[[nodiscard]] uint64_t Address(uint32_t index = 0) const {
|
|
if (index >= RangeCount()) {
|
|
EXIT("TextureCache: image address range index out of bounds, index=%u count=%u\n",
|
|
index, RangeCount());
|
|
}
|
|
if (index == 1) {
|
|
return depth.stencil_address;
|
|
}
|
|
switch (kind) {
|
|
case Kind::Texture: return info.address;
|
|
case Kind::StorageTexture: return info.address;
|
|
case Kind::RenderTarget: return target.address;
|
|
case Kind::DepthTarget: return depth.address;
|
|
case Kind::VideoOut: return video_out.address;
|
|
}
|
|
EXIT("TextureCache: unsupported cached image kind %u for address\n",
|
|
static_cast<uint32_t>(kind));
|
|
}
|
|
[[nodiscard]] uint64_t Size(uint32_t index = 0) const {
|
|
if (index >= RangeCount()) {
|
|
EXIT("TextureCache: image size range index out of bounds, index=%u count=%u\n", index,
|
|
RangeCount());
|
|
}
|
|
if (index == 1) {
|
|
return depth.stencil_size;
|
|
}
|
|
switch (kind) {
|
|
case Kind::Texture: return info.size;
|
|
case Kind::StorageTexture: return info.size;
|
|
case Kind::RenderTarget: return target.size;
|
|
case Kind::DepthTarget: return depth.size;
|
|
case Kind::VideoOut: return video_out.size;
|
|
}
|
|
EXIT("TextureCache: unsupported cached image kind %u for size\n",
|
|
static_cast<uint32_t>(kind));
|
|
}
|
|
[[nodiscard]] bool OverlapsRange(uint64_t address, uint64_t size, bool page) const {
|
|
for (uint32_t i = 0; i < RangeCount(); i++) {
|
|
if (page ? ImagePageRangesOverlap(address, size, Address(i), Size(i))
|
|
: ImageRangeOverlaps(address, size, Address(i), Size(i))) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
// A page-expanded region lookup identifies candidates before exact byte classification. Kyty's
|
|
// tracker retains GPU ownership per 4 KiB page, so an edge-page
|
|
// candidate must remain coherent even when the triggering byte lies just outside the image.
|
|
[[nodiscard]] bool IsGpuReadbackPageCandidate(uint64_t address, uint64_t size) const {
|
|
return gpu_modified && OverlapsRange(address, size, true);
|
|
}
|
|
[[nodiscard]] bool HasExactRange(uint64_t address, uint64_t size) const {
|
|
for (uint32_t i = 0; i < RangeCount(); i++) {
|
|
if (address == Address(i) && size == Size(i)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
void TextureCache::RegisterImageLocked(CachedImage& image) {
|
|
if (image.registered) {
|
|
EXIT("TextureCache: registering an already registered image\n");
|
|
}
|
|
std::vector<ImageOwnerIndex::ByteRange> ranges;
|
|
ranges.reserve(image.RangeCount());
|
|
for (uint32_t range = 0; range < image.RangeCount(); range++) {
|
|
ranges.push_back({image.Address(range), image.Size(range)});
|
|
}
|
|
if (!m_image_owner_index.Register(&image, ranges)) {
|
|
EXIT("TextureCache: invalid or duplicate image registration\n");
|
|
}
|
|
image.registered = true;
|
|
}
|
|
|
|
void TextureCache::UnregisterImageLocked(CachedImage& image, bool release_tracking) {
|
|
if (!image.registered) {
|
|
EXIT("TextureCache: unregistering an unregistered image\n");
|
|
}
|
|
std::vector<ImageOwnerIndex::ByteRange> final_releases;
|
|
if (!m_image_owner_index.Unregister(&image, final_releases)) {
|
|
EXIT("TextureCache: image missing from owner index\n");
|
|
}
|
|
if (release_tracking) {
|
|
for (const auto& release: final_releases) {
|
|
m_memory_tracker.UntrackMemory(release.address, release.size);
|
|
}
|
|
}
|
|
image.registered = false;
|
|
}
|
|
|
|
VulkanImage* TextureCache::PublishImage(CommandBuffer* command,
|
|
std::shared_ptr<CachedImage> image) {
|
|
if (command == nullptr || image == nullptr || image->image == nullptr || image->registered) {
|
|
EXIT("TextureCache: invalid image publication, command=%p record=%p image=%p "
|
|
"registered=%d\n",
|
|
static_cast<const void*>(command), static_cast<const void*>(image.get()),
|
|
image != nullptr ? static_cast<const void*>(image->image) : nullptr,
|
|
image != nullptr && image->registered);
|
|
}
|
|
auto result = image->image;
|
|
command->RetainResourceUntilFence(image);
|
|
m_images.push_back(std::move(image));
|
|
RegisterImageLocked(*m_images.back());
|
|
return result;
|
|
}
|
|
|
|
std::vector<TextureCache::CachedImage*>
|
|
TextureCache::FindImagesInRegionLocked(uint64_t vaddr, uint64_t size, bool page_overlap) {
|
|
return page_overlap ? m_image_owner_index.QueryCandidates(vaddr, size)
|
|
: m_image_owner_index.Query(vaddr, size);
|
|
}
|
|
|
|
struct TextureCache::ReadbackWorker {
|
|
enum class State : uint32_t {
|
|
Idle,
|
|
Claimed,
|
|
Requested,
|
|
Ready,
|
|
Installed,
|
|
Completed,
|
|
Stopping,
|
|
Stopped
|
|
};
|
|
struct ReadbackRange {
|
|
uint64_t address;
|
|
uint64_t size;
|
|
};
|
|
struct ReadbackTransfer {
|
|
std::array<ReadbackRange, 2> ranges {};
|
|
uint32_t count = 0;
|
|
|
|
void Add(uint64_t address, uint64_t size) {
|
|
if (address == 0 || size == 0 || count == ranges.size()) {
|
|
EXIT("TextureCache: invalid image readback transfer range\n");
|
|
}
|
|
ranges[count++] = {address, size};
|
|
}
|
|
[[nodiscard]] std::span<const ReadbackRange> Ranges() const {
|
|
return {ranges.data(), count};
|
|
}
|
|
};
|
|
static_assert(std::atomic<State>::is_always_lock_free);
|
|
|
|
explicit ReadbackWorker(TextureCache& owner): cache(owner), thread([this] { Run(); }) {}
|
|
|
|
~ReadbackWorker() {
|
|
auto expected = State::Idle;
|
|
if (!state.compare_exchange_strong(expected, State::Stopping, std::memory_order_acq_rel)) {
|
|
EXIT("TextureCache: cannot stop readback worker from state %u\n",
|
|
static_cast<uint32_t>(expected));
|
|
}
|
|
state.notify_all();
|
|
thread.join();
|
|
if (state.load(std::memory_order_acquire) != State::Stopped) {
|
|
EXIT("TextureCache: readback worker did not reach stopped state\n");
|
|
}
|
|
}
|
|
|
|
void Request(PageFaultAccess fault_access, uint64_t fault_vaddr, uint64_t fault_size) noexcept {
|
|
const bool command_thread = GraphicsRunIsCommandProcessorThread();
|
|
const bool submissions_prepaused_now = GraphicsRunSubmissionLockHeld() || command_thread;
|
|
const bool unsafe_gpu_lock = GraphicsRunGpuLockHeld() && !submissions_prepaused_now;
|
|
if ((fault_access != PageFaultAccess::Read && fault_access != PageFaultAccess::Write) ||
|
|
unsafe_gpu_lock || LabelInCallback() || g_texture_cache_lock_owner != nullptr ||
|
|
g_texture_fault_owner != &cache) {
|
|
EXIT("TextureCache: unsafe image readback request, access=%u command_thread=%d "
|
|
"submission_lock=%d gpu_lock=%d label_callback=%d cache_lock=%p fault_owner=%p\n",
|
|
static_cast<uint32_t>(fault_access), command_thread,
|
|
GraphicsRunSubmissionLockHeld(), GraphicsRunGpuLockHeld(), LabelInCallback(),
|
|
g_texture_cache_lock_owner, g_texture_fault_owner);
|
|
}
|
|
State expected = State::Idle;
|
|
while (!state.compare_exchange_weak(expected, State::Claimed, std::memory_order_acq_rel)) {
|
|
if (expected == State::Stopping || expected == State::Stopped) {
|
|
EXIT("TextureCache: image readback requested while worker is stopping, state=%u\n",
|
|
static_cast<uint32_t>(expected));
|
|
}
|
|
state.wait(expected, std::memory_order_acquire);
|
|
expected = State::Idle;
|
|
}
|
|
access = fault_access;
|
|
vaddr = fault_vaddr;
|
|
size = fault_size;
|
|
submissions_prepaused = submissions_prepaused_now;
|
|
state.store(State::Requested, std::memory_order_release);
|
|
state.notify_all();
|
|
while (true) {
|
|
const auto current = state.load(std::memory_order_acquire);
|
|
if (current == State::Ready) {
|
|
return;
|
|
}
|
|
if (current != State::Requested) {
|
|
EXIT("TextureCache: invalid state while waiting for image readback, state=%u\n",
|
|
static_cast<uint32_t>(current));
|
|
}
|
|
state.wait(current, std::memory_order_acquire);
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] bool Complete(PageFaultAccess fault_access, uint64_t fault_vaddr,
|
|
uint64_t fault_size) noexcept {
|
|
const auto current = state.load(std::memory_order_acquire);
|
|
if (current == State::Idle || current == State::Stopping || current == State::Stopped) {
|
|
return false;
|
|
}
|
|
if (current != State::Ready) {
|
|
EXIT("TextureCache: active image readback has invalid completion state %u\n",
|
|
static_cast<uint32_t>(current));
|
|
}
|
|
if (access != fault_access || vaddr != fault_vaddr || size != fault_size) {
|
|
EXIT("TextureCache: mismatched active image readback completion\n");
|
|
}
|
|
state.store(State::Installed, std::memory_order_release);
|
|
state.notify_all();
|
|
return true;
|
|
}
|
|
|
|
[[nodiscard]] bool IsReady(PageFaultAccess fault_access, uint64_t fault_vaddr,
|
|
uint64_t fault_size) const noexcept {
|
|
return state.load(std::memory_order_acquire) == State::Ready && access == fault_access &&
|
|
vaddr == fault_vaddr && size == fault_size;
|
|
}
|
|
|
|
void Release(PageFaultAccess fault_access, uint64_t fault_vaddr, uint64_t fault_size) noexcept {
|
|
const auto current = state.load(std::memory_order_acquire);
|
|
if (current == State::Idle || current == State::Stopping || current == State::Stopped) {
|
|
return;
|
|
}
|
|
if (current != State::Installed) {
|
|
EXIT("TextureCache: active image readback has invalid release state %u\n",
|
|
static_cast<uint32_t>(current));
|
|
}
|
|
if (access != fault_access || vaddr != fault_vaddr || size != fault_size) {
|
|
EXIT("TextureCache: mismatched active image readback release\n");
|
|
}
|
|
state.store(State::Completed, std::memory_order_release);
|
|
state.notify_all();
|
|
while (true) {
|
|
const auto current = state.load(std::memory_order_acquire);
|
|
if (current == State::Idle) {
|
|
break;
|
|
}
|
|
if (current != State::Completed) {
|
|
EXIT("TextureCache: invalid state while releasing image readback, state=%u\n",
|
|
static_cast<uint32_t>(current));
|
|
}
|
|
state.wait(current, std::memory_order_acquire);
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] ReadbackTransfer DownloadDepthTarget(CachedImage& cached,
|
|
bool require_fault_range = true) {
|
|
const auto& info = cached.depth;
|
|
if (info.samples != 1 || cached.image->samples != 1) {
|
|
EXIT("TextureCache: multisampled depth-image readback is unsupported, samples=%u/%u\n",
|
|
info.samples, cached.image->samples);
|
|
}
|
|
const bool has_stencil = info.stencil_address != 0 || info.stencil_size != 0;
|
|
const bool has_htile = info.htile_address != 0 || info.htile_size != 0;
|
|
const bool fault_in_depth = vaddr >= info.address && vaddr < info.address + info.size &&
|
|
size <= info.address + info.size - vaddr;
|
|
const bool fault_in_stencil = has_stencil && vaddr >= info.stencil_address &&
|
|
vaddr < info.stencil_address + info.stencil_size &&
|
|
size <= info.stencil_address + info.stencil_size - vaddr;
|
|
const bool d16 =
|
|
info.guest_format == Prospero::GpuEnumValue(Prospero::BufferFormat::k16UNorm) &&
|
|
info.bytes_per_element == 2;
|
|
const bool d32 =
|
|
info.guest_format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32Float) &&
|
|
info.bytes_per_element == 4;
|
|
TileSizeAlign expected_stencil {};
|
|
TileSizeAlign expected_htile {};
|
|
TileSizeAlign expected_depth {};
|
|
const bool prospero_layout =
|
|
(d16 || d32) && IsSupportedDepthReadbackFormat(info) &&
|
|
TileGetDepthSize(info.width, info.height, 0,
|
|
Prospero::GpuEnumValue(d16 ? Prospero::DepthFormat::kZ16
|
|
: Prospero::DepthFormat::kZ32F),
|
|
Prospero::GpuEnumValue(has_stencil
|
|
? Prospero::StencilFormat::k8UInt
|
|
: Prospero::StencilFormat::kInvalid),
|
|
has_htile, &expected_stencil, &expected_htile, &expected_depth);
|
|
const auto expected_pitch =
|
|
prospero_layout
|
|
? TileGetTexturePitch(info.guest_format, info.width, 1,
|
|
Prospero::GpuEnumValue(Prospero::TileMode::kDepth))
|
|
: 0u;
|
|
const auto expected_stencil_pitch =
|
|
prospero_layout && has_stencil
|
|
? TileGetTexturePitch(Prospero::GpuEnumValue(Prospero::BufferFormat::k8UInt),
|
|
info.width, 1,
|
|
Prospero::GpuEnumValue(Prospero::TileMode::kDepth))
|
|
: 0u;
|
|
const bool layered_sizes =
|
|
info.layers != 0 && info.size <= UINT32_MAX &&
|
|
expected_depth.size <= UINT64_MAX / info.layers &&
|
|
info.size == expected_depth.size * info.layers &&
|
|
(!has_stencil || (info.stencil_size <= UINT32_MAX &&
|
|
expected_stencil.size <= UINT64_MAX / info.layers &&
|
|
info.stencil_size == expected_stencil.size * info.layers)) &&
|
|
(!has_htile || (expected_htile.size <= UINT64_MAX / info.layers &&
|
|
info.htile_size == expected_htile.size * info.layers));
|
|
if ((require_fault_range && !fault_in_depth && !fault_in_stencil) || info.address == 0 ||
|
|
info.size == 0 || info.width == 0 || info.height == 0 || info.pitch < info.width ||
|
|
info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kDepth) ||
|
|
!prospero_layout || info.pitch != expected_pitch || !layered_sizes ||
|
|
expected_depth.align != 65536u || cached.image->layers != info.layers ||
|
|
(has_stencil && (info.stencil_address == 0 || expected_stencil_pitch < info.width ||
|
|
expected_stencil.align != 65536u)) ||
|
|
(has_htile && expected_htile.align != 32768u)) {
|
|
EXIT("TextureCache: unsupported depth-image readback layout, fault=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " depth=0x%016" PRIx64 "+0x%016" PRIx64 " stencil=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " htile=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" extent=%ux%u pitch=%u/%u layers=%u/%u tile=%u format=%d guest_format=%u bpe=%u "
|
|
"expected_depth=0x%016" PRIx64 " expected_stencil=0x%016" PRIx64
|
|
" expected_htile=0x%016" PRIx64 "\n",
|
|
vaddr, size, info.address, info.size, info.stencil_address, info.stencil_size,
|
|
info.htile_address, info.htile_size, info.width, info.height, info.pitch,
|
|
expected_pitch, info.layers, cached.image->layers, info.tile_mode,
|
|
static_cast<int>(info.format), info.guest_format, info.bytes_per_element,
|
|
expected_depth.size, expected_stencil.size, expected_htile.size);
|
|
}
|
|
const auto rows = static_cast<uint64_t>(info.height - 1);
|
|
if (rows > (UINT64_MAX - info.width) / info.pitch ||
|
|
(has_stencil && rows > (UINT64_MAX - info.width) / expected_stencil_pitch)) {
|
|
EXIT("TextureCache: depth-image readback size overflow\n");
|
|
}
|
|
const auto depth_linear_elements = rows * info.pitch + info.width;
|
|
const auto stencil_linear_elements = rows * expected_stencil_pitch + info.width;
|
|
if (depth_linear_elements > UINT64_MAX / info.bytes_per_element) {
|
|
EXIT("TextureCache: depth-image readback size overflow\n");
|
|
}
|
|
const auto depth_linear_size = depth_linear_elements * info.bytes_per_element;
|
|
const auto depth_slice_size = info.size / info.layers;
|
|
const auto stencil_slice_size = has_stencil ? info.stencil_size / info.layers : 0;
|
|
const bool meta_overlap =
|
|
cache.HasMetaOverlapLocked(info.address, info.size) ||
|
|
(has_stencil && cache.HasMetaOverlapLocked(info.stencil_address, info.stencil_size));
|
|
const bool buffer_overlap = cache.m_buffer_cache.HasPageOverlap(info.address, info.size) ||
|
|
(has_stencil && cache.m_buffer_cache.HasPageOverlap(
|
|
info.stencil_address, info.stencil_size));
|
|
const auto transfer_size = info.size + info.stencil_size;
|
|
if (depth_linear_size > depth_slice_size ||
|
|
(has_stencil && stencil_linear_elements > stencil_slice_size) ||
|
|
transfer_size > UINT32_MAX || cached.image->format != info.format ||
|
|
cached.image->extent.width != info.width ||
|
|
cached.image->extent.height != info.height || meta_overlap || buffer_overlap) {
|
|
EXIT("TextureCache: depth-image readback storage is unsupported, "
|
|
"depth=0x%016" PRIx64 "+0x%016" PRIx64 " linear=0x%016" PRIx64
|
|
" format=%d/%d extent=%ux%u/%ux%u meta=%d buffer=%d\n",
|
|
info.address, info.size, depth_linear_size, static_cast<int>(cached.image->format),
|
|
static_cast<int>(info.format), cached.image->extent.width,
|
|
cached.image->extent.height, info.width, info.height, meta_overlap,
|
|
buffer_overlap);
|
|
}
|
|
download.resize(transfer_size);
|
|
auto regions = Transfer::MakeLayeredImageBufferCopies(info.layers, depth_slice_size,
|
|
info.pitch, info.width, info.height,
|
|
vk::ImageAspectFlagBits::eDepth);
|
|
if (has_stencil) {
|
|
auto stencil_regions = Transfer::MakeLayeredImageBufferCopies(
|
|
info.layers, stencil_slice_size, expected_stencil_pitch, info.width, info.height,
|
|
vk::ImageAspectFlagBits::eStencil);
|
|
for (auto& region: stencil_regions) {
|
|
region.offset += static_cast<uint32_t>(info.size);
|
|
}
|
|
regions.insert(regions.end(), stencil_regions.begin(), stencil_regions.end());
|
|
}
|
|
Transfer::DownloadImage(cached.ctx, download.data(), transfer_size, regions, cached.image,
|
|
cached.image->layout);
|
|
guest.resize(info.size);
|
|
cache.m_tiler.TileImage(guest.data(), download.data(), info);
|
|
Libs::LibKernel::Memory::WriteBacking(info.address, guest.data(), info.size);
|
|
ReadbackTransfer transfer;
|
|
transfer.Add(info.address, info.size);
|
|
if (has_stencil) {
|
|
guest.resize(info.stencil_size);
|
|
cache.m_tiler.TileStencil(guest.data(), download.data() + info.size, info);
|
|
Libs::LibKernel::Memory::WriteBacking(info.stencil_address, guest.data(),
|
|
info.stencil_size);
|
|
transfer.Add(info.stencil_address, info.stencil_size);
|
|
}
|
|
return transfer;
|
|
}
|
|
|
|
[[nodiscard]] ReadbackTransfer DownloadColorImage(CachedImage& cached) {
|
|
const bool storage = cached.kind == CachedImage::Kind::StorageTexture;
|
|
const bool target = cached.kind == CachedImage::Kind::RenderTarget;
|
|
const auto info =
|
|
storage ? MakeColorImageTransferInfo(cached.info, VulkanFormat(cached.info.format),
|
|
Prospero::NumBytesPerElement(cached.info.format))
|
|
: MakeColorImageTransferInfo(cached.target);
|
|
const bool linear = info.tile_mode == Prospero::GpuEnumValue(Prospero::TileMode::kLinear);
|
|
const bool tiled_target = target && IsTiledRenderTarget(cached.target);
|
|
const bool tiled_storage =
|
|
storage && info.tile_mode == Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget);
|
|
bool single_layer_storage = false;
|
|
if (storage) {
|
|
switch (static_cast<Prospero::ImageType>(cached.info.type)) {
|
|
case Prospero::ImageType::kColor2D:
|
|
case Prospero::ImageType::kColor2DArray:
|
|
single_layer_storage = cached.info.depth == 1;
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
const bool basic_storage =
|
|
!storage ||
|
|
(single_layer_storage && cached.info.base_level == 0 && cached.info.levels == 1 &&
|
|
cached.info.base_array == 0 && (linear || tiled_storage));
|
|
const auto layers = target ? cached.target.layers : 1u;
|
|
TileSizeAlign target_mip_layout {};
|
|
const bool target_mip_chain =
|
|
target && info.levels > 1 && layers == 1 && tiled_target &&
|
|
TileGetRenderTargetMipLayout(info.width, info.height, info.pitch,
|
|
info.bytes_per_element, info.levels, &target_mip_layout,
|
|
nullptr, nullptr) &&
|
|
target_mip_layout.align == 65536 && target_mip_layout.size == info.size &&
|
|
cached.image != nullptr && cached.image->format == info.format &&
|
|
cached.image->extent.width == info.width &&
|
|
cached.image->extent.height == info.height && cached.image->layers == 1 &&
|
|
cached.image->mip_levels == info.levels && cached.image->samples == 1;
|
|
if (info.samples != 1 || (!linear && !tiled_target && !tiled_storage) || !basic_storage ||
|
|
(info.levels != 1 && !target_mip_chain) || layers == 0 || info.size > UINT32_MAX) {
|
|
EXIT("TextureCache: unsupported color-image readback layout, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64
|
|
" extent=%ux%u pitch=%u bpe=%u levels=%u samples=%u tile=%u kind=%u\n",
|
|
info.address, info.size, info.width, info.height, info.pitch,
|
|
info.bytes_per_element, info.levels, info.samples, info.tile_mode,
|
|
static_cast<uint32_t>(cached.kind));
|
|
}
|
|
const auto slice_size = info.size / layers;
|
|
const bool meta_overlap = cache.HasMetaOverlapLocked(info.address, info.size);
|
|
const bool buffer_overlap = cache.m_buffer_cache.HasPageOverlap(info.address, info.size);
|
|
if (meta_overlap || buffer_overlap) {
|
|
EXIT("TextureCache: color-image readback storage is unsupported, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " meta=%d buffer=%d kind=%u\n",
|
|
info.address, info.size, meta_overlap, buffer_overlap,
|
|
static_cast<uint32_t>(cached.kind));
|
|
}
|
|
download.resize(info.size);
|
|
std::fill(download.begin(), download.end(), 0);
|
|
std::vector<ImageBufferCopy> regions;
|
|
if (target_mip_chain) {
|
|
const auto format = ImageOps::RenderTargetTransferFormat(info.bytes_per_element);
|
|
auto layout = TextureCalcUploadLayout(format, info.width, info.height, info.levels, 1,
|
|
info.pitch, info.tile_mode, info.size, false,
|
|
false, false, "RenderTargetReadback");
|
|
if (!layout.fmt_tiled_render_target || layout.pitch != info.pitch) {
|
|
EXIT("TextureCache: inconsistent render-target readback layout, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " pitch=%u/%u levels=%u\n",
|
|
info.address, info.size, info.pitch, layout.pitch, info.levels);
|
|
}
|
|
const auto uploads = TextureBuildUploadRegions(
|
|
layout, info.format, info.width, info.height, 1, info.levels, true, false,
|
|
TextureUploadDestination::MipLevels, TextureUploadSliceLayout::MipChainPerSlice);
|
|
regions.reserve(uploads.size());
|
|
for (const auto& upload: uploads) {
|
|
regions.push_back({upload.offset, upload.pitch, upload.dst_level, upload.width,
|
|
upload.height, upload.copy_height, upload.dst_layer,
|
|
upload.dst_x, upload.dst_y, upload.dst_z, upload.aspect});
|
|
}
|
|
} else {
|
|
regions = Transfer::MakeLayeredImageBufferCopies(layers, slice_size, info.pitch,
|
|
info.width, info.height);
|
|
}
|
|
Transfer::DownloadImage(cached.ctx, download.data(), info.size, regions, cached.image,
|
|
cached.image->layout);
|
|
if (target_mip_chain) {
|
|
guest.resize(info.size);
|
|
ImageInfo layout {};
|
|
layout.address = info.address;
|
|
layout.size = info.size;
|
|
layout.format = ImageOps::RenderTargetTransferFormat(info.bytes_per_element);
|
|
layout.width = info.width;
|
|
layout.height = info.height;
|
|
layout.pitch = info.pitch;
|
|
layout.levels = info.levels;
|
|
layout.view_levels = info.levels;
|
|
layout.tile = info.tile_mode;
|
|
layout.depth = 1;
|
|
layout.type = Prospero::GpuEnumValue(Prospero::ImageType::kColor2D);
|
|
cache.m_tiler.TileImage(guest.data(), download.data(), layout);
|
|
Libs::LibKernel::Memory::WriteBacking(info.address, guest.data(), info.size);
|
|
} else if (tiled_target || tiled_storage) {
|
|
guest.resize(info.size);
|
|
const RenderTargetInfo layout =
|
|
target ? cached.target : RenderTargetInfo {info.address,
|
|
info.size,
|
|
info.format,
|
|
info.width,
|
|
info.height,
|
|
info.pitch,
|
|
info.bytes_per_element,
|
|
info.tile_mode,
|
|
info.levels,
|
|
1};
|
|
cache.m_tiler.TileImage(guest.data(), download.data(), layout);
|
|
Libs::LibKernel::Memory::WriteBacking(info.address, guest.data(), info.size);
|
|
} else {
|
|
Libs::LibKernel::Memory::WriteBacking(info.address, download.data(), info.size);
|
|
}
|
|
ReadbackTransfer transfer;
|
|
transfer.Add(info.address, info.size);
|
|
return transfer;
|
|
}
|
|
|
|
void Run() noexcept {
|
|
while (true) {
|
|
auto current = state.load(std::memory_order_acquire);
|
|
while (current == State::Idle) {
|
|
state.wait(current, std::memory_order_acquire);
|
|
current = state.load(std::memory_order_acquire);
|
|
}
|
|
if (current == State::Stopping) {
|
|
state.store(State::Stopped, std::memory_order_release);
|
|
state.notify_all();
|
|
return;
|
|
}
|
|
if (current != State::Requested) {
|
|
EXIT("TextureCache: readback worker received unsupported state %u\n",
|
|
static_cast<uint32_t>(current));
|
|
}
|
|
|
|
std::optional<GraphicsRunSubmissionLock> submissions;
|
|
if (!submissions_prepaused) {
|
|
submissions.emplace();
|
|
}
|
|
FaultSafeTextureLock lock(&cache, cache.m_lock);
|
|
CachedImage* selected = cache.FindGpuReadbackPageCandidateLocked(vaddr, size);
|
|
const bool render_target =
|
|
selected != nullptr && selected->kind == CachedImage::Kind::RenderTarget;
|
|
const bool storage_texture =
|
|
selected != nullptr && selected->kind == CachedImage::Kind::StorageTexture;
|
|
const bool depth_target =
|
|
selected != nullptr && selected->kind == CachedImage::Kind::DepthTarget;
|
|
if ((!render_target && !storage_texture && !depth_target) || !selected->gpu_modified ||
|
|
selected->buffer_modified || selected->ctx == nullptr ||
|
|
selected->image == nullptr) {
|
|
EXIT("TextureCache: unsupported GPU image readback owner, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " access=%u image=%p kind=%u gpu_modified=%d "
|
|
"buffer_modified=%d ctx=%p vulkan_image=%p\n",
|
|
vaddr, size, static_cast<uint32_t>(access), static_cast<const void*>(selected),
|
|
selected != nullptr ? static_cast<uint32_t>(selected->kind) : UINT32_MAX,
|
|
selected != nullptr && selected->gpu_modified,
|
|
selected != nullptr && selected->buffer_modified,
|
|
selected != nullptr ? static_cast<const void*>(selected->ctx) : nullptr,
|
|
selected != nullptr ? static_cast<const void*>(selected->image) : nullptr);
|
|
}
|
|
|
|
const auto transfer =
|
|
depth_target ? DownloadDepthTarget(*selected) : DownloadColorImage(*selected);
|
|
|
|
if (!cache.m_memory_tracker.IsRegionGpuModified(vaddr, size)) {
|
|
EXIT("TextureCache: readback fault page is not GPU-modified, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 "\n",
|
|
vaddr, size);
|
|
}
|
|
const auto fault_page = vaddr & ~(TRACKER_PAGE_SIZE - 1);
|
|
const auto page_end = fault_page + TRACKER_PAGE_SIZE;
|
|
for (const auto& range: transfer.Ranges()) {
|
|
const auto range_end = range.address + range.size;
|
|
if (fault_page < range_end && page_end > range.address) {
|
|
if (fault_page > range.address) {
|
|
cache.m_memory_tracker.ForEachDownloadRange<true>(
|
|
range.address, fault_page - range.address,
|
|
[](uint64_t, uint64_t) noexcept {});
|
|
}
|
|
if (page_end < range_end) {
|
|
cache.m_memory_tracker.ForEachDownloadRange<true>(
|
|
page_end, range_end - page_end, [](uint64_t, uint64_t) noexcept {});
|
|
}
|
|
} else {
|
|
cache.m_memory_tracker.ForEachDownloadRange<true>(
|
|
range.address, range.size, [](uint64_t, uint64_t) noexcept {});
|
|
}
|
|
}
|
|
|
|
state.store(State::Ready, std::memory_order_release);
|
|
state.notify_all();
|
|
while ((current = state.load(std::memory_order_acquire)) != State::Completed) {
|
|
if (current != State::Ready && current != State::Installed) {
|
|
EXIT("TextureCache: invalid image readback completion state %u\n",
|
|
static_cast<uint32_t>(current));
|
|
}
|
|
state.wait(current, std::memory_order_acquire);
|
|
}
|
|
for (const auto& range: transfer.Ranges()) {
|
|
if (cache.m_memory_tracker.IsRegionGpuModified(range.address, range.size)) {
|
|
EXIT("TextureCache: completed image readback retained GPU ownership, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
|
|
range.address, range.size);
|
|
}
|
|
}
|
|
selected->gpu_modified = false;
|
|
submissions_prepaused = false;
|
|
state.store(State::Idle, std::memory_order_release);
|
|
state.notify_all();
|
|
}
|
|
}
|
|
|
|
TextureCache& cache;
|
|
std::atomic<State> state {State::Idle};
|
|
PageFaultAccess access = PageFaultAccess::Unknown;
|
|
uint64_t vaddr = 0;
|
|
uint64_t size = 0;
|
|
bool submissions_prepaused = false;
|
|
std::vector<uint8_t> download;
|
|
std::vector<uint8_t> guest;
|
|
std::thread thread;
|
|
};
|
|
|
|
void TextureCache::RequireRetirementIsolation(const std::vector<CachedImage*>& retire,
|
|
const char* operation, uint64_t address,
|
|
uint64_t size) const {
|
|
if (retire.empty()) {
|
|
return;
|
|
}
|
|
std::vector<ImageRetirementRange> ranges;
|
|
ranges.reserve(m_images.size() * 2);
|
|
for (const auto& cached: m_images) {
|
|
const bool retiring = std::find(retire.begin(), retire.end(), cached.get()) != retire.end();
|
|
for (uint32_t range = 0; range < cached->RangeCount(); range++) {
|
|
ranges.push_back({cached->Address(range), cached->Size(range), retiring});
|
|
}
|
|
}
|
|
const auto conflict = FindImageRetirementConflict(ranges);
|
|
if (conflict.Exists()) {
|
|
const auto& retired = ranges[conflict.retired];
|
|
const auto& retained = ranges[conflict.retained];
|
|
EXIT("TextureCache: %s retirement leaves a tracked page alias, request=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " retired=0x%016" PRIx64 "+0x%016" PRIx64 " retained=0x%016" PRIx64
|
|
"+0x%016" PRIx64 "\n",
|
|
operation, address, size, retired.address, retired.size, retained.address,
|
|
retained.size);
|
|
}
|
|
}
|
|
|
|
void TextureCache::RetireImages(const std::vector<CachedImage*>& retire,
|
|
const CachedImage* native_image_source) {
|
|
if (retire.empty()) {
|
|
return;
|
|
}
|
|
// FindTexture and FindRenderTarget retain cache records on their recording commands. Erasing
|
|
// the lookup owner therefore defers Vulkan destruction until the last referencing fence.
|
|
size_t removed = 0;
|
|
bool native_image_retired = false;
|
|
for (auto it = m_images.begin(); it != m_images.end();) {
|
|
if (std::find(retire.begin(), retire.end(), it->get()) == retire.end()) {
|
|
++it;
|
|
continue;
|
|
}
|
|
const bool sampled = (*it)->kind == CachedImage::Kind::Texture;
|
|
const bool storage = (*it)->kind == CachedImage::Kind::StorageTexture;
|
|
const bool target = (*it)->kind == CachedImage::Kind::RenderTarget ||
|
|
(*it)->kind == CachedImage::Kind::DepthTarget;
|
|
const bool native_image = it->get() == native_image_source;
|
|
if (native_image) {
|
|
bool source_valid = (*it)->gpu_modified && !(*it)->buffer_modified;
|
|
switch ((*it)->kind) {
|
|
case CachedImage::Kind::StorageTexture:
|
|
source_valid = source_valid && !(*it)->info.IsCpuDirty();
|
|
break;
|
|
case CachedImage::Kind::RenderTarget:
|
|
case CachedImage::Kind::DepthTarget: break;
|
|
case CachedImage::Kind::Texture:
|
|
case CachedImage::Kind::VideoOut: source_valid = false; break;
|
|
}
|
|
for (uint32_t range = 0; source_valid && range < (*it)->RangeCount(); range++) {
|
|
source_valid =
|
|
m_memory_tracker.IsRegionGpuModified((*it)->Address(range), (*it)->Size(range));
|
|
}
|
|
if (!source_valid) {
|
|
EXIT("TextureCache: invalid native image retirement, kind=%u gpu_modified=%d "
|
|
"buffer_modified=%d cpu_dirty=%d\n",
|
|
static_cast<uint32_t>((*it)->kind), (*it)->gpu_modified,
|
|
(*it)->buffer_modified,
|
|
(*it)->kind == CachedImage::Kind::StorageTexture && (*it)->info.IsCpuDirty());
|
|
}
|
|
(*it)->gpu_modified = false;
|
|
native_image_retired = true;
|
|
}
|
|
if ((!sampled && !storage && !target && !native_image) || (*it)->gpu_modified ||
|
|
(storage && ((*it)->buffer_modified || (*it)->info.IsCpuDirty())) ||
|
|
(target && (*it)->buffer_modified)) {
|
|
EXIT("TextureCache: invalid image retirement, kind=%u gpu_modified=%d "
|
|
"buffer_modified=%d\n",
|
|
static_cast<uint32_t>((*it)->kind), (*it)->gpu_modified, (*it)->buffer_modified);
|
|
}
|
|
for (uint32_t range = 0; range < (*it)->RangeCount(); range++) {
|
|
if (target && !native_image &&
|
|
m_memory_tracker.IsRegionGpuModified((*it)->Address(range), (*it)->Size(range))) {
|
|
EXIT("TextureCache: clean target retirement retained tracker GPU ownership, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
|
|
(*it)->Address(range), (*it)->Size(range));
|
|
}
|
|
}
|
|
// A native transition keeps tracker ownership continuous until the replacement is
|
|
// published. Normal retirement only releases 4 KiB pages with no remaining image owner.
|
|
UnregisterImageLocked(**it, !native_image);
|
|
it = m_images.erase(it);
|
|
removed++;
|
|
}
|
|
if (removed != retire.size()) {
|
|
EXIT("TextureCache: image retirement set mismatch, requested=%zu removed=%zu\n",
|
|
retire.size(), removed);
|
|
}
|
|
if (native_image_source != nullptr && !native_image_retired) {
|
|
EXIT("TextureCache: native image retirement source was not retired\n");
|
|
}
|
|
}
|
|
|
|
void TextureCache::RetireDepthMetadataLocked(const std::vector<CachedImage*>& retire,
|
|
uint64_t preserve_address) {
|
|
std::vector<uint64_t> addresses;
|
|
for (auto* cached: retire) {
|
|
if (cached->kind != CachedImage::Kind::DepthTarget || cached->depth.htile_address == 0) {
|
|
continue;
|
|
}
|
|
const bool retained_owner =
|
|
std::any_of(m_images.begin(), m_images.end(), [&](const auto& other) {
|
|
return std::find(retire.begin(), retire.end(), other.get()) == retire.end() &&
|
|
other->kind == CachedImage::Kind::DepthTarget &&
|
|
other->depth.htile_address == cached->depth.htile_address &&
|
|
other->depth.htile_size == cached->depth.htile_size;
|
|
});
|
|
if (!retained_owner && cached->depth.htile_address != preserve_address &&
|
|
std::find(addresses.begin(), addresses.end(), cached->depth.htile_address) ==
|
|
addresses.end()) {
|
|
addresses.push_back(cached->depth.htile_address);
|
|
}
|
|
}
|
|
for (const auto address: addresses) {
|
|
auto metadata = m_surface_metas.find(address);
|
|
auto owner = std::find_if(retire.begin(), retire.end(), [&](const auto* cached) {
|
|
return cached->kind == CachedImage::Kind::DepthTarget &&
|
|
cached->depth.htile_address == address;
|
|
});
|
|
if (metadata == m_surface_metas.end() || owner == retire.end() ||
|
|
metadata->second.size != (*owner)->depth.htile_size) {
|
|
EXIT("TextureCache: retiring depth target has invalid HTile metadata, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
|
|
address, owner != retire.end() ? (*owner)->depth.htile_size : 0);
|
|
}
|
|
if (metadata->second.gpu_modified) {
|
|
m_metadata_tracker.ForEachDownloadRange<true>(metadata->first, metadata->second.size,
|
|
[](uint64_t, uint64_t) noexcept {});
|
|
}
|
|
m_metadata_tracker.UntrackMemory(metadata->first, metadata->second.size);
|
|
m_surface_metas.erase(metadata);
|
|
}
|
|
}
|
|
|
|
void TextureCache::MaterializeImagesToGuestLocked(
|
|
GraphicContext* ctx, const std::vector<std::shared_ptr<CachedImage>>& images) {
|
|
if (std::any_of(images.begin(), images.end(),
|
|
[](const auto& cached) { return cached->gpu_modified; })) {
|
|
Transfer::WaitForGraphicsIdle(ctx);
|
|
}
|
|
for (const auto& cached: images) {
|
|
if (!cached->gpu_modified) {
|
|
continue;
|
|
}
|
|
if (cached->ctx != ctx || cached->buffer_modified) {
|
|
EXIT("TextureCache: image recreation has invalid ownership, kind=%u ctx=%p/%p "
|
|
"buffer_modified=%d\n",
|
|
static_cast<uint32_t>(cached->kind), static_cast<const void*>(cached->ctx),
|
|
static_cast<const void*>(ctx), cached->buffer_modified);
|
|
}
|
|
if (cached->kind != CachedImage::Kind::DepthTarget &&
|
|
cached->kind != CachedImage::Kind::RenderTarget &&
|
|
cached->kind != CachedImage::Kind::StorageTexture) {
|
|
EXIT("TextureCache: unsupported image recreation source kind %u\n",
|
|
static_cast<uint32_t>(cached->kind));
|
|
}
|
|
const auto transfer = cached->kind == CachedImage::Kind::DepthTarget
|
|
? m_readback->DownloadDepthTarget(*cached, false)
|
|
: m_readback->DownloadColorImage(*cached);
|
|
for (const auto& range: transfer.Ranges()) {
|
|
m_memory_tracker.ForEachDownloadRange<true>(range.address, range.size,
|
|
[](uint64_t, uint64_t) noexcept {});
|
|
}
|
|
cached->gpu_modified = false;
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
|
|
bool Equal(const ImageInfo& left, const ImageInfo& right) {
|
|
return left.address == right.address && left.size == right.size &&
|
|
left.format == right.format && left.width == right.width &&
|
|
left.height == right.height && left.pitch == right.pitch &&
|
|
left.base_level == right.base_level && left.levels == right.levels &&
|
|
left.view_levels == right.view_levels && left.tile == right.tile &&
|
|
left.swizzle == right.swizzle && left.depth == right.depth && left.type == right.type &&
|
|
left.base_array == right.base_array;
|
|
}
|
|
|
|
bool EqualStorageBacking(const ImageInfo& left, const ImageInfo& right) {
|
|
return left.address == right.address && left.size == right.size &&
|
|
left.format == right.format && left.width == right.width &&
|
|
left.height == right.height && left.pitch == right.pitch &&
|
|
left.levels == right.levels && left.tile == right.tile && left.depth == right.depth &&
|
|
left.type == right.type && left.base_array == right.base_array;
|
|
}
|
|
|
|
bool Equal(const RenderTargetInfo& left, const RenderTargetInfo& right) {
|
|
return left.address == right.address && left.size == right.size &&
|
|
left.format == right.format && left.width == right.width &&
|
|
left.height == right.height && left.pitch == right.pitch &&
|
|
left.bytes_per_element == right.bytes_per_element && left.tile_mode == right.tile_mode &&
|
|
left.levels == right.levels && left.layers == right.layers &&
|
|
left.samples == right.samples;
|
|
}
|
|
|
|
bool Equal(const DepthTargetInfo& left, const DepthTargetInfo& right) {
|
|
return left.address == right.address && left.size == right.size &&
|
|
left.stencil_address == right.stencil_address &&
|
|
left.stencil_size == right.stencil_size && left.htile_address == right.htile_address &&
|
|
left.htile_size == right.htile_size && left.format == right.format &&
|
|
left.guest_format == right.guest_format && left.width == right.width &&
|
|
left.height == right.height && left.pitch == right.pitch &&
|
|
left.bytes_per_element == right.bytes_per_element && left.tile_mode == right.tile_mode &&
|
|
left.layers == right.layers && left.samples == right.samples &&
|
|
left.stencil_htile_compressed == right.stencil_htile_compressed;
|
|
}
|
|
|
|
[[nodiscard]] bool IsCoherentGuestImageSource(const BufferImageCopySource& source, uint64_t address,
|
|
uint64_t size) {
|
|
// The current PS5 Tiler consumes coherent guest backing directly. A native buffer is an
|
|
// optional future GPU-detiler source or staging fallback.
|
|
return source.cpu_current && source.address == address && source.size == size &&
|
|
(source.buffer != nullptr || source.offset == 0);
|
|
}
|
|
|
|
[[nodiscard]] BufferImageCopySource SelectSourceRange(const BufferImageCopySource& source,
|
|
uint64_t address, uint64_t size) {
|
|
if (address < source.address || size > source.size ||
|
|
address - source.address > source.size - size ||
|
|
(source.buffer != nullptr && source.offset > UINT64_MAX - (address - source.address))) {
|
|
EXIT("TextureCache: image source subrange is invalid\n");
|
|
}
|
|
const auto offset = source.buffer != nullptr ? source.offset + address - source.address : 0;
|
|
return {source.buffer, offset, address, size, source.cpu_current};
|
|
}
|
|
|
|
[[nodiscard]] DepthTargetInfo SelectDepthLayers(const DepthTargetInfo& info, uint32_t base_layer,
|
|
uint32_t layer_count) {
|
|
if (info.layers == 0 || base_layer >= info.layers || layer_count == 0 ||
|
|
layer_count > info.layers - base_layer || info.size % info.layers != 0 ||
|
|
info.stencil_size % info.layers != 0 || info.htile_size % info.layers != 0) {
|
|
EXIT("TextureCache: depth layer range is invalid\n");
|
|
}
|
|
auto result = info;
|
|
result.layers = layer_count;
|
|
for (auto [address, size]: {std::pair {&result.address, &result.size},
|
|
std::pair {&result.stencil_address, &result.stencil_size},
|
|
std::pair {&result.htile_address, &result.htile_size}}) {
|
|
const auto slice_size = *size / info.layers;
|
|
*address += slice_size * base_layer;
|
|
*size = slice_size * layer_count;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void AppendLayerCopies(std::vector<ImageImageCopy>& regions, VulkanImage* source,
|
|
vk::ImageAspectFlags aspect) {
|
|
for (uint32_t layer = 0; layer < source->layers; layer++) {
|
|
for (uint32_t level = 0; level < source->mip_levels; level++) {
|
|
ImageImageCopy region {};
|
|
region.src_image = source;
|
|
region.src_aspect = aspect;
|
|
region.dst_aspect = aspect;
|
|
region.src_level = level;
|
|
region.dst_level = level;
|
|
region.src_layer = layer;
|
|
region.dst_layer = layer;
|
|
region.width = std::max(source->extent.width >> level, 1u);
|
|
region.height = std::max(source->extent.height >> level, 1u);
|
|
regions.push_back(region);
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TextureCache::TextureCache(PageManager& page_manager, BufferCache& buffer_cache,
|
|
ResourceMutex& resource_mutex)
|
|
: m_memory_tracker(page_manager), m_metadata_tracker(page_manager, PageWatchMode::Write),
|
|
m_buffer_cache(buffer_cache), m_resource_mutex(resource_mutex) {
|
|
if (!Common::Thread::IsMainThread()) {
|
|
EXIT("TextureCache: construction is restricted to the main thread\n");
|
|
}
|
|
m_dummy_textures = std::make_unique<DummyTextureCache>();
|
|
m_readback = std::make_unique<ReadbackWorker>(*this);
|
|
}
|
|
|
|
TextureCache::CachedImage* TextureCache::FindGpuReadbackPageCandidateLocked(uint64_t vaddr,
|
|
uint64_t size) {
|
|
CachedImage* selected = nullptr;
|
|
for (auto* cached: FindImagesInRegionLocked(vaddr, size, true)) {
|
|
if (!cached->IsGpuReadbackPageCandidate(vaddr, size)) {
|
|
continue;
|
|
}
|
|
if (selected != nullptr) {
|
|
EXIT("TextureCache: CPU fault has multiple GPU-modified image page candidates, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 " first=%p second=%p\n",
|
|
vaddr, size, static_cast<const void*>(selected), static_cast<const void*>(cached));
|
|
}
|
|
selected = cached;
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
void TextureCache::MarkSampledAliasesCpuDirtyLocked(uint64_t vaddr, uint64_t size) {
|
|
for (auto* cached: FindImagesInRegionLocked(vaddr, size, true)) {
|
|
if (cached->kind != CachedImage::Kind::Texture) {
|
|
continue;
|
|
}
|
|
if (cached->gpu_modified &&
|
|
ImageRangeOverlaps(cached->info.address, cached->info.size, vaddr, size)) {
|
|
EXIT("TextureCache: CPU write overlaps a GPU-modified sampled texture, "
|
|
"write=0x%016" PRIx64 "+0x%016" PRIx64 " image=0x%016" PRIx64 "+0x%016" PRIx64
|
|
"\n",
|
|
vaddr, size, cached->info.address, cached->info.size);
|
|
}
|
|
cached->info.InvalidateCpuWrite(vaddr, size);
|
|
if (cached->info.NeedsMaybeCpuHash()) {
|
|
cached->info.SetMaybeCpuHash(HashSampledImageEdges(cached->info));
|
|
}
|
|
}
|
|
}
|
|
|
|
void TextureCache::RetireSampledTargetAliases(GraphicContext* ctx, const ImageInfo& requested) {
|
|
std::vector<CachedImage*> retire;
|
|
bool wait_idle = false;
|
|
for (auto* cached: FindImagesInRegionLocked(requested.address, requested.size, true)) {
|
|
if (cached->kind != CachedImage::Kind::RenderTarget) {
|
|
continue;
|
|
}
|
|
switch (ClassifySampledRenderTargetOverlap(requested, cached->target,
|
|
cached->buffer_modified, cached->ctx == ctx)) {
|
|
case RenderTargetOverlap::None: continue;
|
|
case RenderTargetOverlap::RetireTarget:
|
|
wait_idle |= cached->gpu_modified;
|
|
retire.push_back(cached);
|
|
break;
|
|
case RenderTargetOverlap::RetireSampled:
|
|
case RenderTargetOverlap::RetireStorage:
|
|
case RenderTargetOverlap::PreserveStorage:
|
|
case RenderTargetOverlap::ExpandTarget:
|
|
case RenderTargetOverlap::Unsupported:
|
|
EXIT("TextureCache: unsupported sampled/render-target alias, sampled=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " target=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" same_context=%d gpu=%d buffer=%d\n",
|
|
requested.address, requested.size, cached->target.address, cached->target.size,
|
|
cached->ctx == ctx, cached->gpu_modified, cached->buffer_modified);
|
|
}
|
|
}
|
|
for (auto* cached: FindImagesInRegionLocked(requested.address, requested.size, true)) {
|
|
if (cached->kind != CachedImage::Kind::DepthTarget) {
|
|
continue;
|
|
}
|
|
const bool overlaps_depth = ImageRangeOverlaps(requested.address, requested.size,
|
|
cached->depth.address, cached->depth.size);
|
|
const bool overlaps_stencil =
|
|
cached->depth.stencil_address != 0 &&
|
|
ImageRangeOverlaps(requested.address, requested.size, cached->depth.stencil_address,
|
|
cached->depth.stencil_size);
|
|
if (!overlaps_depth && !overlaps_stencil) {
|
|
continue;
|
|
}
|
|
const auto delta = cached->depth.address >= requested.address
|
|
? cached->depth.address - requested.address
|
|
: UINT64_MAX;
|
|
const bool contained =
|
|
delta <= requested.size && cached->depth.size <= requested.size - delta;
|
|
const bool sampled_expansion = IsSampledDepthExpansion(requested, cached->depth);
|
|
const bool exact_range =
|
|
requested.address == cached->depth.address && requested.size == cached->depth.size;
|
|
const bool depth_tracker_gpu =
|
|
m_memory_tracker.IsRegionGpuModified(cached->depth.address, cached->depth.size);
|
|
const bool stencil_tracker_gpu =
|
|
cached->depth.stencil_address != 0 &&
|
|
m_memory_tracker.IsRegionGpuModified(cached->depth.stencil_address,
|
|
cached->depth.stencil_size);
|
|
const bool clean_pool_alias = CanRetireGuestCurrentDepthForSampled(
|
|
requested, cached->depth, cached->gpu_modified, cached->buffer_modified,
|
|
depth_tracker_gpu, stencil_tracker_gpu, cached->ctx == ctx);
|
|
const bool native_transition =
|
|
cached->ctx == ctx && !cached->buffer_modified && cached->depth.stencil_address == 0 &&
|
|
cached->depth.stencil_size == 0 && cached->depth.layers == 1 && contained &&
|
|
(exact_range || sampled_expansion);
|
|
const bool supported = clean_pool_alias || native_transition;
|
|
if (!supported) {
|
|
EXIT("TextureCache: unsupported sampled/depth-target alias, sampled=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " depth=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" overlaps=%d/%d contained=%d same_context=%d gpu=%d/%d/%d buffer=%d"
|
|
" stencil=0x%016" PRIx64 "+0x%016" PRIx64 " layers=%u expansion=%d"
|
|
" sampled_info={fmt=%u extent=%ux%u pitch=%u levels=%u/%u tile=%u type=%u}"
|
|
" depth_info={fmt=%u host=%d extent=%ux%u pitch=%u tile=%u htile=0x%016" PRIx64
|
|
"+0x%016" PRIx64 "}\n",
|
|
requested.address, requested.size, cached->depth.address, cached->depth.size,
|
|
overlaps_depth, overlaps_stencil, contained, cached->ctx == ctx,
|
|
cached->gpu_modified, depth_tracker_gpu, stencil_tracker_gpu,
|
|
cached->buffer_modified, cached->depth.stencil_address, cached->depth.stencil_size,
|
|
cached->depth.layers, sampled_expansion, requested.format, requested.width,
|
|
requested.height, requested.pitch, requested.levels, requested.view_levels,
|
|
requested.tile, requested.type, cached->depth.guest_format,
|
|
static_cast<int>(cached->depth.format), cached->depth.width, cached->depth.height,
|
|
cached->depth.pitch, cached->depth.tile_mode, cached->depth.htile_address,
|
|
cached->depth.htile_size);
|
|
}
|
|
wait_idle |= cached->gpu_modified;
|
|
retire.push_back(cached);
|
|
}
|
|
if (retire.empty()) {
|
|
return;
|
|
}
|
|
RequireRetirementIsolation(retire, "sampled target", requested.address, requested.size);
|
|
if (wait_idle) {
|
|
Transfer::WaitForGraphicsIdle(ctx);
|
|
}
|
|
for (auto* cached: retire) {
|
|
if (!cached->gpu_modified) {
|
|
continue;
|
|
}
|
|
const auto transfer = cached->kind == CachedImage::Kind::DepthTarget
|
|
? m_readback->DownloadDepthTarget(*cached, false)
|
|
: m_readback->DownloadColorImage(*cached);
|
|
for (const auto& range: transfer.Ranges()) {
|
|
m_memory_tracker.ForEachDownloadRange<true>(range.address, range.size,
|
|
[](uint64_t, uint64_t) noexcept {});
|
|
}
|
|
cached->gpu_modified = false;
|
|
}
|
|
RetireDepthMetadataLocked(retire);
|
|
RetireImages(retire);
|
|
}
|
|
|
|
void TextureCache::ResolveStorageImageOverlaps(GraphicContext* ctx, const ImageInfo& requested) {
|
|
std::vector<CachedImage*> retire;
|
|
for (auto* cached: FindImagesInRegionLocked(requested.address, requested.size, true)) {
|
|
const bool tracker_gpu =
|
|
m_memory_tracker.IsRegionGpuModified(cached->Address(), cached->Size());
|
|
switch (ClassifyStorageImageOverlap(
|
|
requested.address, requested.size, cached->Address(), cached->Size(),
|
|
cached->kind == CachedImage::Kind::Texture, cached->ctx == ctx, cached->gpu_modified,
|
|
cached->buffer_modified, tracker_gpu)) {
|
|
case StorageImageOverlap::None: continue;
|
|
case StorageImageOverlap::RetireSampled: retire.push_back(cached); continue;
|
|
case StorageImageOverlap::PageNeighbor: continue;
|
|
case StorageImageOverlap::Unsupported:
|
|
EXIT("TextureCache: unsupported storage-image byte alias, requested=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " existing=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" kind=%u same_context=%d gpu=%d/%d buffer=%d\n",
|
|
requested.address, requested.size, cached->Address(), cached->Size(),
|
|
static_cast<uint32_t>(cached->kind), cached->ctx == ctx, cached->gpu_modified,
|
|
tracker_gpu, cached->buffer_modified);
|
|
}
|
|
}
|
|
// Every exact byte overlap was classified above: only clean sampled images are retired and all
|
|
// retained byte aliases remain fatal. A retired full mip chain can still contain a cached,
|
|
// byte-disjoint subresource outside this storage request. The multi-owner index keeps those
|
|
// retained pages tracked and UnregisterImageLocked releases only pages whose final owner left.
|
|
for (auto* cached: retire) {
|
|
if (!cached->gpu_modified) {
|
|
continue;
|
|
}
|
|
const auto transfer = m_readback->DownloadColorImage(*cached);
|
|
for (const auto& range: transfer.Ranges()) {
|
|
m_memory_tracker.ForEachDownloadRange<true>(range.address, range.size,
|
|
[](uint64_t, uint64_t) noexcept {});
|
|
}
|
|
cached->gpu_modified = false;
|
|
}
|
|
RetireImages(retire);
|
|
}
|
|
|
|
void TextureCache::RetireStorageDepthAliasLocked(GraphicContext* ctx, const ImageInfo& requested) {
|
|
CachedImage* selected = nullptr;
|
|
for (auto* entry: FindImagesInRegionLocked(requested.address, requested.size, true)) {
|
|
auto& cached = *entry;
|
|
if (cached.kind != CachedImage::Kind::DepthTarget ||
|
|
!cached.OverlapsRange(requested.address, requested.size, true)) {
|
|
continue;
|
|
}
|
|
const auto& depth = cached.depth;
|
|
const bool exact_d32_uint =
|
|
cached.ctx == ctx && cached.gpu_modified && !cached.buffer_modified &&
|
|
depth.address == requested.address && depth.size == requested.size &&
|
|
depth.stencil_address == 0 && depth.stencil_size == 0 && depth.htile_address == 0 &&
|
|
depth.htile_size == 0 && depth.layers == 1 && depth.format == vk::Format::eD32Sfloat &&
|
|
depth.guest_format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32Float) &&
|
|
depth.bytes_per_element == 4 && depth.width == requested.width &&
|
|
depth.height == requested.height && depth.pitch == requested.pitch &&
|
|
requested.format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32UInt) &&
|
|
requested.tile == Prospero::GpuEnumValue(Prospero::TileMode::kDepth) &&
|
|
requested.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor2D) &&
|
|
requested.depth == 1 && requested.levels == 1;
|
|
if (!exact_d32_uint || selected != nullptr) {
|
|
EXIT("TextureCache: unsupported storage/depth-target alias, requested=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " depth=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" exact=%d ambiguous=%d\n",
|
|
requested.address, requested.size, depth.address, depth.size, exact_d32_uint,
|
|
selected != nullptr);
|
|
}
|
|
selected = &cached;
|
|
}
|
|
if (selected == nullptr) {
|
|
return;
|
|
}
|
|
Transfer::WaitForGraphicsIdle(ctx);
|
|
const auto transfer = m_readback->DownloadDepthTarget(*selected, false);
|
|
for (const auto& range: transfer.Ranges()) {
|
|
m_memory_tracker.ForEachDownloadRange<true>(range.address, range.size,
|
|
[](uint64_t, uint64_t) noexcept {});
|
|
}
|
|
selected->gpu_modified = false;
|
|
RetireImages({selected});
|
|
}
|
|
|
|
TextureCache::~TextureCache() {
|
|
m_readback.reset();
|
|
if (!m_images.empty()) {
|
|
Transfer::WaitForGraphicsIdle(m_images.front()->ctx);
|
|
}
|
|
for (const auto& image: m_images) {
|
|
UnregisterImageLocked(*image, false);
|
|
}
|
|
m_images.clear();
|
|
m_dummy_textures.reset();
|
|
}
|
|
|
|
VulkanImage* TextureCache::FindTexture(CommandBuffer* command, GraphicContext* ctx,
|
|
const ImageInfo& info, bool metadata_read) {
|
|
if (info.address == 0 || info.size == 0 || info.address >= TRACKER_ADDRESS_SIZE ||
|
|
info.size > TRACKER_ADDRESS_SIZE - info.address || info.width == 0 || info.height == 0 ||
|
|
info.depth == 0 || info.levels == 0 || info.levels >= 16 || info.view_levels == 0 ||
|
|
info.base_level + info.view_levels > info.levels) {
|
|
EXIT("TextureCache: invalid sampled-image request, command=%p ctx=%p addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " extent=%ux%ux%u levels=%u\n",
|
|
static_cast<const void*>(command), static_cast<const void*>(ctx), info.address,
|
|
info.size, info.width, info.height, info.depth, info.levels);
|
|
}
|
|
std::lock_guard transaction(m_resource_mutex);
|
|
{
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
RetireSampledTargetAliases(ctx, info);
|
|
}
|
|
BufferImageCopySource source {nullptr, 0, info.address, info.size, true};
|
|
if (m_buffer_cache.HasPageOverlap(info.address, info.size)) {
|
|
// ObtainBufferForImage publishes dirty native-buffer bytes when necessary and otherwise
|
|
// uses a CPU-current staging fallback through guest backing.
|
|
source = m_buffer_cache.ObtainBufferForImage(info.address, info.size);
|
|
if (!IsCoherentGuestImageSource(source, info.address, info.size)) {
|
|
EXIT("TextureCache: sampled-image buffer source is inconsistent, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " source=%p source_addr=0x%016" PRIx64
|
|
" source_size=0x%016" PRIx64 " current=%d\n",
|
|
info.address, info.size, static_cast<const void*>(source.buffer), source.address,
|
|
source.size, source.cpu_current);
|
|
}
|
|
}
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
if (HasMetaOverlapLocked(info.address, info.size) != metadata_read) {
|
|
EXIT("TextureCache: sampled-image metadata classification mismatch, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " requested=%d\n",
|
|
info.address, info.size, metadata_read);
|
|
}
|
|
std::shared_ptr<CachedImage> storage_match;
|
|
std::vector<CachedImage*> storage_retire;
|
|
const auto requested_view_format = TextureGetFormat(info.format);
|
|
for (auto& cached: m_images) {
|
|
if (cached->kind != CachedImage::Kind::StorageTexture) {
|
|
continue;
|
|
}
|
|
const bool tracker_gpu =
|
|
m_memory_tracker.IsRegionGpuModified(cached->info.address, cached->info.size);
|
|
const bool cpu_dirty =
|
|
cached->info.IsCpuDirty() ||
|
|
m_memory_tracker.IsRegionCpuModified(cached->info.address, cached->info.size);
|
|
const bool exact_mip = IsExactRenderTargetMipStorage(
|
|
info, cached->info, requested_view_format, cached->image->format);
|
|
switch (ClassifyStorageSampledOverlap(
|
|
info, cached->info, requested_view_format, cached->image->format, cached->gpu_modified,
|
|
cpu_dirty, cached->ctx == ctx, exact_mip, cached->buffer_modified, tracker_gpu)) {
|
|
case StorageSampledOverlap::None: break;
|
|
case StorageSampledOverlap::ExactImage:
|
|
if (storage_match != nullptr) {
|
|
EXIT("TextureCache: duplicate exact storage image for sampled binding, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
|
|
info.address, info.size);
|
|
}
|
|
storage_match = cached;
|
|
break;
|
|
case StorageSampledOverlap::RetireStorage:
|
|
storage_retire.push_back(cached.get());
|
|
break;
|
|
case StorageSampledOverlap::Unsupported:
|
|
EXIT("TextureCache: unsupported sampled/storage image alias, "
|
|
"requested=0x%016" PRIx64 "+0x%016" PRIx64 " storage=0x%016" PRIx64
|
|
"+0x%016" PRIx64
|
|
" gpu_modified=%d buffer_modified=%d tracker_gpu=%d cpu_dirty=%d"
|
|
" same_context=%d exact_mip=%d"
|
|
" requested_info={format=%u extent=%ux%ux%u pitch=%u base=%u levels=%u"
|
|
" view_levels=%u tile=%u swizzle=0x%03x type=%u base_array=%u}"
|
|
" storage_info={format=%u extent=%ux%ux%u pitch=%u base=%u levels=%u"
|
|
" view_levels=%u tile=%u swizzle=0x%03x type=%u base_array=%u}\n",
|
|
info.address, info.size, cached->info.address, cached->info.size,
|
|
cached->gpu_modified, cached->buffer_modified, tracker_gpu, cpu_dirty,
|
|
cached->ctx == ctx, exact_mip, info.format, info.width, info.height,
|
|
info.depth, info.pitch, info.base_level, info.levels, info.view_levels,
|
|
info.tile, info.swizzle, info.type, info.base_array, cached->info.format,
|
|
cached->info.width, cached->info.height, cached->info.depth,
|
|
cached->info.pitch, cached->info.base_level, cached->info.levels,
|
|
cached->info.view_levels, cached->info.tile, cached->info.swizzle,
|
|
cached->info.type, cached->info.base_array);
|
|
}
|
|
}
|
|
if (storage_match != nullptr && !storage_retire.empty()) {
|
|
EXIT("TextureCache: sampled binding has both exact and mip storage owners, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 " mip_owners=%zu\n",
|
|
info.address, info.size, storage_retire.size());
|
|
}
|
|
if (storage_match != nullptr) {
|
|
if (m_memory_tracker.IsRegionCpuModified(info.address, info.size) ||
|
|
!m_memory_tracker.IsRegionGpuModified(info.address, info.size) ||
|
|
storage_match->image->type != VulkanImageType::StorageTexture ||
|
|
storage_match->image->image_view[VulkanImage::VIEW_DEFAULT] == nullptr) {
|
|
EXIT("TextureCache: sampled storage-image ownership or view is inconsistent, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
|
|
info.address, info.size);
|
|
}
|
|
command->RetainResourceUntilFence(storage_match);
|
|
return storage_match->image;
|
|
}
|
|
if (!storage_retire.empty()) {
|
|
// shadPS4 resolves a modified cached mip before replacing it with the containing image.
|
|
// Kyty does not yet copy between those independently allocated Vulkan images, so use the
|
|
// existing synchronized tiled readback seam and rebuild the complete chain from coherent
|
|
// guest backing. This is an uncommon ownership transition, not a frame lookup fast path.
|
|
Transfer::WaitForGraphicsIdle(ctx);
|
|
for (auto* cached: storage_retire) {
|
|
if (!cached->gpu_modified || cached->buffer_modified || cached->info.IsCpuDirty() ||
|
|
!m_memory_tracker.IsRegionGpuModified(cached->info.address, cached->info.size) ||
|
|
m_memory_tracker.IsRegionCpuModified(cached->info.address, cached->info.size)) {
|
|
EXIT("TextureCache: sampled mip-storage ownership changed during transition, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 " gpu=%d buffer=%d dirty=%d\n",
|
|
cached->info.address, cached->info.size, cached->gpu_modified,
|
|
cached->buffer_modified, cached->info.IsCpuDirty());
|
|
}
|
|
const auto transfer = m_readback->DownloadColorImage(*cached);
|
|
for (const auto& range: transfer.Ranges()) {
|
|
m_memory_tracker.ForEachDownloadRange<true>(range.address, range.size,
|
|
[](uint64_t, uint64_t) noexcept {});
|
|
}
|
|
cached->gpu_modified = false;
|
|
}
|
|
RetireImages(storage_retire);
|
|
}
|
|
if (m_memory_tracker.IsRegionCpuModified(info.address, info.size)) {
|
|
MarkSampledAliasesCpuDirtyLocked(info.address, info.size);
|
|
}
|
|
std::shared_ptr<CachedImage> match;
|
|
for (auto& cached: m_images) {
|
|
if (cached->kind != CachedImage::Kind::Texture || !Equal(info, cached->info)) {
|
|
continue;
|
|
}
|
|
if (match != nullptr || cached->gpu_modified || cached->ctx != ctx) {
|
|
EXIT("TextureCache: invalid exact sampled-image cache match, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " duplicate=%d gpu_modified=%d same_context=%d\n",
|
|
info.address, info.size, match != nullptr, cached->gpu_modified,
|
|
cached->ctx == ctx);
|
|
}
|
|
match = cached;
|
|
}
|
|
if (match != nullptr) {
|
|
if (match->info.IsMaybeCpuDirty() && !match->info.IsDefinitelyCpuDirty()) {
|
|
const bool changed =
|
|
match->info.ResolveMaybeCpuHash(HashSampledImageEdges(match->info));
|
|
if (!changed) {
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, false, [](uint64_t, uint64_t) noexcept {},
|
|
[]() noexcept {});
|
|
}
|
|
}
|
|
const bool buffer_dirty = match->buffer_modified;
|
|
if (buffer_dirty) {
|
|
if (match->gpu_modified ||
|
|
!IsCoherentGuestImageSource(source, info.address, info.size)) {
|
|
EXIT("TextureCache: sampled-image refresh has invalid buffer ownership, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64
|
|
" source=%p current=%d gpu_modified=%d\n",
|
|
info.address, info.size, static_cast<const void*>(source.buffer),
|
|
source.cpu_current, match->gpu_modified);
|
|
}
|
|
m_memory_tracker.MarkRegionAsCpuModified(info.address, info.size);
|
|
}
|
|
if (match->info.IsCpuDirty() || buffer_dirty) {
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, false, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
m_tiler.DetileImage(match->ctx,
|
|
static_cast<GpuTextureVulkanImage*>(match->image),
|
|
match->info, source, true, false);
|
|
});
|
|
if (match->info.IsCpuDirty()) {
|
|
match->info.RefreshComplete();
|
|
}
|
|
match->buffer_modified = false;
|
|
}
|
|
command->RetainResourceUntilFence(match);
|
|
return match->image;
|
|
}
|
|
for (const auto& cached: m_images) {
|
|
if (cached->kind != CachedImage::Kind::Texture) {
|
|
if (!cached->OverlapsRange(info.address, info.size, true)) {
|
|
continue;
|
|
}
|
|
EXIT("TextureCache: sampled image overlaps GPU target, requested=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " target=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" target_kind=%u requested_info={format=%u extent=%ux%ux%u pitch=%u levels=%u"
|
|
" tile=%u type=%u}\n",
|
|
info.address, info.size, cached->Address(), cached->Size(),
|
|
static_cast<uint32_t>(cached->kind), info.format, info.width, info.height,
|
|
info.depth, info.pitch, info.levels, info.tile, info.type);
|
|
}
|
|
const auto overlap =
|
|
ClassifySampledOverlap(info, cached->info, cached->gpu_modified, cached->ctx == ctx);
|
|
if (overlap == SampledOverlap::Unsupported) {
|
|
EXIT("TextureCache: unsupported sampled-texture alias, requested=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " existing=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" gpu_modified=%d same_context=%d\n",
|
|
info.address, info.size, cached->info.address, cached->info.size,
|
|
cached->gpu_modified, cached->ctx == ctx);
|
|
}
|
|
}
|
|
m_images.reserve(m_images.size() + 1);
|
|
auto cached = std::make_shared<CachedImage>();
|
|
cached->kind = CachedImage::Kind::Texture;
|
|
cached->info = info;
|
|
cached->ctx = ctx;
|
|
vk::ComponentMapping components {};
|
|
cached->image = ImageOps::CreateTexture(ctx, info, false, &components);
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, false, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
m_tiler.DetileImage(cached->ctx, static_cast<GpuTextureVulkanImage*>(cached->image),
|
|
cached->info, source, false, false);
|
|
});
|
|
ImageOps::CreateTextureViews(ctx, static_cast<GpuTextureVulkanImage*>(cached->image), info,
|
|
false, components);
|
|
return PublishImage(command, std::move(cached));
|
|
}
|
|
|
|
StorageTextureVulkanImage* TextureCache::FindStorageTexture(CommandBuffer* command,
|
|
GraphicContext* ctx,
|
|
const ImageInfo& info) {
|
|
const bool supported_type =
|
|
info.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor2D) ||
|
|
info.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor2DArray) ||
|
|
info.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor3D);
|
|
const bool supported_depth_tile =
|
|
info.tile == Prospero::GpuEnumValue(Prospero::TileMode::kDepth) &&
|
|
IsSupportedStorageDepthTile(info.format, info.type, info.width, info.height, info.depth);
|
|
if (info.address == 0 || info.size == 0 || info.address >= TRACKER_ADDRESS_SIZE ||
|
|
info.size > TRACKER_ADDRESS_SIZE - info.address || info.width == 0 || info.height == 0 ||
|
|
info.depth == 0 || info.levels == 0 || info.levels > 16 || info.base_level >= info.levels ||
|
|
info.view_levels != 1 || info.base_array != 0 || !supported_type ||
|
|
(info.tile != Prospero::GpuEnumValue(Prospero::TileMode::kLinear) &&
|
|
info.tile != Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget) &&
|
|
!supported_depth_tile) ||
|
|
!IsSupportedStorageSwizzle(info.format, info.swizzle)) {
|
|
EXIT("TextureCache: unsupported storage-image request, command=%p ctx=%p "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64
|
|
" extent=%ux%ux%u levels=%u base_level=%u base_array=%u type=%u tile=%u "
|
|
"swizzle=0x%03x\n",
|
|
static_cast<const void*>(command), static_cast<const void*>(ctx), info.address,
|
|
info.size, info.width, info.height, info.depth, info.levels, info.base_level,
|
|
info.base_array, info.type, info.tile, info.swizzle);
|
|
}
|
|
if ((info.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor2D) && info.depth != 1) ||
|
|
(info.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor3D) && info.depth == 0) ||
|
|
(info.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor2DArray) &&
|
|
info.base_array >= info.depth)) {
|
|
EXIT("TextureCache: storage-image type and depth disagree, type=%u depth=%u\n", info.type,
|
|
info.depth);
|
|
}
|
|
|
|
std::lock_guard transaction(m_resource_mutex);
|
|
const bool buffer_overlap = m_buffer_cache.HasPageOverlap(info.address, info.size);
|
|
BufferImageCopySource source {nullptr, 0, info.address, info.size, true};
|
|
if (buffer_overlap) {
|
|
source = m_buffer_cache.ObtainBufferForImage(info.address, info.size);
|
|
if (!IsCoherentGuestImageSource(source, info.address, info.size)) {
|
|
EXIT("TextureCache: storage-image buffer source is inconsistent, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " source=%p source_addr=0x%016" PRIx64
|
|
" source_size=0x%016" PRIx64 " current=%d\n",
|
|
info.address, info.size, static_cast<const void*>(source.buffer), source.address,
|
|
source.size, source.cpu_current);
|
|
}
|
|
}
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
ResolveImageMetadataOverlapsLocked(ctx, info.address, info.size);
|
|
if (supported_depth_tile &&
|
|
info.format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32UInt)) {
|
|
RetireStorageDepthAliasLocked(ctx, info);
|
|
}
|
|
|
|
std::shared_ptr<CachedImage> match;
|
|
for (auto& cached: m_images) {
|
|
if (cached->kind != CachedImage::Kind::StorageTexture ||
|
|
!EqualStorageBacking(info, cached->info)) {
|
|
continue;
|
|
}
|
|
if (match != nullptr || cached->ctx != ctx || cached->info.IsCpuDirty()) {
|
|
EXIT("TextureCache: invalid exact storage-image cache match, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " duplicate=%d same_context=%d cpu_dirty=%d\n",
|
|
info.address, info.size, match != nullptr, cached->ctx == ctx,
|
|
cached->info.IsCpuDirty());
|
|
}
|
|
match = cached;
|
|
}
|
|
if (match != nullptr) {
|
|
const bool cpu_modified = m_memory_tracker.IsRegionCpuModified(info.address, info.size);
|
|
const bool gpu_modified = m_memory_tracker.IsRegionGpuModified(info.address, info.size);
|
|
const auto rebind = ClassifyStorageBufferRebind(
|
|
buffer_overlap, match->gpu_modified, match->buffer_modified, gpu_modified, cpu_modified,
|
|
IsCoherentGuestImageSource(source, info.address, info.size));
|
|
if (rebind == StorageBufferRebind::Unsupported) {
|
|
EXIT("TextureCache: storage-image ownership is inconsistent, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64
|
|
" cached_gpu_modified=%d buffer_modified=%d tracker_gpu_modified=%d "
|
|
"cpu_modified=%d buffer_overlap=%d source_current=%d\n",
|
|
info.address, info.size, match->gpu_modified, match->buffer_modified, gpu_modified,
|
|
cpu_modified, buffer_overlap, source.cpu_current);
|
|
}
|
|
if (rebind == StorageBufferRebind::RefreshFromBacking) {
|
|
// The formatted buffer may cover only one page of a packed mip chain. The
|
|
// synchronization path published the complete tiled backing before that write,
|
|
// and ObtainBufferForImage has now folded its dirty subrange back into it.
|
|
m_memory_tracker.MarkRegionAsCpuModified(info.address, info.size);
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, true, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
m_tiler.DetileImage(match->ctx,
|
|
static_cast<GpuTextureVulkanImage*>(match->image),
|
|
match->info, source, true, true);
|
|
});
|
|
match->buffer_modified = false;
|
|
match->gpu_modified = true;
|
|
} else if (!gpu_modified) {
|
|
if (cpu_modified) {
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, true, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
m_tiler.DetileImage(
|
|
match->ctx, static_cast<GpuTextureVulkanImage*>(match->image),
|
|
match->info, {nullptr, 0, match->info.address, match->info.size, true},
|
|
true, true);
|
|
});
|
|
} else {
|
|
// A readback leaves both copies current. Reclaim ownership without an unnecessary
|
|
// upload through the clean UpdateImage storage-binding path.
|
|
m_memory_tracker.MarkRegionAsGpuModified(info.address, info.size);
|
|
}
|
|
match->gpu_modified = true;
|
|
}
|
|
command->RetainResourceUntilFence(match);
|
|
return static_cast<StorageTextureVulkanImage*>(match->image);
|
|
}
|
|
|
|
ResolveStorageImageOverlaps(ctx, info);
|
|
m_images.reserve(m_images.size() + 1);
|
|
auto cached = std::make_shared<CachedImage>();
|
|
cached->kind = CachedImage::Kind::StorageTexture;
|
|
cached->info = info;
|
|
cached->ctx = ctx;
|
|
vk::ComponentMapping components {};
|
|
cached->image = ImageOps::CreateTexture(ctx, info, true, &components);
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, true, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
m_tiler.DetileImage(cached->ctx, static_cast<GpuTextureVulkanImage*>(cached->image),
|
|
cached->info, source, false, true);
|
|
});
|
|
cached->gpu_modified = true;
|
|
ImageOps::CreateTextureViews(ctx, static_cast<GpuTextureVulkanImage*>(cached->image), info,
|
|
true, components);
|
|
return static_cast<StorageTextureVulkanImage*>(PublishImage(command, std::move(cached)));
|
|
}
|
|
|
|
RenderTextureVulkanImage* TextureCache::FindRenderTarget(CommandBuffer* command,
|
|
GraphicContext* ctx,
|
|
const RenderTargetInfo& info) {
|
|
const bool standard64 = IsSupportedStandard64RenderTarget(info);
|
|
const bool valid_samples =
|
|
info.samples == 1 || info.samples == 2 || info.samples == 4 || info.samples == 8;
|
|
if (info.address == 0 || info.size == 0 || info.address >= TRACKER_ADDRESS_SIZE ||
|
|
info.size > TRACKER_ADDRESS_SIZE - info.address || info.format == vk::Format::eUndefined ||
|
|
info.width == 0 || info.height == 0 || info.pitch < info.width ||
|
|
info.bytes_per_element == 0 || info.levels == 0 || info.levels > 16 || info.layers == 0 ||
|
|
info.size % info.layers != 0 || !valid_samples ||
|
|
(info.samples > 1 &&
|
|
(info.levels != 1 || info.layers != 1 ||
|
|
info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget))) ||
|
|
(info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kLinear) &&
|
|
info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget) &&
|
|
!standard64)) {
|
|
EXIT("TextureCache: invalid render-target request, ctx=%p addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " extent=%ux%u pitch=%u bpe=%u samples=%u tile=%u format=%d\n",
|
|
static_cast<const void*>(ctx), info.address, info.size, info.width, info.height,
|
|
info.pitch, info.bytes_per_element, info.samples, info.tile_mode,
|
|
static_cast<int>(info.format));
|
|
}
|
|
if (info.levels == 1 &&
|
|
info.tile_mode == Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget)) {
|
|
TileSizeAlign layout {};
|
|
const auto encoded_samples = static_cast<uint32_t>(std::countr_zero(info.samples));
|
|
if (!TileGetRenderTargetSize(info.width, info.height, info.pitch, info.bytes_per_element,
|
|
&layout, encoded_samples) ||
|
|
layout.size > UINT64_MAX / info.layers ||
|
|
static_cast<uint64_t>(layout.size) * info.layers != info.size) {
|
|
EXIT("TextureCache: invalid render-target block layout, size=0x%016" PRIx64
|
|
" expected=0x%08x extent=%ux%u pitch=%u bpe=%u samples=%u\n",
|
|
info.size, layout.size, info.width, info.height, info.pitch,
|
|
info.bytes_per_element, info.samples);
|
|
}
|
|
}
|
|
if (info.levels > 1) {
|
|
TileSizeAlign layout {};
|
|
if (info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget) ||
|
|
!TileGetRenderTargetMipLayout(info.width, info.height, info.pitch,
|
|
info.bytes_per_element, info.levels, &layout, nullptr,
|
|
nullptr) ||
|
|
layout.size > UINT64_MAX / info.layers ||
|
|
static_cast<uint64_t>(layout.size) * info.layers != info.size) {
|
|
EXIT("TextureCache: unsupported render-target mip layout, size=0x%016" PRIx64
|
|
" expected=0x%08x extent=%ux%u pitch=%u bpe=%u levels=%u tile=%u\n",
|
|
info.size, layout.size, info.width, info.height, info.pitch,
|
|
info.bytes_per_element, info.levels, info.tile_mode);
|
|
}
|
|
}
|
|
if (standard64) {
|
|
const auto format = ImageOps::RenderTargetTransferFormat(info.bytes_per_element);
|
|
const auto expected_pitch =
|
|
TileGetTexturePitch(format, info.width, info.levels, info.tile_mode);
|
|
TileSizeAlign layout {};
|
|
TileGetTextureSize(format, info.width, info.height, expected_pitch, info.levels,
|
|
info.tile_mode, &layout, nullptr, nullptr);
|
|
if (expected_pitch != info.pitch || layout.align != 65536 || layout.size != info.size) {
|
|
EXIT("TextureCache: invalid Standard64KB render-target layout,"
|
|
" size=0x%016" PRIx64 " expected=0x%08x align=0x%08x pitch=%u/%u\n",
|
|
info.size, layout.size, layout.align, info.pitch, expected_pitch);
|
|
}
|
|
}
|
|
const auto rows = static_cast<uint64_t>(info.height - 1);
|
|
if (rows > (UINT64_MAX - info.width) / info.pitch) {
|
|
EXIT("TextureCache: render-target element count overflow, extent=%ux%u pitch=%u\n",
|
|
info.width, info.height, info.pitch);
|
|
}
|
|
const auto elements = rows * info.pitch + info.width;
|
|
if (elements > UINT64_MAX / info.bytes_per_element ||
|
|
elements * info.bytes_per_element > UINT64_MAX / info.layers ||
|
|
info.size < elements * info.bytes_per_element * info.layers) {
|
|
EXIT("TextureCache: invalid render-target storage, size=0x%016" PRIx64
|
|
" elements=0x%016" PRIx64 " bpe=%u\n",
|
|
info.size, elements, info.bytes_per_element);
|
|
}
|
|
std::lock_guard transaction(m_resource_mutex);
|
|
BufferImageCopySource target_source {nullptr, 0, info.address, info.size, true};
|
|
const bool target_buffer_overlap = m_buffer_cache.HasPageOverlap(info.address, info.size);
|
|
if (target_buffer_overlap) {
|
|
// A render target may use a containing native buffer after dirty bytes are published or
|
|
// coherent guest backing for a clean partial view. ObtainBufferForImage keeps GPU-dirty
|
|
// partial ownership and inconsistent state as hard failures.
|
|
target_source = m_buffer_cache.ObtainBufferForImage(info.address, info.size);
|
|
}
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
std::shared_ptr<CachedImage> match;
|
|
for (auto& cached: m_images) {
|
|
if (cached->kind != CachedImage::Kind::RenderTarget || cached->ctx != ctx ||
|
|
(!Equal(info, cached->target) && !IsCompatibleRenderTargetView(cached->target, info) &&
|
|
!IsCompatibleRenderTargetBacking(cached->target, info))) {
|
|
continue;
|
|
}
|
|
match = cached;
|
|
}
|
|
if (match != nullptr) {
|
|
ResolveImageMetadataOverlapsLocked(ctx, info.address, info.size);
|
|
if (info.samples > 1 && (match->buffer_modified ||
|
|
m_memory_tracker.IsRegionCpuModified(info.address, info.size))) {
|
|
EXIT("TextureCache: multisampled render-target refresh is unsupported, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 " samples=%u\n",
|
|
info.address, info.size, info.samples);
|
|
}
|
|
if (match->buffer_modified) {
|
|
if (match->gpu_modified ||
|
|
!IsCoherentGuestImageSource(target_source, info.address, info.size)) {
|
|
EXIT("TextureCache: render-target refresh has invalid buffer ownership, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64
|
|
" source=%p current=%d gpu_modified=%d\n",
|
|
info.address, info.size, static_cast<const void*>(target_source.buffer),
|
|
target_source.cpu_current, match->gpu_modified);
|
|
}
|
|
m_memory_tracker.MarkRegionAsCpuModified(info.address, info.size);
|
|
}
|
|
if (!match->gpu_modified && m_memory_tracker.IsRegionCpuModified(info.address, info.size)) {
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, false, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
ImageOps::UploadRenderTarget(
|
|
ctx, static_cast<RenderTextureVulkanImage*>(match->image), info, true);
|
|
});
|
|
match->buffer_modified = false;
|
|
}
|
|
command->RetainResourceUntilFence(match);
|
|
return static_cast<RenderTextureVulkanImage*>(match->image);
|
|
}
|
|
std::vector<CachedImage*> retire;
|
|
std::vector<std::shared_ptr<CachedImage>> recreated_depth_sources;
|
|
std::shared_ptr<CachedImage> native_image_source;
|
|
for (const auto& entry: m_images) {
|
|
auto& cached = *entry;
|
|
if (!cached.OverlapsRange(info.address, info.size, true)) {
|
|
continue;
|
|
}
|
|
RenderTargetOverlap overlap = RenderTargetOverlap::Unsupported;
|
|
switch (cached.kind) {
|
|
case CachedImage::Kind::Texture:
|
|
overlap = ClassifyRenderTargetOverlap(cached.info, cached.gpu_modified,
|
|
cached.ctx == ctx, info);
|
|
break;
|
|
case CachedImage::Kind::StorageTexture:
|
|
overlap = ClassifyStorageRenderTargetOverlap(
|
|
cached.info, cached.image->format, cached.gpu_modified, cached.buffer_modified,
|
|
cached.info.IsCpuDirty() ||
|
|
m_memory_tracker.IsRegionCpuModified(cached.info.address, cached.info.size),
|
|
m_memory_tracker.IsRegionGpuModified(cached.info.address, cached.info.size),
|
|
cached.ctx == ctx, info);
|
|
break;
|
|
case CachedImage::Kind::RenderTarget:
|
|
overlap = ClassifyRenderTargetOverlap(
|
|
cached.target, cached.gpu_modified, cached.buffer_modified,
|
|
m_memory_tracker.IsRegionGpuModified(cached.target.address, cached.target.size),
|
|
cached.ctx == ctx,
|
|
IsCoherentGuestImageSource(target_source, info.address, info.size), info);
|
|
break;
|
|
case CachedImage::Kind::DepthTarget: {
|
|
bool tracker_gpu_modified = false;
|
|
for (uint32_t range = 0; range < cached.RangeCount(); range++) {
|
|
tracker_gpu_modified |= m_memory_tracker.IsRegionGpuModified(
|
|
cached.Address(range), cached.Size(range));
|
|
}
|
|
if (CanRecreateDepthForRenderTarget(
|
|
cached.depth, cached.gpu_modified, cached.buffer_modified,
|
|
tracker_gpu_modified, cached.ctx == ctx,
|
|
IsCoherentGuestImageSource(target_source, info.address, info.size), info)) {
|
|
overlap = RenderTargetOverlap::RetireTarget;
|
|
}
|
|
} break;
|
|
case CachedImage::Kind::VideoOut: break;
|
|
}
|
|
if (overlap == RenderTargetOverlap::None) {
|
|
continue;
|
|
}
|
|
bool supported = false;
|
|
switch (overlap) {
|
|
case RenderTargetOverlap::RetireSampled:
|
|
supported = cached.kind == CachedImage::Kind::Texture;
|
|
break;
|
|
case RenderTargetOverlap::RetireStorage:
|
|
supported = cached.kind == CachedImage::Kind::StorageTexture;
|
|
break;
|
|
case RenderTargetOverlap::PreserveStorage:
|
|
supported = cached.kind == CachedImage::Kind::StorageTexture &&
|
|
native_image_source == nullptr;
|
|
if (supported) {
|
|
native_image_source = entry;
|
|
}
|
|
break;
|
|
case RenderTargetOverlap::ExpandTarget:
|
|
supported = cached.kind == CachedImage::Kind::RenderTarget &&
|
|
native_image_source == nullptr;
|
|
if (supported) {
|
|
native_image_source = entry;
|
|
}
|
|
break;
|
|
case RenderTargetOverlap::RetireTarget:
|
|
supported = cached.kind == CachedImage::Kind::RenderTarget ||
|
|
cached.kind == CachedImage::Kind::DepthTarget;
|
|
if (supported && cached.kind == CachedImage::Kind::DepthTarget) {
|
|
recreated_depth_sources.push_back(entry);
|
|
}
|
|
break;
|
|
case RenderTargetOverlap::None:
|
|
case RenderTargetOverlap::Unsupported: break;
|
|
}
|
|
if (!supported) {
|
|
EXIT("TextureCache: unsupported render-target alias, requested=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " existing_kind=%u existing=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" gpu_modified=%d same_context=%d"
|
|
" sampled_format=%u extent=%ux%ux%u pitch=%u levels=%u base_level=%u"
|
|
" tile=%u type=%u base_array=%u\n",
|
|
info.address, info.size, static_cast<uint32_t>(cached.kind), cached.Address(),
|
|
cached.Size(), cached.gpu_modified, cached.ctx == ctx, cached.info.format,
|
|
cached.info.width, cached.info.height, cached.info.depth, cached.info.pitch,
|
|
cached.info.levels, cached.info.base_level, cached.info.tile, cached.info.type,
|
|
cached.info.base_array);
|
|
}
|
|
retire.push_back(&cached);
|
|
}
|
|
RequireRetirementIsolation(retire, "render target", info.address, info.size);
|
|
MaterializeImagesToGuestLocked(ctx, recreated_depth_sources);
|
|
for (const auto& cached: recreated_depth_sources) {
|
|
// The exact guest range owns the current bytes. Clear a stale buffer marker only after
|
|
// source-coherence validation so retirement can replace the obsolete Vulkan shape.
|
|
cached->buffer_modified = false;
|
|
}
|
|
RetireDepthMetadataLocked(retire);
|
|
RetireImages(retire, native_image_source.get());
|
|
ResolveImageMetadataOverlapsLocked(ctx, info.address, info.size);
|
|
auto cached = std::make_shared<CachedImage>();
|
|
cached->kind = CachedImage::Kind::RenderTarget;
|
|
cached->target = info;
|
|
cached->ctx = ctx;
|
|
cached->image = ImageOps::CreateRenderTarget(ctx, info);
|
|
const bool preserve_native = native_image_source != nullptr;
|
|
if (preserve_native) {
|
|
command->RetainResourceUntilFence(native_image_source);
|
|
if (native_image_source->kind == CachedImage::Kind::RenderTarget) {
|
|
const auto& old = native_image_source->target;
|
|
const auto tail_address = info.address + old.size;
|
|
const auto tail_size = info.size - old.size;
|
|
if (old.layers >= info.layers || tail_size == 0 ||
|
|
!IsCoherentGuestImageSource(target_source, info.address, info.size)) {
|
|
EXIT("TextureCache: invalid render-target expansion source, old_layers=%u "
|
|
"new_layers=%u old_size=0x%016" PRIx64 " new_size=0x%016" PRIx64 "\n",
|
|
old.layers, info.layers, old.size, info.size);
|
|
}
|
|
m_memory_tracker.ForEachUploadRange(
|
|
tail_address, tail_size, true, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
ImageOps::UploadRenderTargetLayers(
|
|
ctx, static_cast<RenderTextureVulkanImage*>(cached->image), info,
|
|
old.layers, info.layers - old.layers, false);
|
|
});
|
|
}
|
|
std::vector<ImageImageCopy> regions;
|
|
regions.reserve(static_cast<size_t>(native_image_source->image->layers) *
|
|
native_image_source->image->mip_levels);
|
|
AppendLayerCopies(regions, native_image_source->image, vk::ImageAspectFlagBits::eColor);
|
|
Transfer::CopyImage(command, regions, cached->image, RENDER_COLOR_IMAGE_LAYOUT);
|
|
} else {
|
|
if (info.samples > 1) {
|
|
if (!IsCoherentGuestImageSource(target_source, info.address, info.size) ||
|
|
!GuestRangeIsZero(info.address, info.size)) {
|
|
EXIT("TextureCache: nonzero multisampled render-target upload is unsupported, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 " samples=%u\n",
|
|
info.address, info.size, info.samples);
|
|
}
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, false, [](uint64_t, uint64_t) noexcept {},
|
|
[]() noexcept {});
|
|
static_cast<RenderTextureVulkanImage*>(cached->image)->initial_clear_pending = true;
|
|
} else {
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, false, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
ImageOps::UploadRenderTarget(
|
|
ctx, static_cast<RenderTextureVulkanImage*>(cached->image), info, false);
|
|
});
|
|
}
|
|
}
|
|
cached->gpu_modified = preserve_native;
|
|
return static_cast<RenderTextureVulkanImage*>(PublishImage(command, std::move(cached)));
|
|
}
|
|
|
|
DepthStencilVulkanImage* TextureCache::FindDepthTarget(CommandBuffer* command, GraphicContext* ctx,
|
|
const DepthTargetInfo& info) {
|
|
const bool has_stencil = info.stencil_address != 0 || info.stencil_size != 0;
|
|
const bool has_htile = info.htile_address != 0 || info.htile_size != 0;
|
|
const bool valid_samples =
|
|
info.samples == 1 || info.samples == 2 || info.samples == 4 || info.samples == 8;
|
|
if (info.address == 0 || info.size == 0 || info.address >= TRACKER_ADDRESS_SIZE ||
|
|
info.size > TRACKER_ADDRESS_SIZE - info.address || (info.address & 0xffffu) != 0 ||
|
|
info.width == 0 || info.height == 0 || info.pitch < info.width || info.layers == 0 ||
|
|
!valid_samples || (info.samples > 1 && info.layers != 1) || info.size % info.layers != 0 ||
|
|
info.size > UINT32_MAX || info.stencil_size > UINT32_MAX ||
|
|
info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kDepth) ||
|
|
(has_stencil &&
|
|
(info.stencil_address == 0 || info.stencil_size == 0 ||
|
|
info.stencil_size % info.layers != 0 || info.stencil_address >= TRACKER_ADDRESS_SIZE ||
|
|
info.stencil_size > TRACKER_ADDRESS_SIZE - info.stencil_address ||
|
|
(info.stencil_address & 0xffffu) != 0 ||
|
|
ImagePageRangesOverlap(info.address, info.size, info.stencil_address,
|
|
info.stencil_size))) ||
|
|
((info.htile_address == 0) != (info.htile_size == 0)) ||
|
|
(has_htile &&
|
|
(info.htile_size % info.layers != 0 || info.htile_address >= TRACKER_ADDRESS_SIZE ||
|
|
info.htile_size > TRACKER_ADDRESS_SIZE - info.htile_address ||
|
|
(info.htile_address & 0x7fffu) != 0 ||
|
|
ImagePageRangesOverlap(info.address, info.size, info.htile_address, info.htile_size) ||
|
|
(has_stencil && ImagePageRangesOverlap(info.stencil_address, info.stencil_size,
|
|
info.htile_address, info.htile_size)))) ||
|
|
(info.stencil_htile_compressed && (!has_stencil || !has_htile)) ||
|
|
!IsSupportedDepthTargetFormat(info)) {
|
|
EXIT("TextureCache: unsupported depth target, command=%p ctx=%p depth=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " stencil=0x%016" PRIx64 "+0x%016" PRIx64 " htile=0x%016" PRIx64
|
|
"+0x%016" PRIx64
|
|
" extent=%ux%u pitch=%u samples=%u tile=%u format=%d guest_format=%u bpe=%u\n",
|
|
static_cast<const void*>(command), static_cast<const void*>(ctx), info.address,
|
|
info.size, info.stencil_address, info.stencil_size, info.htile_address,
|
|
info.htile_size, info.width, info.height, info.pitch, info.samples, info.tile_mode,
|
|
static_cast<int>(info.format), info.guest_format, info.bytes_per_element);
|
|
}
|
|
const auto* depth_policy = FindGuestDepthFormatPolicy(info.guest_format);
|
|
TileSizeAlign expected_depth {};
|
|
TileSizeAlign expected_stencil {};
|
|
TileSizeAlign expected_htile {};
|
|
const auto encoded_samples = static_cast<uint32_t>(std::countr_zero(info.samples));
|
|
if (depth_policy == nullptr || depth_policy->bytes_per_element != info.bytes_per_element ||
|
|
info.pitch != TileGetDepthPitch(info.width, info.bytes_per_element, encoded_samples) ||
|
|
!TileGetDepthSize(
|
|
info.width, info.height, 0, Prospero::GpuEnumValue(depth_policy->depth_format),
|
|
Prospero::GpuEnumValue(has_stencil ? Prospero::StencilFormat::k8UInt
|
|
: Prospero::StencilFormat::kInvalid),
|
|
has_htile, &expected_stencil, &expected_htile, &expected_depth, encoded_samples) ||
|
|
expected_depth.size > UINT64_MAX / info.layers ||
|
|
static_cast<uint64_t>(expected_depth.size) * info.layers != info.size ||
|
|
(has_stencil &&
|
|
(expected_stencil.size > UINT64_MAX / info.layers ||
|
|
static_cast<uint64_t>(expected_stencil.size) * info.layers != info.stencil_size)) ||
|
|
(has_htile &&
|
|
(expected_htile.size > UINT64_MAX / info.layers ||
|
|
static_cast<uint64_t>(expected_htile.size) * info.layers != info.htile_size))) {
|
|
EXIT("TextureCache: invalid depth-target block layout, depth=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" stencil=0x%016" PRIx64 "+0x%016" PRIx64 " htile=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" extent=%ux%u pitch=%u samples=%u\n",
|
|
info.address, info.size, info.stencil_address, info.stencil_size, info.htile_address,
|
|
info.htile_size, info.width, info.height, info.pitch, info.samples);
|
|
}
|
|
const auto rows = static_cast<uint64_t>(info.height - 1);
|
|
if (rows > (UINT64_MAX - info.width) / info.pitch) {
|
|
EXIT("TextureCache: depth-target element count overflow, extent=%ux%u pitch=%u\n",
|
|
info.width, info.height, info.pitch);
|
|
}
|
|
const auto elements = rows * info.pitch + info.width;
|
|
if (elements > UINT64_MAX / info.bytes_per_element / info.samples ||
|
|
elements * info.bytes_per_element * info.samples > UINT64_MAX / info.layers ||
|
|
info.size < elements * info.bytes_per_element * info.samples * info.layers) {
|
|
EXIT("TextureCache: depth storage is too small, size=0x%016" PRIx64
|
|
" elements=0x%016" PRIx64 " bpe=%u\n",
|
|
info.size, elements, info.bytes_per_element);
|
|
}
|
|
if (has_stencil && (elements > UINT64_MAX / info.samples / info.layers ||
|
|
info.stencil_size < elements * info.samples * info.layers)) {
|
|
EXIT("TextureCache: stencil storage is too small, size=0x%016" PRIx64
|
|
" elements=0x%016" PRIx64 "\n",
|
|
info.stencil_size, elements);
|
|
}
|
|
if (has_stencil && !info.stencil_load_clear && !CanLoadRawStencilPlane(info)) {
|
|
EXIT("TextureCache: HTile-compressed stencil load is unsupported, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " htile=0x%016" PRIx64 "+0x%016" PRIx64 "\n",
|
|
info.stencil_address, info.stencil_size, info.htile_address, info.htile_size);
|
|
}
|
|
if (has_htile) {
|
|
RegisterMeta(ctx, info.htile_address, info.htile_size, info.layers);
|
|
}
|
|
std::lock_guard transaction(m_resource_mutex);
|
|
// BufferCache treats an untracked guest range as CPU-current. That is useful when
|
|
// uploading from guest memory, but it is not evidence that a clean native image is
|
|
// stale. Only a genuinely overlapping buffer participates in transition selection.
|
|
const bool depth_buffer_overlap = m_buffer_cache.HasPageOverlap(info.address, info.size);
|
|
const auto depth_source = m_buffer_cache.ObtainBufferForImage(info.address, info.size);
|
|
const auto stencil_source =
|
|
has_stencil ? m_buffer_cache.ObtainBufferForImage(info.stencil_address, info.stencil_size)
|
|
: BufferImageCopySource {};
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
ResolveImageMetadataOverlapsLocked(ctx, info.address, info.size);
|
|
if (has_stencil) {
|
|
ResolveImageMetadataOverlapsLocked(ctx, info.stencil_address, info.stencil_size);
|
|
}
|
|
std::shared_ptr<CachedImage> match;
|
|
for (auto& cached: m_images) {
|
|
if (cached->kind != CachedImage::Kind::DepthTarget || cached->ctx != ctx ||
|
|
(!Equal(info, cached->depth) && !IsCompatibleDepthTargetBacking(cached->depth, info))) {
|
|
continue;
|
|
}
|
|
match = cached;
|
|
}
|
|
if (match != nullptr) {
|
|
const bool depth_cpu_modified =
|
|
m_memory_tracker.IsRegionCpuModified(info.address, info.size);
|
|
const bool stencil_cpu_modified =
|
|
has_stencil &&
|
|
m_memory_tracker.IsRegionCpuModified(info.stencil_address, info.stencil_size);
|
|
if (RequiresMultisampleDepthRefresh(info, match->buffer_modified, depth_cpu_modified,
|
|
stencil_cpu_modified)) {
|
|
EXIT("TextureCache: multisampled depth-target refresh is unsupported, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64
|
|
" samples=%u access=%d/%d clear=%d/%d buffer=%d cpu=%d/%d\n",
|
|
info.address, info.size, info.samples, info.depth_access, info.stencil_access,
|
|
info.depth_load_clear, info.stencil_load_clear, match->buffer_modified,
|
|
depth_cpu_modified, stencil_cpu_modified);
|
|
}
|
|
if (!CanLoadStencilAttachment(info, match->stencil_initialized)) {
|
|
EXIT("TextureCache: stencil load requires initialized contents, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 "\n",
|
|
info.stencil_address, info.stencil_size);
|
|
}
|
|
if (has_stencil && info.stencil_load_clear) {
|
|
match->stencil_initialized = true;
|
|
}
|
|
if (match->buffer_modified) {
|
|
if (match->gpu_modified || !depth_buffer_overlap ||
|
|
!IsCoherentGuestImageSource(depth_source, info.address, info.size)) {
|
|
EXIT("TextureCache: depth-target refresh has invalid buffer ownership, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64
|
|
" gpu_modified=%d overlap=%d source_current=%d\n",
|
|
info.address, info.size, match->gpu_modified, depth_buffer_overlap,
|
|
depth_source.cpu_current);
|
|
}
|
|
m_memory_tracker.MarkRegionAsCpuModified(info.address, info.size);
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, true, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
m_tiler.DetileImage(ctx, static_cast<DepthStencilVulkanImage*>(match->image),
|
|
info, depth_source, true);
|
|
});
|
|
match->buffer_modified = false;
|
|
match->gpu_modified = true;
|
|
} else if (!match->gpu_modified && depth_source.cpu_dirty) {
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, false, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
m_tiler.DetileImage(ctx, static_cast<DepthStencilVulkanImage*>(match->image),
|
|
info, depth_source, true);
|
|
});
|
|
}
|
|
if (!match->gpu_modified && has_stencil && stencil_source.cpu_dirty) {
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.stencil_address, info.stencil_size, false, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
if (!info.stencil_load_clear) {
|
|
m_tiler.DetileStencil(ctx,
|
|
static_cast<DepthStencilVulkanImage*>(match->image),
|
|
info, stencil_source, true);
|
|
match->stencil_initialized = true;
|
|
}
|
|
});
|
|
}
|
|
command->RetainResourceUntilFence(match);
|
|
return static_cast<DepthStencilVulkanImage*>(match->image);
|
|
}
|
|
std::vector<CachedImage*> retire;
|
|
std::shared_ptr<CachedImage> sampled_depth_source;
|
|
std::shared_ptr<CachedImage> native_depth_source;
|
|
std::shared_ptr<CachedImage> discarded_depth_source;
|
|
std::shared_ptr<CachedImage> retired_storage_source;
|
|
std::vector<std::shared_ptr<CachedImage>> recreated_target_sources;
|
|
for (const auto& entry: m_images) {
|
|
auto& cached = *entry;
|
|
const bool overlaps =
|
|
cached.OverlapsRange(info.address, info.size, true) ||
|
|
(has_stencil && cached.OverlapsRange(info.stencil_address, info.stencil_size, true));
|
|
if (!overlaps) {
|
|
continue;
|
|
}
|
|
DepthOverlap overlap = DepthOverlap::Unsupported;
|
|
switch (cached.kind) {
|
|
case CachedImage::Kind::Texture: {
|
|
const auto native_overlap =
|
|
cached.ctx == ctx ? ClassifyDepthOverlap(cached.info, cached.gpu_modified, info)
|
|
: DepthOverlap::Unsupported;
|
|
const bool overlaps_depth = ImageRangeOverlaps(
|
|
cached.info.address, cached.info.size, info.address, info.size);
|
|
const bool overlaps_stencil =
|
|
has_stencil && ImageRangeOverlaps(cached.info.address, cached.info.size,
|
|
info.stencil_address, info.stencil_size);
|
|
const bool guest_source_current =
|
|
(!overlaps_depth || info.depth_load_clear ||
|
|
IsCoherentGuestImageSource(depth_source, info.address, info.size)) &&
|
|
(!overlaps_stencil || info.stencil_load_clear ||
|
|
IsCoherentGuestImageSource(stencil_source, info.stencil_address,
|
|
info.stencil_size));
|
|
overlap = native_overlap;
|
|
if (overlap == DepthOverlap::Unsupported &&
|
|
CanRetireGuestCurrentSampledForDepth(
|
|
cached.info, info, cached.gpu_modified, cached.buffer_modified,
|
|
cached.info.IsCpuDirty(),
|
|
m_memory_tracker.IsRegionGpuModified(cached.info.address, cached.info.size),
|
|
cached.ctx == ctx, guest_source_current)) {
|
|
overlap = DepthOverlap::RetireSampled;
|
|
}
|
|
if (overlap == DepthOverlap::RetireSampled && !info.depth_load_clear &&
|
|
native_overlap == DepthOverlap::RetireSampled &&
|
|
sampled_depth_source == nullptr) {
|
|
sampled_depth_source = entry;
|
|
}
|
|
} break;
|
|
case CachedImage::Kind::DepthTarget:
|
|
overlap =
|
|
ClassifyDepthTargetOverlap(cached.depth, cached.gpu_modified,
|
|
cached.buffer_modified, cached.ctx == ctx, info);
|
|
break;
|
|
case CachedImage::Kind::StorageTexture:
|
|
overlap = ClassifyStorageDepthOverlap(
|
|
cached.info, cached.gpu_modified, cached.buffer_modified,
|
|
cached.info.IsCpuDirty() ||
|
|
m_memory_tracker.IsRegionCpuModified(cached.info.address, cached.info.size),
|
|
m_memory_tracker.IsRegionGpuModified(cached.info.address, cached.info.size),
|
|
cached.ctx == ctx, info);
|
|
break;
|
|
case CachedImage::Kind::RenderTarget: {
|
|
const bool overlaps_depth = ImageRangeOverlaps(
|
|
cached.target.address, cached.target.size, info.address, info.size);
|
|
const bool overlaps_stencil =
|
|
has_stencil && ImageRangeOverlaps(cached.target.address, cached.target.size,
|
|
info.stencil_address, info.stencil_size);
|
|
const bool guest_source_current =
|
|
(!overlaps_depth || info.depth_load_clear ||
|
|
IsCoherentGuestImageSource(depth_source, info.address, info.size)) &&
|
|
(!overlaps_stencil || info.stencil_load_clear ||
|
|
IsCoherentGuestImageSource(stencil_source, info.stencil_address,
|
|
info.stencil_size));
|
|
overlap = CanRecreateRenderTargetForDepth(
|
|
cached.target, cached.gpu_modified, cached.buffer_modified,
|
|
m_memory_tracker.IsRegionGpuModified(cached.target.address,
|
|
cached.target.size),
|
|
cached.ctx == ctx, guest_source_current, info)
|
|
? DepthOverlap::RecreateTarget
|
|
: DepthOverlap::Unsupported;
|
|
} break;
|
|
case CachedImage::Kind::VideoOut: break;
|
|
}
|
|
bool supported = false;
|
|
switch (overlap) {
|
|
case DepthOverlap::RetireSampled:
|
|
supported = cached.kind == CachedImage::Kind::Texture;
|
|
break;
|
|
case DepthOverlap::RetireStorage:
|
|
supported = cached.kind == CachedImage::Kind::StorageTexture &&
|
|
retired_storage_source == nullptr && native_depth_source == nullptr &&
|
|
discarded_depth_source == nullptr && recreated_target_sources.empty();
|
|
if (supported) {
|
|
retired_storage_source = entry;
|
|
}
|
|
break;
|
|
case DepthOverlap::ExpandTarget:
|
|
supported = cached.kind == CachedImage::Kind::DepthTarget &&
|
|
native_depth_source == nullptr && retired_storage_source == nullptr &&
|
|
recreated_target_sources.empty();
|
|
if (supported) {
|
|
native_depth_source = entry;
|
|
}
|
|
break;
|
|
case DepthOverlap::DiscardTarget:
|
|
supported = cached.kind == CachedImage::Kind::DepthTarget &&
|
|
discarded_depth_source == nullptr && native_depth_source == nullptr;
|
|
supported = supported && retired_storage_source == nullptr &&
|
|
recreated_target_sources.empty();
|
|
if (supported) {
|
|
discarded_depth_source = entry;
|
|
}
|
|
break;
|
|
case DepthOverlap::RecreateTarget:
|
|
supported = (cached.kind == CachedImage::Kind::DepthTarget ||
|
|
cached.kind == CachedImage::Kind::RenderTarget) &&
|
|
native_depth_source == nullptr && discarded_depth_source == nullptr &&
|
|
retired_storage_source == nullptr;
|
|
if (supported) {
|
|
recreated_target_sources.push_back(entry);
|
|
}
|
|
break;
|
|
case DepthOverlap::None:
|
|
case DepthOverlap::Unsupported: break;
|
|
}
|
|
if (!supported) {
|
|
EXIT("TextureCache: unsupported depth-target alias, depth=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " existing_kind=%u existing=0x%016" PRIx64 "+0x%016" PRIx64 "\n",
|
|
info.address, info.size, static_cast<uint32_t>(cached.kind), cached.Address(),
|
|
cached.Size());
|
|
}
|
|
retire.push_back(&cached);
|
|
}
|
|
RequireRetirementIsolation(retire, "depth target", info.address, info.size);
|
|
MaterializeImagesToGuestLocked(ctx, recreated_target_sources);
|
|
if (retired_storage_source != nullptr) {
|
|
if (retired_storage_source->gpu_modified) {
|
|
m_memory_tracker.UnmarkRegionAsGpuModified(retired_storage_source->info.address,
|
|
retired_storage_source->info.size);
|
|
retired_storage_source->gpu_modified = false;
|
|
}
|
|
}
|
|
const auto* transition_source =
|
|
native_depth_source != nullptr
|
|
? native_depth_source.get()
|
|
: (discarded_depth_source != nullptr ? discarded_depth_source.get() : nullptr);
|
|
RetireDepthMetadataLocked(retire, info.htile_address);
|
|
RetireImages(retire, transition_source);
|
|
const bool coherent_guest_stencil =
|
|
has_stencil &&
|
|
IsCoherentGuestImageSource(stencil_source, info.stencil_address, info.stencil_size);
|
|
// Native expansion must preserve old layers. Guest data initializes only a wholly new image.
|
|
const bool stencil_contents_available = native_depth_source != nullptr
|
|
? native_depth_source->stencil_initialized
|
|
: coherent_guest_stencil;
|
|
if (!CanLoadStencilAttachment(info, stencil_contents_available)) {
|
|
EXIT("TextureCache: new stencil target requires a clear before stencil access, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
|
|
info.stencil_address, info.stencil_size);
|
|
}
|
|
if (info.samples > 1 && (native_depth_source != nullptr || sampled_depth_source != nullptr ||
|
|
retired_storage_source != nullptr)) {
|
|
EXIT("TextureCache: multisampled depth native transition is unsupported, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 " samples=%u\n",
|
|
info.address, info.size, info.samples);
|
|
}
|
|
const bool initial_depth_clear = info.samples > 1 && !info.depth_load_clear;
|
|
const bool initial_stencil_clear = info.samples > 1 && has_stencil && !info.stencil_load_clear;
|
|
if (initial_depth_clear &&
|
|
(!IsCoherentGuestImageSource(depth_source, info.address, info.size) ||
|
|
!GuestRangeIsZero(info.address, info.size))) {
|
|
EXIT("TextureCache: nonzero multisampled depth upload is unsupported, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 " samples=%u\n",
|
|
info.address, info.size, info.samples);
|
|
}
|
|
if (initial_stencil_clear &&
|
|
(!coherent_guest_stencil || !GuestRangeIsZero(info.stencil_address, info.stencil_size))) {
|
|
EXIT("TextureCache: nonzero multisampled stencil upload is unsupported, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 " samples=%u\n",
|
|
info.stencil_address, info.stencil_size, info.samples);
|
|
}
|
|
auto cached = std::make_shared<CachedImage>();
|
|
cached->kind = CachedImage::Kind::DepthTarget;
|
|
cached->depth = info;
|
|
cached->ctx = ctx;
|
|
cached->stencil_initialized = !has_stencil || info.stencil_load_clear || initial_stencil_clear;
|
|
cached->image = ImageOps::CreateDepthTarget(ctx, info);
|
|
auto* depth_image = static_cast<DepthStencilVulkanImage*>(cached->image);
|
|
depth_image->initial_depth_clear_pending = initial_depth_clear;
|
|
depth_image->initial_stencil_clear_pending = initial_stencil_clear;
|
|
if (discarded_depth_source != nullptr) {
|
|
command->RetainResourceUntilFence(discarded_depth_source);
|
|
}
|
|
if (retired_storage_source != nullptr) {
|
|
command->RetainResourceUntilFence(retired_storage_source);
|
|
}
|
|
if (native_depth_source != nullptr) {
|
|
const auto& old = native_depth_source->depth;
|
|
if (old.layers >= info.layers || native_depth_source->image->layers != old.layers) {
|
|
EXIT("TextureCache: invalid depth expansion source, old_layers=%u new_layers=%u\n",
|
|
old.layers, info.layers);
|
|
}
|
|
command->RetainResourceUntilFence(native_depth_source);
|
|
const auto tail = SelectDepthLayers(info, old.layers, info.layers - old.layers);
|
|
const auto depth_tail = SelectSourceRange(depth_source, tail.address, tail.size);
|
|
if (!info.depth_load_clear &&
|
|
!IsCoherentGuestImageSource(depth_tail, tail.address, tail.size)) {
|
|
EXIT("TextureCache: depth expansion tail is not CPU-current\n");
|
|
}
|
|
m_memory_tracker.ForEachUploadRange(
|
|
tail.address, tail.size, true, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
if (!info.depth_load_clear) {
|
|
m_tiler.DetileImage(ctx, static_cast<DepthStencilVulkanImage*>(cached->image),
|
|
tail, depth_tail, false, old.layers);
|
|
}
|
|
});
|
|
if (has_stencil) {
|
|
const auto stencil_tail =
|
|
SelectSourceRange(stencil_source, tail.stencil_address, tail.stencil_size);
|
|
if (!info.stencil_load_clear &&
|
|
!IsCoherentGuestImageSource(stencil_tail, tail.stencil_address,
|
|
tail.stencil_size)) {
|
|
EXIT("TextureCache: stencil expansion tail is not CPU-current\n");
|
|
}
|
|
m_memory_tracker.ForEachUploadRange(
|
|
tail.stencil_address, tail.stencil_size, true, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
if (!info.stencil_load_clear) {
|
|
m_tiler.DetileStencil(ctx,
|
|
static_cast<DepthStencilVulkanImage*>(cached->image),
|
|
tail, stencil_tail, false, old.layers);
|
|
}
|
|
});
|
|
cached->stencil_initialized = true;
|
|
}
|
|
std::vector<ImageImageCopy> regions;
|
|
regions.reserve(native_depth_source->image->layers * (has_stencil ? 2u : 1u));
|
|
AppendLayerCopies(regions, native_depth_source->image, vk::ImageAspectFlagBits::eDepth);
|
|
if (has_stencil) {
|
|
AppendLayerCopies(regions, native_depth_source->image,
|
|
vk::ImageAspectFlagBits::eStencil);
|
|
}
|
|
Transfer::CopyImage(command, regions, cached->image,
|
|
vk::ImageLayout::eDepthStencilAttachmentOptimal);
|
|
cached->gpu_modified = true;
|
|
} else {
|
|
if (info.samples > 1) {
|
|
// Multisample guest planes cannot be copied through Vulkan buffer-image commands.
|
|
// Their verified-zero initial contents are materialized by the first render pass.
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, false, [](uint64_t, uint64_t) noexcept {},
|
|
[]() noexcept {});
|
|
if (has_stencil) {
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.stencil_address, info.stencil_size, false,
|
|
[](uint64_t, uint64_t) noexcept {}, []() noexcept {});
|
|
}
|
|
return static_cast<DepthStencilVulkanImage*>(PublishImage(command, std::move(cached)));
|
|
}
|
|
if (sampled_depth_source != nullptr &&
|
|
(sampled_depth_source->image->type != VulkanImageType::Texture ||
|
|
sampled_depth_source->image->format != VulkanFormat(info.guest_format) ||
|
|
sampled_depth_source->image->extent.width != info.width ||
|
|
sampled_depth_source->image->extent.height != info.height)) {
|
|
EXIT("TextureCache: sampled-depth native source is inconsistent\n");
|
|
}
|
|
const auto transition_source = SelectDepthTransitionSource(
|
|
info.depth_load_clear, sampled_depth_source != nullptr,
|
|
sampled_depth_source != nullptr && sampled_depth_source->info.IsCpuDirty(),
|
|
sampled_depth_source != nullptr && sampled_depth_source->buffer_modified,
|
|
depth_buffer_overlap, depth_source.cpu_dirty);
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, false, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
switch (transition_source) {
|
|
case DepthTransitionSource::None: break;
|
|
case DepthTransitionSource::Guest:
|
|
m_tiler.DetileImage(ctx,
|
|
static_cast<DepthStencilVulkanImage*>(cached->image),
|
|
info, depth_source, false);
|
|
break;
|
|
case DepthTransitionSource::Native:
|
|
command->RetainResourceUntilFence(sampled_depth_source);
|
|
Transfer::CopyImageViaBuffer(
|
|
command, ctx, sampled_depth_source->image,
|
|
vk::ImageAspectFlagBits::eColor, cached->image,
|
|
vk::ImageAspectFlagBits::eDepth, info.bytes_per_element,
|
|
vk::ImageLayout::eDepthStencilAttachmentOptimal);
|
|
break;
|
|
}
|
|
});
|
|
if (has_stencil) {
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.stencil_address, info.stencil_size, false, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
if (!info.stencil_load_clear) {
|
|
m_tiler.DetileStencil(ctx,
|
|
static_cast<DepthStencilVulkanImage*>(cached->image),
|
|
info, stencil_source, false);
|
|
}
|
|
});
|
|
cached->stencil_initialized = true;
|
|
}
|
|
}
|
|
return static_cast<DepthStencilVulkanImage*>(PublishImage(command, std::move(cached)));
|
|
}
|
|
|
|
std::vector<VideoOutVulkanImage*>
|
|
TextureCache::RegisterVideoOutSurfaces(GraphicContext* ctx,
|
|
const std::vector<VideoOutInfo>& infos) {
|
|
if (infos.empty()) {
|
|
EXIT("TextureCache: video-out registration requires surfaces\n");
|
|
}
|
|
for (const auto& info: infos) {
|
|
ImageOps::ValidateVideoOut(ctx, info);
|
|
}
|
|
for (size_t i = 0; i < infos.size(); i++) {
|
|
for (size_t j = i + 1; j < infos.size(); j++) {
|
|
if (ImagePageRangesOverlap(infos[i].address, infos[i].size, infos[j].address,
|
|
infos[j].size)) {
|
|
EXIT("TextureCache: video-out surfaces share pages, first=%zu second=%zu\n", i, j);
|
|
}
|
|
}
|
|
}
|
|
std::lock_guard transaction(m_resource_mutex);
|
|
for (const auto& info: infos) {
|
|
if (m_buffer_cache.HasPageOverlap(info.address, info.size)) {
|
|
EXIT("TextureCache: video-out surface aliases buffer pages, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 "\n",
|
|
info.address, info.size);
|
|
}
|
|
}
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
for (const auto& info: infos) {
|
|
ResolveImageMetadataOverlapsLocked(ctx, info.address, info.size);
|
|
for (const auto& cached: m_images) {
|
|
if (cached->OverlapsRange(info.address, info.size, true)) {
|
|
EXIT(
|
|
"TextureCache: video-out surface aliases cached image pages, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " existing_kind=%u\n",
|
|
info.address, info.size, static_cast<uint32_t>(cached->kind));
|
|
}
|
|
}
|
|
}
|
|
m_images.reserve(m_images.size() + infos.size());
|
|
std::vector<VideoOutVulkanImage*> result;
|
|
result.reserve(infos.size());
|
|
for (const auto& info: infos) {
|
|
auto cached = std::make_shared<CachedImage>();
|
|
cached->kind = CachedImage::Kind::VideoOut;
|
|
cached->video_out = info;
|
|
cached->ctx = ctx;
|
|
cached->image = ImageOps::CreateVideoOut(ctx, info);
|
|
if (info.compression == VideoOutCompression::Uncompressed) {
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, false, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept {
|
|
ImageOps::UploadVideoOut(ctx, static_cast<VideoOutVulkanImage*>(cached->image),
|
|
info, false);
|
|
});
|
|
} else {
|
|
// A compressed guest surface cannot be decoded without its DCC metadata. Establish the
|
|
// normal tracked range, but leave the shared native image to be initialized by a GPU
|
|
// render/clear before any sampled or presentation read.
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, false, [](uint64_t, uint64_t) noexcept {},
|
|
[]() noexcept {});
|
|
}
|
|
result.push_back(static_cast<VideoOutVulkanImage*>(cached->image));
|
|
m_images.push_back(std::move(cached));
|
|
RegisterImageLocked(*m_images.back());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void TextureCache::RefreshVideoOut(VideoOutVulkanImage* image, bool render_target) {
|
|
if (image == nullptr) {
|
|
EXIT("TextureCache: invalid video-out refresh, image=%p\n",
|
|
static_cast<const void*>(image));
|
|
}
|
|
std::lock_guard transaction(m_resource_mutex);
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
const auto it = std::find_if(m_images.begin(), m_images.end(),
|
|
[image](const auto& cached) { return cached->image == image; });
|
|
if (it == m_images.end() || (*it)->kind != CachedImage::Kind::VideoOut) {
|
|
EXIT("TextureCache: video-out image is not registered, image=%p\n",
|
|
static_cast<const void*>(image));
|
|
}
|
|
auto& cached = **it;
|
|
if (cached.gpu_modified) {
|
|
return;
|
|
}
|
|
const auto& info = cached.video_out;
|
|
const bool image_dirty = m_memory_tracker.IsRegionCpuModified(info.address, info.size);
|
|
const bool buffer_overlap = m_buffer_cache.HasPageOverlap(info.address, info.size);
|
|
const bool buffer_dirty =
|
|
cached.buffer_modified ||
|
|
(buffer_overlap && (m_buffer_cache.IsRegionCpuModified(info.address, info.size) ||
|
|
m_buffer_cache.IsRegionGpuModified(info.address, info.size)));
|
|
if (!image_dirty && !buffer_dirty) {
|
|
if (info.compression == VideoOutCompression::Uncompressed ||
|
|
CanUseVideoOutNativeWithoutUpload(info.compression, render_target, false, false)) {
|
|
return;
|
|
}
|
|
EXIT("TextureCache: compressed video-out read requires native GPU contents, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
|
|
info.address, info.size);
|
|
}
|
|
if (info.compression != VideoOutCompression::Uncompressed) {
|
|
EXIT("TextureCache: compressed video-out guest refresh is unsupported, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64
|
|
" image_dirty=%d buffer_dirty=%d render_target=%d\n",
|
|
info.address, info.size, image_dirty, buffer_dirty, render_target);
|
|
}
|
|
if (buffer_dirty) {
|
|
const auto source = m_buffer_cache.ObtainBufferForImage(info.address, info.size);
|
|
if (!IsCoherentGuestImageSource(source, info.address, info.size)) {
|
|
EXIT("TextureCache: invalid video-out source, addr=0x%016" PRIx64 " size=0x%016" PRIx64
|
|
" buffer=%p current=%d source=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" buffer_modified=%d\n",
|
|
info.address, info.size, static_cast<const void*>(source.buffer),
|
|
source.cpu_current, source.address, source.size, cached.buffer_modified);
|
|
}
|
|
cached.buffer_modified = false;
|
|
}
|
|
m_memory_tracker.ForEachUploadRange(
|
|
info.address, info.size, false, [](uint64_t, uint64_t) noexcept {},
|
|
[&]() noexcept { ImageOps::UploadVideoOut(cached.ctx, image, info, true); });
|
|
}
|
|
|
|
void TextureCache::UnregisterVideoOutSurfaces(const std::vector<VideoOutVulkanImage*>& images) {
|
|
if (images.empty()) {
|
|
EXIT("TextureCache: video-out unregistration requires surfaces\n");
|
|
}
|
|
std::lock_guard transaction(m_resource_mutex);
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
std::vector<CachedImage*> selected;
|
|
selected.reserve(images.size());
|
|
for (size_t i = 0; i < images.size(); i++) {
|
|
auto* image = images[i];
|
|
if (image == nullptr ||
|
|
std::find(images.begin(), images.begin() + i, image) != images.begin() + i) {
|
|
EXIT("TextureCache: invalid or duplicate video-out image at index %zu, image=%p\n", i,
|
|
static_cast<const void*>(image));
|
|
}
|
|
auto it = std::find_if(m_images.begin(), m_images.end(),
|
|
[image](const auto& cached) { return cached->image == image; });
|
|
if (it == m_images.end() || (*it)->kind != CachedImage::Kind::VideoOut) {
|
|
EXIT("TextureCache: video-out image is not registered, index=%zu image=%p\n", i,
|
|
static_cast<const void*>(image));
|
|
}
|
|
selected.push_back(it->get());
|
|
}
|
|
GraphicContext* ctx = selected.front()->ctx;
|
|
for (auto* cached: selected) {
|
|
if (cached->ctx != ctx) {
|
|
EXIT("TextureCache: video-out surfaces span graphics contexts, expected=%p actual=%p\n",
|
|
static_cast<const void*>(ctx), static_cast<const void*>(cached->ctx));
|
|
}
|
|
if (cached->buffer_modified) {
|
|
EXIT("TextureCache: cannot unregister a buffer-dirty video-out surface, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
|
|
cached->Address(), cached->Size());
|
|
}
|
|
}
|
|
Transfer::WaitForGraphicsIdle(ctx);
|
|
for (auto* cached: selected) {
|
|
if (cached->gpu_modified) {
|
|
m_memory_tracker.UnmarkRegionAsGpuModified(cached->Address(), cached->Size());
|
|
cached->gpu_modified = false;
|
|
}
|
|
UnregisterImageLocked(*cached, true);
|
|
}
|
|
for (auto* image: images) {
|
|
auto it = std::find_if(m_images.begin(), m_images.end(),
|
|
[image](const auto& cached) { return cached->image == image; });
|
|
m_images.erase(it);
|
|
}
|
|
}
|
|
|
|
bool TextureCache::ClearImageFromBuffer(CommandBuffer* command, uint64_t vaddr, uint64_t size,
|
|
uint32_t packed_clear) {
|
|
if (command == nullptr || command->IsInvalid() || vaddr == 0 || size == 0 ||
|
|
vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) {
|
|
EXIT("TextureCache: invalid compute image clear, command=%p addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 "\n",
|
|
static_cast<const void*>(command), vaddr, size);
|
|
}
|
|
m_buffer_cache.ValidateGpuAccess(vaddr, size, false, true);
|
|
std::lock_guard transaction(m_resource_mutex);
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
enum class ClearAspect : uint8_t { None, Color, Depth, Stencil };
|
|
auto classify = [vaddr, size](const CachedImage& cached) {
|
|
const bool color = cached.kind == CachedImage::Kind::VideoOut ||
|
|
cached.kind == CachedImage::Kind::RenderTarget;
|
|
if (color && cached.Address() == vaddr && cached.Size() == size) {
|
|
return ClearAspect::Color;
|
|
}
|
|
if (cached.kind != CachedImage::Kind::DepthTarget) {
|
|
return ClearAspect::None;
|
|
}
|
|
if (CanNativeClearDepthFromBuffer(cached.depth, vaddr, size)) {
|
|
return ClearAspect::Depth;
|
|
}
|
|
if (cached.RangeCount() == 2 && cached.Address(1) == vaddr && cached.Size(1) == size) {
|
|
return ClearAspect::Stencil;
|
|
}
|
|
return ClearAspect::None;
|
|
};
|
|
|
|
std::shared_ptr<CachedImage> match;
|
|
ClearAspect aspect = ClearAspect::None;
|
|
bool incompatible = false;
|
|
uint32_t matches = 0;
|
|
for (const auto& cached: m_images) {
|
|
if (!cached->OverlapsRange(vaddr, size, true)) {
|
|
continue;
|
|
}
|
|
const auto candidate = classify(*cached);
|
|
if (candidate == ClearAspect::None) {
|
|
incompatible = true;
|
|
continue;
|
|
}
|
|
if (aspect == ClearAspect::None) {
|
|
aspect = candidate;
|
|
match = cached;
|
|
} else if (candidate != aspect) {
|
|
incompatible = true;
|
|
continue;
|
|
}
|
|
matches++;
|
|
}
|
|
if (matches > 1) {
|
|
EXIT("TextureCache: compute image clear has ambiguous exact aliases, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 " aspect=%u matches=%u\n",
|
|
vaddr, size, static_cast<uint32_t>(aspect), matches);
|
|
}
|
|
if (incompatible || match == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
float depth_clear = 0.0f;
|
|
if (aspect == ClearAspect::Depth &&
|
|
!DecodePackedDepthClear(match->image->format, packed_clear, &depth_clear)) {
|
|
return false;
|
|
}
|
|
uint8_t stencil_clear = 0;
|
|
if (aspect == ClearAspect::Stencil && !DecodePackedStencilClear(packed_clear, &stencil_clear)) {
|
|
return false;
|
|
}
|
|
|
|
RequireNoMetaOverlapLocked(vaddr, size);
|
|
const bool buffer_overlap = m_buffer_cache.HasPageOverlap(vaddr, size);
|
|
const bool buffer_cpu_modified =
|
|
buffer_overlap && m_buffer_cache.IsRegionCpuModified(vaddr, size);
|
|
const bool buffer_gpu_modified =
|
|
buffer_overlap && m_buffer_cache.IsRegionGpuModified(vaddr, size);
|
|
BufferImageCopySource source {nullptr, 0, vaddr, size, true};
|
|
if (buffer_overlap && !buffer_cpu_modified && !buffer_gpu_modified) {
|
|
source = m_buffer_cache.ObtainBufferForImage(vaddr, size);
|
|
}
|
|
const bool buffer_source_valid =
|
|
!buffer_overlap || IsCoherentGuestImageSource(source, vaddr, size);
|
|
const bool image_cpu_modified = m_memory_tracker.IsRegionCpuModified(vaddr, size);
|
|
const bool invalid_common = !buffer_source_valid || buffer_cpu_modified ||
|
|
buffer_gpu_modified || match->buffer_modified || image_cpu_modified;
|
|
if (aspect == ClearAspect::Color && invalid_common) {
|
|
EXIT("TextureCache: compute image clear requires exclusive GPU image ownership, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64
|
|
" buffer_overlap=%d source_valid=%d buffer_cpu_modified=%d"
|
|
" buffer_gpu_modified=%d buffer_modified=%d image_cpu_modified=%d\n",
|
|
vaddr, size, buffer_overlap, buffer_source_valid, buffer_cpu_modified,
|
|
buffer_gpu_modified, match->buffer_modified, image_cpu_modified);
|
|
}
|
|
if (aspect != ClearAspect::Color && (!match->gpu_modified || invalid_common)) {
|
|
const bool depth = aspect == ClearAspect::Depth;
|
|
const char* plane = depth ? "depth" : "stencil";
|
|
const char* source_name = depth ? "buffer" : "stencil";
|
|
EXIT("TextureCache: compute %s clear requires a GPU-owned depth image and a clean "
|
|
"%s source, addr=0x%016" PRIx64 " size=0x%016" PRIx64
|
|
" gpu_modified=%d buffer_overlap=%d source_valid=%d buffer_cpu_modified=%d"
|
|
" buffer_gpu_modified=%d buffer_modified=%d image_cpu_modified=%d\n",
|
|
plane, source_name, vaddr, size, match->gpu_modified, buffer_overlap,
|
|
buffer_source_valid, buffer_cpu_modified, buffer_gpu_modified, match->buffer_modified,
|
|
image_cpu_modified);
|
|
}
|
|
|
|
auto vk_buffer = command->Handle();
|
|
if (aspect == ClearAspect::Color) {
|
|
vk::ClearColorValue clear {};
|
|
if (!DecodePackedColorClear(match->image->format, packed_clear, &clear)) {
|
|
return false;
|
|
}
|
|
GraphicsRenderColorImageBarrier(vk_buffer, match->image,
|
|
vk::ImageLayout::eTransferDstOptimal);
|
|
const vk::ImageSubresourceRange range {vk::ImageAspectFlagBits::eColor, 0,
|
|
VK_REMAINING_MIP_LEVELS, 0, match->image->layers};
|
|
vk_buffer.clearColorImage(match->image->image, match->image->layout, &clear, 1, &range);
|
|
GraphicsRenderColorImageBarrier(vk_buffer, match->image, RENDER_COLOR_IMAGE_LAYOUT);
|
|
if (!match->gpu_modified) {
|
|
m_memory_tracker.MarkRegionAsGpuModified(vaddr, size);
|
|
match->gpu_modified = true;
|
|
}
|
|
command->RetainResourceUntilFence(match);
|
|
return true;
|
|
}
|
|
|
|
if (match->image->layout == vk::ImageLayout::eUndefined ||
|
|
match->image->layout == vk::ImageLayout::ePreinitialized) {
|
|
EXIT("TextureCache: compute %s clear has invalid source layout %u\n",
|
|
aspect == ClearAspect::Depth ? "depth" : "stencil",
|
|
static_cast<uint32_t>(match->image->layout));
|
|
}
|
|
const auto old_layout = match->image->layout;
|
|
GraphicsRenderDepthStencilImageBarrier(vk_buffer, match->image,
|
|
vk::ImageLayout::eTransferDstOptimal);
|
|
const vk::ClearDepthStencilValue clear {depth_clear, stencil_clear};
|
|
const auto clear_aspect = static_cast<vk::ImageAspectFlags>(
|
|
aspect == ClearAspect::Depth ? vk::ImageAspectFlagBits::eDepth
|
|
: vk::ImageAspectFlagBits::eStencil);
|
|
const vk::ImageSubresourceRange range {clear_aspect, 0, VK_REMAINING_MIP_LEVELS, 0,
|
|
match->image->layers};
|
|
vk_buffer.clearDepthStencilImage(match->image->image, match->image->layout, &clear, 1, &range);
|
|
GraphicsRenderDepthStencilImageBarrier(vk_buffer, match->image, old_layout);
|
|
if (aspect == ClearAspect::Stencil) {
|
|
match->stencil_initialized = true;
|
|
}
|
|
command->RetainResourceUntilFence(match);
|
|
return true;
|
|
}
|
|
|
|
void TextureCache::MarkGpuWritten(VulkanImage* image) {
|
|
if (image == nullptr) {
|
|
EXIT("TextureCache: invalid GPU-write notification, image=%p\n",
|
|
static_cast<const void*>(image));
|
|
}
|
|
std::lock_guard transaction(m_resource_mutex);
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
for (auto& cached: m_images) {
|
|
if (cached->image != image) {
|
|
continue;
|
|
}
|
|
if (cached->kind != CachedImage::Kind::RenderTarget &&
|
|
cached->kind != CachedImage::Kind::DepthTarget &&
|
|
cached->kind != CachedImage::Kind::VideoOut) {
|
|
EXIT("TextureCache: sampled texture cannot be marked GPU-written, image=%p kind=%u\n",
|
|
static_cast<const void*>(image), static_cast<uint32_t>(cached->kind));
|
|
}
|
|
for (uint32_t i = 0; i < cached->RangeCount(); i++) {
|
|
if (m_buffer_cache.HasPageOverlap(cached->Address(i), cached->Size(i))) {
|
|
const bool cpu_modified =
|
|
m_buffer_cache.IsRegionCpuModified(cached->Address(i), cached->Size(i));
|
|
const bool gpu_modified =
|
|
m_buffer_cache.IsRegionGpuModified(cached->Address(i), cached->Size(i));
|
|
if (cpu_modified || gpu_modified) {
|
|
EXIT(
|
|
"TextureCache: GPU-written image aliases a dirty buffer, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " cpu_modified=%d gpu_modified=%d\n",
|
|
cached->Address(i), cached->Size(i), cpu_modified, gpu_modified);
|
|
}
|
|
}
|
|
if (m_memory_tracker.IsRegionCpuModified(cached->Address(i), cached->Size(i))) {
|
|
EXIT("TextureCache: GPU-write begins while image range is CPU-modified, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
|
|
cached->Address(i), cached->Size(i));
|
|
}
|
|
}
|
|
if (cached->kind == CachedImage::Kind::DepthTarget) {
|
|
if ((cached->depth.htile_address == 0) != (cached->depth.htile_size == 0)) {
|
|
EXIT("TextureCache: depth target has incomplete HTile range, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 "\n",
|
|
cached->depth.htile_address, cached->depth.htile_size);
|
|
}
|
|
if (cached->depth.htile_address != 0) {
|
|
auto meta = m_surface_metas.find(cached->depth.htile_address);
|
|
if (meta == m_surface_metas.end() ||
|
|
meta->second.size != cached->depth.htile_size) {
|
|
EXIT("TextureCache: depth target HTile metadata is missing or mismatched, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
|
|
cached->depth.htile_address, cached->depth.htile_size);
|
|
}
|
|
m_buffer_cache.ValidateGpuAccess(cached->depth.htile_address,
|
|
cached->depth.htile_size, false, true);
|
|
m_metadata_tracker.ForEachUploadRange(
|
|
cached->depth.htile_address, cached->depth.htile_size, true,
|
|
[](uint64_t, uint64_t) noexcept {}, []() noexcept {});
|
|
meta->second.gpu_modified = true;
|
|
}
|
|
}
|
|
if (!cached->gpu_modified) {
|
|
for (uint32_t i = 0; i < cached->RangeCount(); i++) {
|
|
m_memory_tracker.MarkRegionAsGpuModified(cached->Address(i), cached->Size(i));
|
|
}
|
|
cached->gpu_modified = true;
|
|
}
|
|
return;
|
|
}
|
|
EXIT("TextureCache: GPU-written image is not registered, image=%p\n",
|
|
static_cast<const void*>(image));
|
|
}
|
|
|
|
void TextureCache::PrepareHostWrite(uint64_t vaddr, uint64_t size) {
|
|
if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
|
|
size > TRACKER_ADDRESS_SIZE - vaddr) {
|
|
EXIT("TextureCache: invalid host-write range, addr=0x%016" PRIx64 " size=0x%016" PRIx64
|
|
"\n",
|
|
vaddr, size);
|
|
}
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
const bool metadata_overlap = HasMetaOverlapLocked(vaddr, size);
|
|
bool found = false;
|
|
for (const auto& cached: m_images) {
|
|
for (uint32_t range = 0; range < cached->RangeCount(); range++) {
|
|
// CPU-current depth targets use the same tracked guest refresh path as sampled and
|
|
// color images. GPU-owned targets and metadata aliases remain unsupported below.
|
|
const bool host_refreshable = (cached->kind == CachedImage::Kind::Texture ||
|
|
cached->kind == CachedImage::Kind::RenderTarget ||
|
|
cached->kind == CachedImage::Kind::DepthTarget) &&
|
|
!cached->buffer_modified;
|
|
switch (ClassifyHostWriteOverlap(vaddr, size, cached->Address(range),
|
|
cached->Size(range), host_refreshable,
|
|
cached->gpu_modified, metadata_overlap)) {
|
|
case HostWriteOverlap::None: break;
|
|
case HostWriteOverlap::InvalidateImage: found = true; break;
|
|
case HostWriteOverlap::Unsupported:
|
|
EXIT("TextureCache: host write aliases unsupported image, "
|
|
"write=0x%016" PRIx64 "+0x%016" PRIx64 " image=0x%016" PRIx64
|
|
"+0x%016" PRIx64
|
|
" kind=%u gpu_modified=%d buffer_modified=%d metadata_overlap=%d\n",
|
|
vaddr, size, cached->Address(range), cached->Size(range),
|
|
static_cast<uint32_t>(cached->kind), cached->gpu_modified,
|
|
cached->buffer_modified, metadata_overlap);
|
|
}
|
|
}
|
|
}
|
|
if (!found) {
|
|
EXIT("TextureCache: host write expected a refreshable image overlap, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 "\n",
|
|
vaddr, size);
|
|
}
|
|
// Explicitly invalidate on a write fault. The resource transaction is already held here, so
|
|
// writing first would recursively enter the cache.
|
|
m_memory_tracker.MarkRegionAsCpuModified(vaddr, size);
|
|
MarkSampledAliasesCpuDirtyLocked(vaddr, size);
|
|
}
|
|
|
|
void TextureCache::SynchronizeColorImageToBufferLocked(CachedImage& cached, uint64_t write_address,
|
|
uint64_t write_size) {
|
|
const bool render_target = cached.kind == CachedImage::Kind::RenderTarget;
|
|
const bool video_out = cached.kind == CachedImage::Kind::VideoOut;
|
|
const bool storage = cached.kind == CachedImage::Kind::StorageTexture;
|
|
RenderTargetInfo target = cached.target;
|
|
if (storage) {
|
|
const auto& info = cached.info;
|
|
target.address = info.address;
|
|
target.size = info.size;
|
|
target.format = VulkanFormat(info.format);
|
|
target.width = info.width;
|
|
target.height = info.height;
|
|
target.pitch = info.pitch;
|
|
target.bytes_per_element = Prospero::RenderTargetBytesPerElement(info.format);
|
|
target.tile_mode = info.tile;
|
|
target.levels = info.levels;
|
|
}
|
|
if (video_out) {
|
|
const auto& info = cached.video_out;
|
|
target.address = info.address;
|
|
target.size = info.size;
|
|
target.format = info.format;
|
|
target.width = info.width;
|
|
target.height = info.height;
|
|
target.pitch = info.pitch;
|
|
target.bytes_per_element = info.bytes_per_element;
|
|
target.tile_mode = info.tile_mode;
|
|
}
|
|
const bool linear = target.tile_mode == Prospero::GpuEnumValue(Prospero::TileMode::kLinear);
|
|
const bool tiled = IsTiledRenderTarget(target);
|
|
TileSizeAlign exact {};
|
|
bool single_slice = false;
|
|
if (IsSupportedStandard64RenderTarget(target)) {
|
|
exact = {static_cast<uint32_t>(target.size), 65536};
|
|
single_slice = true;
|
|
} else {
|
|
single_slice = TileGetRenderTargetSize(target.width, target.height, target.pitch,
|
|
target.bytes_per_element, &exact);
|
|
}
|
|
const bool layered_size =
|
|
single_slice && static_cast<uint64_t>(exact.size) * target.layers == target.size;
|
|
if (storage && target.levels > 1) {
|
|
single_slice = TileGetRenderTargetMipLayout(target.width, target.height, target.pitch,
|
|
target.bytes_per_element, target.levels, &exact,
|
|
nullptr, nullptr);
|
|
}
|
|
const bool exact_tiled = tiled && exact.align == 65536 &&
|
|
(storage ? single_slice && exact.size == target.size : layered_size);
|
|
const bool valid_kind =
|
|
render_target || storage ||
|
|
(video_out && cached.video_out.compression == VideoOutCompression::Uncompressed);
|
|
if (!valid_kind || !cached.gpu_modified || cached.buffer_modified ||
|
|
(!storage && target.levels != 1) || target.size > UINT32_MAX || (!linear && !exact_tiled) ||
|
|
HasMetaOverlapLocked(target.address, target.size)) {
|
|
EXIT("TextureCache: unsupported color-image buffer synchronization, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64
|
|
" extent=%ux%u pitch=%u bpe=%u levels=%u layers=%u tile=%u"
|
|
" kind=%u compression=%u gpu_modified=%d buffer_modified=%d\n",
|
|
target.address, target.size, target.width, target.height, target.pitch,
|
|
target.bytes_per_element, target.levels, target.layers, target.tile_mode,
|
|
static_cast<uint32_t>(cached.kind),
|
|
video_out ? static_cast<uint32_t>(cached.video_out.compression) : 0,
|
|
cached.gpu_modified, cached.buffer_modified);
|
|
}
|
|
if (write_address < target.address || write_size == 0 ||
|
|
write_address - target.address > target.size ||
|
|
write_size > target.size - (write_address - target.address)) {
|
|
EXIT("TextureCache: image synchronization write is outside backing, "
|
|
"write=0x%016" PRIx64 "+0x%016" PRIx64 " image=0x%016" PRIx64 "+0x%016" PRIx64 "\n",
|
|
write_address, write_size, target.address, target.size);
|
|
}
|
|
const auto slice_size = target.size / target.layers;
|
|
if (cached.image->format != target.format || cached.image->extent.width != target.width ||
|
|
cached.image->extent.height != target.height ||
|
|
(tiled && !IsSupportedRenderTargetElementSize(target.bytes_per_element)) ||
|
|
HasMetaOverlapLocked(target.address, target.size)) {
|
|
EXIT("TextureCache: color-image buffer synchronization storage mismatch, "
|
|
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 " linear=0x%016" PRIx64 "\n",
|
|
target.address, target.size);
|
|
}
|
|
|
|
// This is the CPU Tiler backend for the image-to-buffer synchronization seam.
|
|
// The guest vector and Vulkan staging allocation retain capacity; a future PS5 GPU tiler can
|
|
// replace this block without changing alias classification or ownership transitions.
|
|
Transfer::WaitForGraphicsIdle(cached.ctx);
|
|
std::vector<ImageBufferCopy> regions;
|
|
if (storage) {
|
|
auto layout = TextureCalcUploadLayout(
|
|
cached.info.format, cached.info.width, cached.info.height, cached.info.levels,
|
|
cached.info.depth, cached.info.pitch, cached.info.tile, cached.info.size, true, false,
|
|
false, "StorageTextureReadback");
|
|
auto uploads = TextureBuildUploadRegions(
|
|
layout, cached.image->format, cached.info.width, cached.info.height, cached.info.depth,
|
|
cached.info.levels, false, false, TextureUploadDestination::MipLevels,
|
|
TextureUploadSliceLayout::MipChainPerSlice);
|
|
regions.reserve(uploads.size());
|
|
for (const auto& upload: uploads) {
|
|
regions.push_back({upload.offset, upload.pitch, upload.dst_level, upload.width,
|
|
upload.height, upload.copy_height, upload.dst_layer, upload.dst_x,
|
|
upload.dst_y, upload.dst_z, upload.aspect});
|
|
}
|
|
} else {
|
|
regions = Transfer::MakeLayeredImageBufferCopies(target.layers, slice_size, target.pitch,
|
|
target.width, target.height);
|
|
}
|
|
Transfer::ProcessDownloadedImage(
|
|
cached.ctx, target.size, regions, cached.image, cached.image->layout,
|
|
[&](std::span<const uint8_t> linear) {
|
|
if (storage) {
|
|
m_buffer_transition_guest.resize(target.size);
|
|
m_tiler.TileImage(m_buffer_transition_guest.data(), linear.data(), cached.info);
|
|
Libs::LibKernel::Memory::WriteBacking(
|
|
target.address, m_buffer_transition_guest.data(), target.size);
|
|
} else if (tiled) {
|
|
m_buffer_transition_guest.resize(target.size);
|
|
m_tiler.TileImage(m_buffer_transition_guest.data(), linear.data(), target);
|
|
Libs::LibKernel::Memory::WriteBacking(
|
|
target.address, m_buffer_transition_guest.data(), target.size);
|
|
} else {
|
|
Libs::LibKernel::Memory::WriteBacking(target.address, linear.data(), target.size);
|
|
}
|
|
});
|
|
m_memory_tracker.ForEachDownloadRange<true>(target.address, target.size,
|
|
[](uint64_t, uint64_t) noexcept {});
|
|
// Only the impending buffer-write range needs publication into BufferCache ownership. The
|
|
// complete image backing was reconstructed above so a later partial-buffer rebind can detile
|
|
// the coherent mip chain without requiring one cached buffer to contain the whole image.
|
|
m_buffer_cache.PublishImageBacking(write_address, write_size);
|
|
cached.gpu_modified = false;
|
|
cached.buffer_modified = true;
|
|
}
|
|
|
|
void TextureCache::SynchronizeDepthImageToBufferLocked(CachedImage& cached, uint64_t write_address,
|
|
uint64_t write_size) {
|
|
const auto& info = cached.depth;
|
|
const bool has_stencil = info.stencil_address != 0 || info.stencil_size != 0;
|
|
const bool has_htile = info.htile_address != 0 || info.htile_size != 0;
|
|
TileSizeAlign expected_stencil {};
|
|
TileSizeAlign expected_htile {};
|
|
TileSizeAlign expected_depth {};
|
|
const bool d16 =
|
|
info.guest_format == Prospero::GpuEnumValue(Prospero::BufferFormat::k16UNorm) &&
|
|
info.format == vk::Format::eD16Unorm && info.bytes_per_element == 2;
|
|
const bool d32 =
|
|
info.guest_format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32Float) &&
|
|
info.format == vk::Format::eD32Sfloat && info.bytes_per_element == 4;
|
|
const bool layout =
|
|
(d16 || d32) &&
|
|
TileGetDepthSize(info.width, info.height, 0,
|
|
Prospero::GpuEnumValue(d16 ? Prospero::DepthFormat::kZ16
|
|
: Prospero::DepthFormat::kZ32F),
|
|
Prospero::GpuEnumValue(Prospero::StencilFormat::kInvalid), false,
|
|
&expected_stencil, &expected_htile, &expected_depth);
|
|
if (cached.kind != CachedImage::Kind::DepthTarget || !cached.gpu_modified ||
|
|
cached.buffer_modified || write_address != info.address || write_size != info.size ||
|
|
has_stencil || has_htile || info.layers != 1 || !layout || expected_depth.align != 65536 ||
|
|
expected_depth.size != info.size || cached.image->format != info.format ||
|
|
cached.image->extent.width != info.width || cached.image->extent.height != info.height ||
|
|
HasMetaOverlapLocked(info.address, info.size)) {
|
|
EXIT("TextureCache: unsupported depth-image buffer synchronization, "
|
|
"write=0x%016" PRIx64 "+0x%016" PRIx64 " depth=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" extent=%ux%u layers=%u format=%d guest=%u bpe=%u"
|
|
" stencil=%d htile=%d gpu_modified=%d buffer_modified=%d\n",
|
|
write_address, write_size, info.address, info.size, info.width, info.height,
|
|
info.layers, static_cast<int>(info.format), info.guest_format, info.bytes_per_element,
|
|
has_stencil, has_htile, cached.gpu_modified, cached.buffer_modified);
|
|
}
|
|
Transfer::WaitForGraphicsIdle(cached.ctx);
|
|
const auto regions = Transfer::MakeLayeredImageBufferCopies(
|
|
1, info.size, info.pitch, info.width, info.height, vk::ImageAspectFlagBits::eDepth);
|
|
Transfer::ProcessDownloadedImage(
|
|
cached.ctx, info.size, regions, cached.image, cached.image->layout,
|
|
[&](std::span<const uint8_t> linear) {
|
|
m_buffer_transition_guest.resize(info.size);
|
|
m_tiler.TileImage(m_buffer_transition_guest.data(), linear.data(), info);
|
|
Libs::LibKernel::Memory::WriteBacking(info.address, m_buffer_transition_guest.data(),
|
|
info.size);
|
|
});
|
|
m_memory_tracker.ForEachDownloadRange<true>(info.address, info.size,
|
|
[](uint64_t, uint64_t) noexcept {});
|
|
m_buffer_cache.PublishImageBacking(write_address, write_size);
|
|
cached.gpu_modified = false;
|
|
cached.buffer_modified = true;
|
|
}
|
|
|
|
bool TextureCache::InvalidateMemoryFromGPU(uint64_t vaddr, uint64_t size,
|
|
bool formatted_buffer_write) {
|
|
if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
|
|
size > TRACKER_ADDRESS_SIZE - vaddr) {
|
|
EXIT("TextureCache: invalid GPU invalidation range, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 "\n",
|
|
vaddr, size);
|
|
}
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
for (auto it = m_surface_metas.begin(); it != m_surface_metas.end();) {
|
|
switch (ClassifyImageRangeOverlap(vaddr, size, it->first, it->second.size)) {
|
|
case ImageRangeOverlap::None:
|
|
case ImageRangeOverlap::PageOnly: ++it; continue;
|
|
case ImageRangeOverlap::Bytes: break;
|
|
}
|
|
LOGF("TextureCache: discarding overwritten virtual metadata, write=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " metadata=0x%016" PRIx64 "+0x%016" PRIx64 "\n",
|
|
vaddr, size, it->first, it->second.size);
|
|
if (it->second.gpu_modified) {
|
|
m_metadata_tracker.ForEachDownloadRange<true>(it->first, it->second.size,
|
|
[](uint64_t, uint64_t) noexcept {});
|
|
}
|
|
m_metadata_tracker.UntrackMemory(it->first, it->second.size);
|
|
it = m_surface_metas.erase(it);
|
|
}
|
|
auto match = m_images.end();
|
|
BufferImageWrite action = BufferImageWrite::None;
|
|
for (auto it = m_images.begin(); it != m_images.end(); ++it) {
|
|
auto& cached = **it;
|
|
if (!cached.OverlapsRange(vaddr, size, false)) {
|
|
continue;
|
|
}
|
|
const auto next = ClassifyBufferImageWrite(vaddr, size, cached.Address(), cached.Size(),
|
|
cached.BufferBinding(), cached.gpu_modified,
|
|
formatted_buffer_write, cached.buffer_modified);
|
|
if (match != m_images.end() || next == BufferImageWrite::None ||
|
|
next == BufferImageWrite::Unsupported) {
|
|
EXIT("TextureCache: unsupported GPU invalidation alias, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " cached_kind=%u cached=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" gpu_modified=%d buffer_modified=%d formatted=%d ambiguous=%d\n",
|
|
vaddr, size, static_cast<uint32_t>(cached.kind), cached.Address(), cached.Size(),
|
|
cached.gpu_modified, cached.buffer_modified, formatted_buffer_write,
|
|
match != m_images.end());
|
|
}
|
|
match = it;
|
|
action = next;
|
|
}
|
|
if (match == m_images.end()) {
|
|
return false;
|
|
}
|
|
auto& cached = **match;
|
|
switch (action) {
|
|
case BufferImageWrite::InvalidateTexture:
|
|
case BufferImageWrite::InvalidateVideoOut:
|
|
case BufferImageWrite::InvalidateStorageTexture:
|
|
case BufferImageWrite::InvalidateDepthTarget:
|
|
case BufferImageWrite::InvalidateRenderTarget: cached.buffer_modified = true; return true;
|
|
case BufferImageWrite::SynchronizeRenderTarget:
|
|
case BufferImageWrite::SynchronizeStorageTexture:
|
|
SynchronizeColorImageToBufferLocked(cached, vaddr, size);
|
|
return true;
|
|
case BufferImageWrite::SynchronizeDepthTarget:
|
|
SynchronizeDepthImageToBufferLocked(cached, vaddr, size);
|
|
return true;
|
|
case BufferImageWrite::SynchronizeVideoOut:
|
|
SynchronizeColorImageToBufferLocked(cached, vaddr, size);
|
|
return true;
|
|
case BufferImageWrite::None:
|
|
case BufferImageWrite::Unsupported:
|
|
EXIT("TextureCache: invalid GPU invalidation action %u\n",
|
|
static_cast<uint32_t>(action));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
DepthStencilVulkanImage* TextureCache::FindDepthTargetByRange(CommandBuffer* command,
|
|
uint64_t vaddr, uint64_t size,
|
|
bool allow_containing_sampled) {
|
|
if (command == nullptr || vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
|
|
size > TRACKER_ADDRESS_SIZE - vaddr) {
|
|
EXIT("TextureCache: invalid depth-target range query, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 "\n",
|
|
vaddr, size);
|
|
}
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
CachedImage* found = nullptr;
|
|
for (auto* cached: FindImagesInRegionLocked(vaddr, size, false)) {
|
|
if (cached->kind != CachedImage::Kind::DepthTarget ||
|
|
!cached->OverlapsRange(vaddr, size, false)) {
|
|
continue;
|
|
}
|
|
const bool containing_sampled =
|
|
allow_containing_sampled && vaddr == cached->depth.address && size > cached->depth.size;
|
|
if ((!IsDepthTargetRangeCompatible(cached->depth, vaddr, size) && !containing_sampled) ||
|
|
found != nullptr) {
|
|
EXIT("TextureCache: incompatible or ambiguous depth-target range, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " cached=0x%016" PRIx64 "+0x%016" PRIx64 " previous=%p\n",
|
|
vaddr, size, cached->depth.address, cached->depth.size,
|
|
static_cast<const void*>(found));
|
|
}
|
|
if (containing_sampled) {
|
|
return nullptr;
|
|
}
|
|
const bool stencil_range =
|
|
vaddr == cached->depth.stencil_address && size == cached->depth.stencil_size;
|
|
if (stencil_range && !cached->stencil_initialized) {
|
|
EXIT("TextureCache: sampled stencil range is uninitialized, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 "\n",
|
|
vaddr, size);
|
|
}
|
|
found = cached;
|
|
}
|
|
if (found == nullptr) {
|
|
return nullptr;
|
|
}
|
|
const auto owner = std::find_if(m_images.begin(), m_images.end(),
|
|
[found](const auto& image) { return image.get() == found; });
|
|
if (owner == m_images.end()) {
|
|
EXIT("TextureCache: page-table depth target has no cache owner\n");
|
|
}
|
|
command->RetainResourceUntilFence(*owner);
|
|
return static_cast<DepthStencilVulkanImage*>(found->image);
|
|
}
|
|
|
|
RenderTextureVulkanImage* TextureCache::FindRenderTargetByRange(CommandBuffer* command,
|
|
uint64_t vaddr, uint64_t size) {
|
|
if (command == nullptr || vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
|
|
size > TRACKER_ADDRESS_SIZE - vaddr) {
|
|
EXIT("TextureCache: invalid render-target range query, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 "\n",
|
|
vaddr, size);
|
|
}
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
CachedImage* found = nullptr;
|
|
for (auto* cached: FindImagesInRegionLocked(vaddr, size, false)) {
|
|
if (cached->kind != CachedImage::Kind::RenderTarget ||
|
|
!ImageRangeOverlaps(vaddr, size, cached->Address(), cached->Size())) {
|
|
continue;
|
|
}
|
|
const auto slice_size =
|
|
cached->target.layers != 0 ? cached->target.size / cached->target.layers : 0;
|
|
const bool contained = vaddr == cached->target.address && slice_size != 0 &&
|
|
cached->target.size % cached->target.layers == 0 &&
|
|
size <= cached->target.size && size % slice_size == 0;
|
|
if (!contained) {
|
|
continue;
|
|
}
|
|
if (found != nullptr) {
|
|
EXIT("TextureCache: incompatible or ambiguous render-target range, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " cached=0x%016" PRIx64 "+0x%016" PRIx64 " previous=%p\n",
|
|
vaddr, size, cached->Address(), cached->Size(), static_cast<const void*>(found));
|
|
}
|
|
found = cached;
|
|
}
|
|
if (found == nullptr) {
|
|
return nullptr;
|
|
}
|
|
const auto owner = std::find_if(m_images.begin(), m_images.end(),
|
|
[found](const auto& image) { return image.get() == found; });
|
|
if (owner == m_images.end()) {
|
|
EXIT("TextureCache: page-table render target has no cache owner\n");
|
|
}
|
|
command->RetainResourceUntilFence(*owner);
|
|
return static_cast<RenderTextureVulkanImage*>(found->image);
|
|
}
|
|
|
|
TextureCache::RegionInfo TextureCache::QueryRegion(uint64_t vaddr, uint64_t size) {
|
|
if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
|
|
size > TRACKER_ADDRESS_SIZE - vaddr) {
|
|
EXIT("TextureCache: invalid region query, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
|
|
vaddr, size);
|
|
}
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
RegionInfo result;
|
|
const auto candidates = FindImagesInRegionLocked(vaddr, size, true);
|
|
result.image_pages = !candidates.empty();
|
|
for (const auto* cached: candidates) {
|
|
const bool bytes = cached->OverlapsRange(vaddr, size, false);
|
|
result.image_bytes |= bytes;
|
|
result.gpu_image_bytes |= bytes && cached->gpu_modified;
|
|
result.non_sampled_pages |= cached->kind != CachedImage::Kind::Texture;
|
|
}
|
|
for (const auto& [address, metadata]: m_surface_metas) {
|
|
result.metadata_pages |= ImagePageRangesOverlap(vaddr, size, address, metadata.size);
|
|
result.metadata_bytes |= ImageRangeOverlaps(vaddr, size, address, metadata.size);
|
|
}
|
|
result.gpu_metadata_bytes =
|
|
result.metadata_bytes && m_metadata_tracker.IsRegionGpuModified(vaddr, size);
|
|
return result;
|
|
}
|
|
|
|
bool TextureCache::ResolveMetaRange(uint64_t vaddr, uint64_t size, MetaRangeInfo* info) {
|
|
if (info == nullptr || vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
|
|
size > TRACKER_ADDRESS_SIZE - vaddr) {
|
|
return false;
|
|
}
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
MetaRangeInfo found {};
|
|
bool matched = false;
|
|
for (const auto& [address, metadata]: m_surface_metas) {
|
|
const auto slice_size = metadata.size / metadata.layers;
|
|
const bool full = vaddr == address && size == metadata.size;
|
|
const auto offset = vaddr >= address ? vaddr - address : UINT64_MAX;
|
|
const bool slice =
|
|
!full && size == slice_size && offset < metadata.size && offset % slice_size == 0;
|
|
if (!full && !slice) {
|
|
continue;
|
|
}
|
|
MetaRangeInfo candidate {.metadata_address = address,
|
|
.metadata_size = metadata.size,
|
|
.slice = full ? 0u : static_cast<uint32_t>(offset / slice_size),
|
|
.full = full};
|
|
if (matched && (candidate.metadata_address != found.metadata_address ||
|
|
candidate.metadata_size != found.metadata_size ||
|
|
candidate.slice != found.slice || candidate.full != found.full)) {
|
|
EXIT("TextureCache: ambiguous exact metadata range, request=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " first=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" slice=%u full=%d second=0x%016" PRIx64 "+0x%016" PRIx64 " slice=%u full=%d\n",
|
|
vaddr, size, found.metadata_address, found.metadata_size, found.slice, found.full,
|
|
candidate.metadata_address, candidate.metadata_size, candidate.slice,
|
|
candidate.full);
|
|
}
|
|
found = candidate;
|
|
matched = true;
|
|
}
|
|
if (matched) {
|
|
*info = found;
|
|
}
|
|
return matched;
|
|
}
|
|
|
|
void TextureCache::RegisterMeta(GraphicContext* ctx, uint64_t vaddr, uint64_t size,
|
|
uint32_t layers) {
|
|
if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
|
|
size > TRACKER_ADDRESS_SIZE - vaddr || (vaddr & 0x7fffu) != 0 || layers == 0 ||
|
|
layers > 32 || size % layers != 0) {
|
|
EXIT("TextureCache: invalid metadata registration, addr=0x%016" PRIx64 " size=0x%016" PRIx64
|
|
" layers=%u\n",
|
|
vaddr, size, layers);
|
|
}
|
|
std::lock_guard transaction(m_resource_mutex);
|
|
if (m_buffer_cache.HasPageOverlap(vaddr, size)) {
|
|
// Register virtual surface metadata independently of an earlier buffer view. Kyty's split
|
|
// caches first publish any dirty buffer bytes; clean partial views use guest backing.
|
|
const auto source = m_buffer_cache.ObtainBufferForImage(vaddr, size);
|
|
if (!IsCoherentGuestImageSource(source, vaddr, size)) {
|
|
EXIT("TextureCache: metadata buffer source is inconsistent, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " source=%p source_addr=0x%016" PRIx64
|
|
" source_size=0x%016" PRIx64 " current=%d\n",
|
|
vaddr, size, static_cast<const void*>(source.buffer), source.address, source.size,
|
|
source.cpu_current);
|
|
}
|
|
}
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
auto existing = m_surface_metas.find(vaddr);
|
|
uint64_t range_vaddr = vaddr;
|
|
uint64_t range_size = size;
|
|
if (existing != m_surface_metas.end()) {
|
|
const auto slice_size = size / layers;
|
|
const auto old_slice_size = existing->second.size / existing->second.layers;
|
|
if (slice_size != old_slice_size ||
|
|
(size > existing->second.size) != (layers > existing->second.layers)) {
|
|
EXIT("TextureCache: incompatible metadata backing growth\n");
|
|
}
|
|
if (layers <= existing->second.layers) {
|
|
return;
|
|
}
|
|
range_vaddr += existing->second.size;
|
|
range_size -= existing->second.size;
|
|
}
|
|
for (const auto& [address, meta]: m_surface_metas) {
|
|
if (address != vaddr &&
|
|
ImagePageRangesOverlap(range_vaddr, range_size, address, meta.size)) {
|
|
LOGF("TextureCache: registering overlapping metadata views, requested=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " existing=0x%016" PRIx64 "+0x%016" PRIx64 "\n",
|
|
range_vaddr, range_size, address, meta.size);
|
|
}
|
|
}
|
|
std::vector<CachedImage*> retire;
|
|
for (const auto& cached: m_images) {
|
|
if (!cached->OverlapsRange(range_vaddr, range_size, true)) {
|
|
continue;
|
|
}
|
|
const bool sampled = cached->kind == CachedImage::Kind::Texture;
|
|
const bool writable_image = cached->kind == CachedImage::Kind::StorageTexture ||
|
|
cached->kind == CachedImage::Kind::RenderTarget ||
|
|
cached->kind == CachedImage::Kind::DepthTarget;
|
|
const bool gpu_modified = cached->gpu_modified || m_memory_tracker.IsRegionGpuModified(
|
|
cached->Address(), cached->Size());
|
|
const bool cpu_dirty =
|
|
cached->kind == CachedImage::Kind::StorageTexture &&
|
|
(cached->info.IsCpuDirty() ||
|
|
m_memory_tracker.IsRegionCpuModified(cached->info.address, cached->info.size));
|
|
const auto overlap =
|
|
ClassifyMetaImageOverlap(sampled, writable_image, gpu_modified, cached->buffer_modified,
|
|
cpu_dirty, cached->ctx == ctx);
|
|
if (overlap == MetaImageOverlap::Unsupported) {
|
|
EXIT("TextureCache: metadata aliases image pages, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " image_kind=%u image=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" gpu_modified=%d buffer_modified=%d same_context=%d\n",
|
|
range_vaddr, range_size, static_cast<uint32_t>(cached->kind), cached->Address(),
|
|
cached->Size(), cached->gpu_modified, cached->buffer_modified, cached->ctx == ctx);
|
|
}
|
|
if (overlap == MetaImageOverlap::RetireImage) {
|
|
retire.push_back(cached.get());
|
|
continue;
|
|
}
|
|
LOGF("TextureCache: metadata aliases a CPU-current sampled image, metadata=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " image=0x%016" PRIx64 "+0x%016" PRIx64 "\n",
|
|
range_vaddr, range_size, cached->Address(), cached->Size());
|
|
}
|
|
if (!retire.empty()) {
|
|
RequireRetirementIsolation(retire, "metadata", range_vaddr, range_size);
|
|
for (const auto* cached: retire) {
|
|
LOGF("TextureCache: retiring a guest-current image for metadata reuse, "
|
|
"metadata=0x%016" PRIx64 "+0x%016" PRIx64 " image=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" kind=%u\n",
|
|
range_vaddr, range_size, cached->Address(), cached->Size(),
|
|
static_cast<uint32_t>(cached->kind));
|
|
}
|
|
RetireDepthMetadataLocked(retire, vaddr);
|
|
RetireImages(retire);
|
|
}
|
|
if (existing == m_surface_metas.end()) {
|
|
m_surface_metas.emplace(vaddr, MetaDataInfo {.size = size, .layers = layers});
|
|
} else {
|
|
existing->second.size = size;
|
|
existing->second.layers = layers;
|
|
}
|
|
}
|
|
|
|
bool TextureCache::IsMeta(uint64_t vaddr) {
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
return m_surface_metas.contains(vaddr);
|
|
}
|
|
|
|
bool TextureCache::IsMetaRange(uint64_t vaddr, uint64_t size) {
|
|
if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
|
|
size > TRACKER_ADDRESS_SIZE - vaddr) {
|
|
return false;
|
|
}
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
const auto it = m_surface_metas.find(vaddr);
|
|
return it != m_surface_metas.end() && it->second.size == size;
|
|
}
|
|
|
|
bool TextureCache::HasMetaOverlapLocked(uint64_t vaddr, uint64_t size) const {
|
|
for (const auto& [address, meta]: m_surface_metas) {
|
|
if (ImagePageRangesOverlap(vaddr, size, address, meta.size)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void TextureCache::RequireNoMetaOverlapLocked(uint64_t vaddr, uint64_t size) const {
|
|
for (const auto& [address, meta]: m_surface_metas) {
|
|
if (!ImagePageRangesOverlap(vaddr, size, address, meta.size)) {
|
|
continue;
|
|
}
|
|
const auto owner = std::find_if(m_images.begin(), m_images.end(), [&](const auto& cached) {
|
|
return cached->kind == CachedImage::Kind::DepthTarget &&
|
|
cached->depth.htile_address == address && cached->depth.htile_size == meta.size;
|
|
});
|
|
EXIT("TextureCache: image range overlaps virtual metadata, image=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " metadata=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" layers=%u gpu=%d clear=0x%08x owner=%p owner_depth=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" owner_gpu=%d owner_buffer=%d\n",
|
|
vaddr, size, address, meta.size, meta.layers, meta.gpu_modified, meta.clear_mask,
|
|
owner != m_images.end() ? static_cast<const void*>(owner->get()) : nullptr,
|
|
owner != m_images.end() ? (*owner)->depth.address : 0,
|
|
owner != m_images.end() ? (*owner)->depth.size : 0,
|
|
owner != m_images.end() && (*owner)->gpu_modified,
|
|
owner != m_images.end() && (*owner)->buffer_modified);
|
|
}
|
|
}
|
|
|
|
void TextureCache::ResolveImageMetadataOverlapsLocked(GraphicContext* ctx, uint64_t vaddr,
|
|
uint64_t size) {
|
|
std::vector<CachedImage*> retire;
|
|
for (const auto& [address, meta]: m_surface_metas) {
|
|
if (!ImagePageRangesOverlap(vaddr, size, address, meta.size)) {
|
|
continue;
|
|
}
|
|
bool found_owner = false;
|
|
for (const auto& cached: m_images) {
|
|
if (cached->kind != CachedImage::Kind::DepthTarget ||
|
|
cached->depth.htile_address != address) {
|
|
continue;
|
|
}
|
|
found_owner = true;
|
|
bool tracker_gpu_modified = false;
|
|
for (uint32_t range = 0; range < cached->RangeCount(); range++) {
|
|
tracker_gpu_modified |= m_memory_tracker.IsRegionGpuModified(cached->Address(range),
|
|
cached->Size(range));
|
|
}
|
|
const bool metadata_tracker_gpu_modified =
|
|
m_metadata_tracker.IsRegionGpuModified(address, meta.size);
|
|
if (cached->depth.htile_size != meta.size ||
|
|
!CanRetireGuestCurrentDepthForMetadataReuse(
|
|
cached->gpu_modified, cached->buffer_modified, tracker_gpu_modified,
|
|
meta.gpu_modified, metadata_tracker_gpu_modified, meta.clear_mask,
|
|
cached->ctx == ctx)) {
|
|
EXIT("TextureCache: image range overlaps owned metadata, image=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " metadata=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" layers=%u clear=0x%08x same_context=%d gpu=%d/%d/%d buffer=%d\n",
|
|
vaddr, size, address, meta.size, meta.layers, meta.clear_mask,
|
|
cached->ctx == ctx, cached->gpu_modified, tracker_gpu_modified,
|
|
metadata_tracker_gpu_modified, cached->buffer_modified);
|
|
}
|
|
if (std::find(retire.begin(), retire.end(), cached.get()) == retire.end()) {
|
|
retire.push_back(cached.get());
|
|
}
|
|
}
|
|
if (!found_owner) {
|
|
EXIT("TextureCache: image range overlaps unowned metadata, image=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " metadata=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" layers=%u gpu=%d clear=0x%08x\n",
|
|
vaddr, size, address, meta.size, meta.layers, meta.gpu_modified, meta.clear_mask);
|
|
}
|
|
}
|
|
RequireRetirementIsolation(retire, "image metadata", vaddr, size);
|
|
RetireDepthMetadataLocked(retire);
|
|
RetireImages(retire);
|
|
RequireNoMetaOverlapLocked(vaddr, size);
|
|
}
|
|
|
|
bool TextureCache::IsMetaCleared(uint64_t vaddr, uint32_t slice) {
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
const auto it = m_surface_metas.find(vaddr);
|
|
if (it == m_surface_metas.end()) {
|
|
return false;
|
|
}
|
|
if (slice >= it->second.layers) {
|
|
EXIT("TextureCache: metadata clear slice out of range, addr=0x%016" PRIx64
|
|
" slice=%u layers=%u\n",
|
|
vaddr, slice, it->second.layers);
|
|
}
|
|
if ((it->second.clear_mask & (1u << slice)) == 0) {
|
|
return false;
|
|
}
|
|
const auto slice_size = it->second.size / it->second.layers;
|
|
const auto slice_addr = vaddr + slice_size * slice;
|
|
if (!it->second.gpu_modified ||
|
|
!m_metadata_tracker.IsRegionGpuModified(slice_addr, slice_size) ||
|
|
m_metadata_tracker.IsRegionCpuModified(slice_addr, slice_size)) {
|
|
EXIT("TextureCache: cleared metadata slice is not GPU-owned\n");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TextureCache::ClearMeta(uint64_t vaddr) {
|
|
std::lock_guard transaction(m_resource_mutex);
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
const auto it = m_surface_metas.find(vaddr);
|
|
if (it == m_surface_metas.end()) {
|
|
return false;
|
|
}
|
|
m_metadata_tracker.ForEachUploadRange(
|
|
vaddr, it->second.size, true, [](uint64_t, uint64_t) noexcept {}, []() noexcept {});
|
|
it->second.gpu_modified = true;
|
|
it->second.clear_mask = it->second.layers == 32 ? UINT32_MAX : (1u << it->second.layers) - 1u;
|
|
return true;
|
|
}
|
|
|
|
bool TextureCache::TouchMeta(uint64_t vaddr, uint32_t slice, bool is_clear) {
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
const auto it = m_surface_metas.find(vaddr);
|
|
if (it == m_surface_metas.end()) {
|
|
return false;
|
|
}
|
|
if (slice >= it->second.layers) {
|
|
EXIT("TextureCache: metadata update slice out of range\n");
|
|
}
|
|
const auto slice_size = it->second.size / it->second.layers;
|
|
const auto slice_addr = vaddr + slice_size * slice;
|
|
if (!it->second.gpu_modified ||
|
|
!m_metadata_tracker.IsRegionGpuModified(slice_addr, slice_size)) {
|
|
EXIT("TextureCache: metadata update requires GPU ownership, addr=0x%016" PRIx64 "\n",
|
|
vaddr);
|
|
}
|
|
if (m_metadata_tracker.IsRegionCpuModified(slice_addr, slice_size)) {
|
|
EXIT("TextureCache: metadata update races CPU modification, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 "\n",
|
|
slice_addr, slice_size);
|
|
}
|
|
if (is_clear) {
|
|
it->second.clear_mask |= 1u << slice;
|
|
} else {
|
|
it->second.clear_mask &= ~(1u << slice);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TextureCache::InvalidateMemory(PageFaultAccess access, uint64_t vaddr, uint64_t size,
|
|
PageFaultPhase phase) noexcept {
|
|
if (access != PageFaultAccess::Read && access != PageFaultAccess::Write) {
|
|
EXIT("TextureCache: unsupported page-fault access %u, addr=0x%016" PRIx64
|
|
" size=0x%016" PRIx64 " phase=%u\n",
|
|
static_cast<uint32_t>(access), vaddr, size, static_cast<uint32_t>(phase));
|
|
}
|
|
if (phase == PageFaultPhase::Invalidate) {
|
|
if (g_texture_fault_owner != nullptr) {
|
|
EXIT("TextureCache: recursive page-fault invalidation, owner=%p\n",
|
|
g_texture_fault_owner);
|
|
}
|
|
m_fault_mutex.lock();
|
|
g_texture_fault_owner = this;
|
|
|
|
const bool metadata =
|
|
access == PageFaultAccess::Write &&
|
|
m_metadata_tracker.InvalidateVirtualGpuWrite(access, vaddr, size, phase);
|
|
CpuFaultAction action = CpuFaultAction::Untracked;
|
|
bool needs_readback = false;
|
|
{
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
if (access == PageFaultAccess::Write) {
|
|
MarkSampledAliasesCpuDirtyLocked(vaddr, size);
|
|
}
|
|
needs_readback = FindGpuReadbackPageCandidateLocked(vaddr, size) != nullptr;
|
|
if (!needs_readback) {
|
|
action = m_memory_tracker.BeginCpuFault(vaddr, size, access);
|
|
}
|
|
}
|
|
|
|
if (needs_readback) {
|
|
if (GraphicsRunIsCommandProcessorThread()) {
|
|
GraphicsRunFinishCommandProcessors();
|
|
}
|
|
m_readback->Request(access, vaddr, size);
|
|
action = m_memory_tracker.BeginCpuFault(vaddr, size, access);
|
|
if (action != CpuFaultAction::Download) {
|
|
EXIT("TextureCache: downloaded image did not retain the active fault page "
|
|
"tracking, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " action=%u\n",
|
|
vaddr, size, static_cast<uint32_t>(action));
|
|
}
|
|
} else if (action == CpuFaultAction::Download) {
|
|
EXIT("generic region invalidation cannot download GPU-dirty memory\n");
|
|
}
|
|
return metadata || action != CpuFaultAction::Untracked;
|
|
}
|
|
|
|
if (g_texture_fault_owner != this) {
|
|
EXIT("TextureCache: page-fault phase has no matching invalidation, phase=%u owner=%p\n",
|
|
static_cast<uint32_t>(phase), g_texture_fault_owner);
|
|
}
|
|
if (phase == PageFaultPhase::Complete) {
|
|
const bool metadata =
|
|
access == PageFaultAccess::Write &&
|
|
m_metadata_tracker.InvalidateVirtualGpuWrite(access, vaddr, size, phase);
|
|
const bool downloaded = m_readback->IsReady(access, vaddr, size);
|
|
const bool image = m_memory_tracker.CompleteCpuFault(vaddr, size, access, downloaded);
|
|
const bool readback = m_readback->Complete(access, vaddr, size);
|
|
if (readback && !image) {
|
|
EXIT("TextureCache: image readback completed without a matching tracker fault\n");
|
|
}
|
|
return metadata || image || readback;
|
|
}
|
|
if (phase != PageFaultPhase::Release) {
|
|
EXIT("TextureCache: unsupported page-fault phase %u\n", static_cast<uint32_t>(phase));
|
|
}
|
|
if (access == PageFaultAccess::Write) {
|
|
(void)m_metadata_tracker.InvalidateVirtualGpuWrite(access, vaddr, size, phase);
|
|
}
|
|
m_readback->Release(access, vaddr, size);
|
|
g_texture_fault_owner = nullptr;
|
|
m_fault_mutex.unlock();
|
|
return true;
|
|
}
|
|
|
|
void TextureCache::UnmapMemory(uint64_t vaddr, uint64_t size) {
|
|
if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
|
|
size > TRACKER_ADDRESS_SIZE - vaddr) {
|
|
EXIT("TextureCache: invalid unmap range, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
|
|
vaddr, size);
|
|
}
|
|
std::lock_guard transaction(m_resource_mutex);
|
|
FaultSafeTextureLock lock(this, m_lock);
|
|
const auto end = vaddr + size;
|
|
for (const auto& [address, meta]: m_surface_metas) {
|
|
if (ImagePageRangesOverlap(vaddr, size, address, meta.size) &&
|
|
(vaddr > address || end < address + meta.size)) {
|
|
EXIT("TextureCache: partial metadata unmap is unsupported, unmap=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " metadata=0x%016" PRIx64 "+0x%016" PRIx64 "\n",
|
|
vaddr, size, address, meta.size);
|
|
}
|
|
}
|
|
for (const auto& cached: m_images) {
|
|
for (uint32_t i = 0; i < cached->RangeCount(); i++) {
|
|
if (ImagePageRangesOverlap(vaddr, size, cached->Address(i), cached->Size(i)) &&
|
|
(vaddr > cached->Address(i) || end < cached->Address(i) + cached->Size(i))) {
|
|
EXIT("TextureCache: partial image unmap is unsupported, unmap=0x%016" PRIx64
|
|
"+0x%016" PRIx64 " image=0x%016" PRIx64 "+0x%016" PRIx64 "\n",
|
|
vaddr, size, cached->Address(i), cached->Size(i));
|
|
}
|
|
}
|
|
if (cached->OverlapsRange(vaddr, size, false) &&
|
|
cached->kind == CachedImage::Kind::VideoOut) {
|
|
EXIT("TextureCache: registered video-out surface must be unregistered before unmap, "
|
|
"unmap=0x%016" PRIx64 "+0x%016" PRIx64 " image=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" buffer_modified=%d\n",
|
|
vaddr, size, cached->Address(), cached->Size(), cached->buffer_modified);
|
|
}
|
|
}
|
|
std::vector<uint64_t> retire_depth_metadata;
|
|
for (const auto& cached: m_images) {
|
|
if (cached->kind != CachedImage::Kind::DepthTarget ||
|
|
!cached->OverlapsRange(vaddr, size, false) || cached->depth.htile_address == 0) {
|
|
continue;
|
|
}
|
|
const bool retained_owner =
|
|
std::any_of(m_images.begin(), m_images.end(), [&](const auto& other) {
|
|
return other.get() != cached.get() &&
|
|
other->kind == CachedImage::Kind::DepthTarget &&
|
|
!other->OverlapsRange(vaddr, size, false) &&
|
|
other->depth.htile_address == cached->depth.htile_address &&
|
|
other->depth.htile_size == cached->depth.htile_size;
|
|
});
|
|
if (!retained_owner &&
|
|
std::find(retire_depth_metadata.begin(), retire_depth_metadata.end(),
|
|
cached->depth.htile_address) == retire_depth_metadata.end()) {
|
|
retire_depth_metadata.push_back(cached->depth.htile_address);
|
|
}
|
|
}
|
|
for (const auto address: retire_depth_metadata) {
|
|
const auto meta = m_surface_metas.find(address);
|
|
const auto owner = std::find_if(m_images.begin(), m_images.end(), [&](const auto& cached) {
|
|
return cached->kind == CachedImage::Kind::DepthTarget &&
|
|
cached->OverlapsRange(vaddr, size, false) &&
|
|
cached->depth.htile_address == address;
|
|
});
|
|
if (meta == m_surface_metas.end() || owner == m_images.end() ||
|
|
meta->second.size != (*owner)->depth.htile_size) {
|
|
EXIT("TextureCache: retiring depth image has invalid HTile registration, "
|
|
"unmap=0x%016" PRIx64 "+0x%016" PRIx64 " metadata=0x%016" PRIx64
|
|
" registered=0x%016" PRIx64 " expected=0x%016" PRIx64 "\n",
|
|
vaddr, size, address, meta != m_surface_metas.end() ? meta->second.size : 0,
|
|
owner != m_images.end() ? (*owner)->depth.htile_size : 0);
|
|
}
|
|
}
|
|
std::vector<ImageRetirementRange> metadata_ranges;
|
|
metadata_ranges.reserve(m_surface_metas.size());
|
|
for (const auto& [address, meta]: m_surface_metas) {
|
|
const bool retiring = ImageRangeOverlaps(vaddr, size, address, meta.size) ||
|
|
std::find(retire_depth_metadata.begin(), retire_depth_metadata.end(),
|
|
address) != retire_depth_metadata.end();
|
|
metadata_ranges.push_back({address, meta.size, retiring});
|
|
}
|
|
const auto metadata_conflict = FindImageRetirementConflict(metadata_ranges);
|
|
if (metadata_conflict.Exists()) {
|
|
const auto& retired = metadata_ranges[metadata_conflict.retired];
|
|
const auto& retained = metadata_ranges[metadata_conflict.retained];
|
|
EXIT("TextureCache: depth-image retirement leaves a tracked metadata alias, "
|
|
"unmap=0x%016" PRIx64 "+0x%016" PRIx64 " retired=0x%016" PRIx64 "+0x%016" PRIx64
|
|
" retained=0x%016" PRIx64 "+0x%016" PRIx64 "\n",
|
|
vaddr, size, retired.address, retired.size, retained.address, retained.size);
|
|
}
|
|
GraphicContext* wait_ctx = nullptr;
|
|
for (auto& cached: m_images) {
|
|
if (cached->OverlapsRange(vaddr, size, false)) {
|
|
if (wait_ctx != nullptr && wait_ctx != cached->ctx) {
|
|
EXIT("TextureCache: unmap spans multiple graphics contexts, first=%p second=%p\n",
|
|
static_cast<const void*>(wait_ctx), static_cast<const void*>(cached->ctx));
|
|
}
|
|
wait_ctx = cached->ctx;
|
|
}
|
|
}
|
|
if (wait_ctx != nullptr) {
|
|
Transfer::WaitForGraphicsIdle(wait_ctx);
|
|
}
|
|
for (auto& cached: m_images) {
|
|
if (!cached->OverlapsRange(vaddr, size, false) || !cached->gpu_modified) {
|
|
continue;
|
|
}
|
|
for (uint32_t i = 0; i < cached->RangeCount(); i++) {
|
|
m_memory_tracker.UnmarkRegionAsGpuModified(cached->Address(i), cached->Size(i));
|
|
}
|
|
cached->gpu_modified = false;
|
|
}
|
|
for (auto it = m_surface_metas.begin(); it != m_surface_metas.end();) {
|
|
const bool allocation_unmapped =
|
|
ImageRangeOverlaps(vaddr, size, it->first, it->second.size);
|
|
const bool depth_owner_retired =
|
|
std::find(retire_depth_metadata.begin(), retire_depth_metadata.end(), it->first) !=
|
|
retire_depth_metadata.end();
|
|
if (!allocation_unmapped && !depth_owner_retired) {
|
|
++it;
|
|
continue;
|
|
}
|
|
if (depth_owner_retired) {
|
|
LOGF("TextureCache: retiring HTile metadata with depth image, "
|
|
"unmap=0x%016" PRIx64 "+0x%016" PRIx64 " metadata=0x%016" PRIx64 "+0x%016" PRIx64
|
|
"\n",
|
|
vaddr, size, it->first, it->second.size);
|
|
}
|
|
if (it->second.gpu_modified) {
|
|
m_metadata_tracker.ForEachDownloadRange<true>(it->first, it->second.size,
|
|
[](uint64_t, uint64_t) noexcept {});
|
|
}
|
|
m_metadata_tracker.UntrackMemory(it->first, it->second.size);
|
|
it = m_surface_metas.erase(it);
|
|
}
|
|
m_memory_tracker.UntrackMemory(vaddr, size);
|
|
for (auto it = m_images.begin(); it != m_images.end();) {
|
|
if (!(**it).OverlapsRange(vaddr, size, false)) {
|
|
++it;
|
|
continue;
|
|
}
|
|
UnregisterImageLocked(**it, false);
|
|
it = m_images.erase(it);
|
|
}
|
|
}
|
|
|
|
VulkanImage* TextureCache::GetDummySampledTexture(bool uint_format, bool image_3d) {
|
|
KYTY_PROFILER_BLOCK("TextureCache::GetDummySampledTexture");
|
|
return m_dummy_textures->Get(g_render_ctx->GetGraphicCtx(), DummyTextureCache::Usage::Sampled,
|
|
uint_format, image_3d);
|
|
}
|
|
|
|
VulkanImage* TextureCache::GetDummyStorageTexture(bool uint_format, bool image_3d) {
|
|
KYTY_PROFILER_BLOCK("TextureCache::GetDummyStorageTexture");
|
|
return m_dummy_textures->Get(g_render_ctx->GetGraphicCtx(), DummyTextureCache::Usage::Storage,
|
|
uint_format, image_3d);
|
|
}
|
|
|
|
} // namespace Libs::Graphics
|