Preserve storage images across render target transitions

This commit is contained in:
nmzik
2026-07-16 19:43:28 +02:00
parent 9bed3bddd6
commit f743a48c03
4 changed files with 172 additions and 27 deletions
+28 -1
View File
@@ -173,7 +173,13 @@ struct VideoOutPixelFormatInfo {
enum class DepthOverlap : uint8_t { None, RetireSampled, Unsupported };
enum class DepthTransitionSource : uint8_t { None, Guest, Native };
enum class RenderTargetOverlap : uint8_t { None, RetireSampled, RetireTarget, Unsupported };
enum class RenderTargetOverlap : uint8_t {
None,
RetireSampled,
PreserveStorage,
RetireTarget,
Unsupported
};
enum class SampledOverlap : uint8_t { None, ReadOnlyAlias, Unsupported };
enum class StorageSampledOverlap : uint8_t { None, ExactImage, Unsupported };
enum class HostWriteOverlap : uint8_t { None, InvalidateImage, Unsupported };
@@ -474,6 +480,27 @@ ClassifyRenderTargetOverlap(const ImageInfo& sampled, bool sampled_gpu_modified,
: RenderTargetOverlap::Unsupported;
}
[[nodiscard]] inline RenderTargetOverlap ClassifyStorageRenderTargetOverlap(
const ImageInfo& storage, VkFormat storage_format, bool storage_gpu_modified,
bool storage_buffer_modified, bool storage_cpu_dirty, bool same_context,
const RenderTargetInfo& target) {
if (!ImagePageRangesOverlap(storage.address, storage.size, target.address, target.size)) {
return RenderTargetOverlap::None;
}
const bool exact_native_image =
storage.address == target.address && storage.size == target.size &&
storage_format == target.format && storage.width == target.width &&
storage.height == target.height && storage.pitch == target.pitch &&
storage.base_level == 0 && storage.levels == 1 && storage.view_levels == 1 &&
storage.tile == target.tile_mode && storage.depth == 1 &&
storage.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor2D) &&
storage.base_array == 0 && target.levels == 1;
return exact_native_image && storage_gpu_modified && !storage_buffer_modified &&
!storage_cpu_dirty && same_context
? RenderTargetOverlap::PreserveStorage
: RenderTargetOverlap::Unsupported;
}
[[nodiscard]] inline bool IsRgba8SrgbViewFormat(VkFormat format) noexcept {
return format == VK_FORMAT_R8G8B8A8_UNORM || format == VK_FORMAT_R8G8B8A8_SRGB ||
format == VK_FORMAT_B8G8R8A8_UNORM || format == VK_FORMAT_B8G8R8A8_SRGB;
+96 -25
View File
@@ -527,13 +527,15 @@ struct TextureCache::ReadbackWorker {
std::thread thread;
};
void TextureCache::RetireImages(const std::vector<CachedImage*>& retire) {
void TextureCache::RetireImages(const std::vector<CachedImage*>& retire,
const CachedImage* native_storage_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;
size_t removed = 0;
bool native_storage_retired = false;
for (auto it = m_images.begin(); it != m_images.end();) {
if (std::find(retire.begin(), retire.end(), it->get()) == retire.end()) {
++it;
@@ -541,7 +543,22 @@ void TextureCache::RetireImages(const std::vector<CachedImage*>& retire) {
}
const bool sampled = (*it)->kind == CachedImage::Kind::Texture;
const bool target = (*it)->kind == CachedImage::Kind::RenderTarget;
if ((!sampled && !target) || (*it)->gpu_modified || (target && (*it)->buffer_modified)) {
const bool native_storage = it->get() == native_storage_source;
if (native_storage) {
if ((*it)->kind != CachedImage::Kind::StorageTexture || !(*it)->gpu_modified ||
(*it)->buffer_modified || (*it)->info.IsCpuDirty() ||
!m_memory_tracker.IsRegionGpuModified((*it)->Address(), (*it)->Size())) {
EXIT("TextureCache: invalid native storage-image retirement, kind=%u "
"gpu_modified=%d buffer_modified=%d cpu_dirty=%d tracker_gpu_modified=%d\n",
static_cast<uint32_t>((*it)->kind), (*it)->gpu_modified,
(*it)->buffer_modified, (*it)->info.IsCpuDirty(),
m_memory_tracker.IsRegionGpuModified((*it)->Address(), (*it)->Size()));
}
(*it)->gpu_modified = false;
native_storage_retired = true;
}
if ((!sampled && !target && !native_storage) || (*it)->gpu_modified ||
(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);
@@ -553,7 +570,11 @@ void TextureCache::RetireImages(const std::vector<CachedImage*>& retire) {
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
(*it)->Address(range), (*it)->Size(range));
}
m_memory_tracker.UntrackMemory((*it)->Address(range), (*it)->Size(range));
// An exact storage-to-target transition transfers this registration to the new cache
// record. Keeping it GPU-owned also keeps guest page protection continuous.
if (!native_storage) {
m_memory_tracker.UntrackMemory((*it)->Address(range), (*it)->Size(range));
}
}
it = m_images.erase(it);
removed++;
@@ -562,6 +583,9 @@ void TextureCache::RetireImages(const std::vector<CachedImage*>& retire) {
EXIT("TextureCache: image retirement set mismatch, requested=%zu removed=%zu\n",
retire.size(), removed);
}
if (native_storage_source != nullptr && !native_storage_retired) {
EXIT("TextureCache: native storage-image retirement source was not retired\n");
}
}
namespace {
@@ -1759,25 +1783,53 @@ RenderTextureVulkanImage* TextureCache::FindRenderTarget(CommandBuffer*
command->RetainResourceUntilFence(match);
return static_cast<RenderTextureVulkanImage*>(match->image);
}
std::vector<CachedImage*> retire;
std::vector<CachedImage*> retire;
std::shared_ptr<CachedImage> native_storage_source;
for (const auto& entry: m_images) {
auto& cached = *entry;
if (!cached.OverlapsRange(info.address, info.size, true)) {
continue;
}
const auto overlap =
cached.kind == CachedImage::Kind::Texture
? ClassifyRenderTargetOverlap(cached.info, cached.gpu_modified, cached.ctx == ctx,
info)
: cached.kind == CachedImage::Kind::RenderTarget
? ClassifyRenderTargetOverlap(cached.target, cached.gpu_modified,
cached.buffer_modified, cached.ctx == ctx, info)
: RenderTargetOverlap::Unsupported;
const bool retire_sampled = cached.kind == CachedImage::Kind::Texture &&
overlap == RenderTargetOverlap::RetireSampled;
const bool retire_target = cached.kind == CachedImage::Kind::RenderTarget &&
overlap == RenderTargetOverlap::RetireTarget;
if (!retire_sampled && !retire_target) {
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),
cached.ctx == ctx, info);
break;
case CachedImage::Kind::RenderTarget:
overlap = ClassifyRenderTargetOverlap(cached.target, cached.gpu_modified,
cached.buffer_modified, cached.ctx == ctx, info);
break;
case CachedImage::Kind::DepthTarget:
case CachedImage::Kind::VideoOut: break;
}
bool supported = false;
switch (overlap) {
case RenderTargetOverlap::RetireSampled:
supported = cached.kind == CachedImage::Kind::Texture;
break;
case RenderTargetOverlap::PreserveStorage:
supported = cached.kind == CachedImage::Kind::StorageTexture &&
native_storage_source == nullptr;
if (supported) {
native_storage_source = entry;
}
break;
case RenderTargetOverlap::RetireTarget:
supported = cached.kind == CachedImage::Kind::RenderTarget;
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"
@@ -1809,18 +1861,37 @@ RenderTextureVulkanImage* TextureCache::FindRenderTarget(CommandBuffer*
info.address, info.size, retired.address, retired.size, retained.address,
retained.size);
}
RetireImages(retire);
RetireImages(retire, native_storage_source.get());
auto cached = std::make_shared<CachedImage>();
cached->kind = CachedImage::Kind::RenderTarget;
cached->target = info;
cached->ctx = ctx;
cached->image = CreateRenderTarget(ctx, info, &cached->memory);
m_memory_tracker.ForEachUploadRange(
info.address, info.size, false, [](uint64_t, uint64_t) noexcept {},
[&]() noexcept {
UploadRenderTarget(ctx, static_cast<RenderTextureVulkanImage*>(cached->image), info,
false);
});
const bool preserve_native = native_storage_source != nullptr;
if (preserve_native) {
command->RetainResourceUntilFence(native_storage_source);
ImageImageCopy region {};
region.src_image = native_storage_source->image;
region.src_level = 0;
region.dst_level = 0;
region.width = info.width;
region.height = info.height;
UtilImageToImage(command, std::span<const ImageImageCopy>(&region, 1), cached->image,
static_cast<uint64_t>(RENDER_COLOR_IMAGE_LAYOUT));
} else {
m_memory_tracker.ForEachUploadRange(
info.address, info.size, false, [](uint64_t, uint64_t) noexcept {},
[&]() noexcept {
UploadRenderTarget(ctx, static_cast<RenderTextureVulkanImage*>(cached->image), info,
false);
});
}
cached->gpu_modified = preserve_native;
if (preserve_native) {
LOGF("TextureCache: preserved storage image through native render-target copy, "
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 " format=%d extent=%ux%u\n",
info.address, info.size, static_cast<int>(info.format), info.width, info.height);
}
auto* image = static_cast<RenderTextureVulkanImage*>(cached->image);
command->RetainResourceUntilFence(cached);
m_images.push_back(std::move(cached));
@@ -115,7 +115,8 @@ private:
uint64_t size) const;
void RequireNoMetaOverlapLocked(uint64_t vaddr, uint64_t size) const;
void MarkSampledAliasesCpuDirtyLocked(uint64_t vaddr, uint64_t size);
void RetireImages(const std::vector<CachedImage*>& retire);
void RetireImages(const std::vector<CachedImage*>& retire,
const CachedImage* native_storage_source = nullptr);
void SynchronizeRenderTargetToBufferLocked(CachedImage& cached);
Common::Mutex m_dummy_mutex;