From bbebb6419b7891d634931c45e7ec005fc9e6c5ad Mon Sep 17 00:00:00 2001 From: nmzik Date: Sun, 13 Sep 2026 16:50:01 +0400 Subject: [PATCH] graphics: preserve PS5 array mip contents during cache promotion PPSA05975 --- .../host_gpu/renderer/cache/textureCache.cpp | 65 +++++------ .../host_gpu/renderer/image/imageInfo.h | 102 +++++++----------- tests/ShaderRecompilerComputeTests.cpp | 88 +++++++++++---- tests/shaderCfgTests.cpp | 87 ++++++++++++--- 4 files changed, 204 insertions(+), 138 deletions(-) diff --git a/src/graphics/host_gpu/renderer/cache/textureCache.cpp b/src/graphics/host_gpu/renderer/cache/textureCache.cpp index 9747d259..fe9801bf 100644 --- a/src/graphics/host_gpu/renderer/cache/textureCache.cpp +++ b/src/graphics/host_gpu/renderer/cache/textureCache.cpp @@ -810,17 +810,10 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques const bool safe_to_delete = current_tick - std::min(current_tick, cached.tick_accessed_last) > NumFramesBeforeRemoval; - if (requested.data.address == cached.info.data.address) { - const uint32_t requested_block = requested.bytes_per_block * requested.samples; - const uint32_t cached_block = cached.info.bytes_per_block * cached.info.samples; - if (requested.BlockExtent() != cached.info.BlockExtent() || - requested_block != cached_block) { - if (safe_to_delete) { - FreeImage(cached_id); - } - return {merged_id}; - } - + const uint32_t requested_block = requested.bytes_per_block * requested.samples; + const uint32_t cached_block = cached.info.bytes_per_block * cached.info.samples; + if (requested.data.address == cached.info.data.address && + requested.BlockExtent() == cached.info.BlockExtent() && requested_block == cached_block) { if (const auto depth_id = ResolveDepthOverlap(requested, binding, cached_id)) { return {depth_id}; } @@ -873,38 +866,27 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques static_cast(cached.info.tile_mode)); } - if (requested.data.address > cached.info.data.address) { - const int32_t mip = requested.MipOf(cached.info); - if (mip >= 0) { - const int32_t layer = requested.SliceOf(cached.info, mip); - if (layer >= 0) { - return {cached_id, mip, layer}; - } - } - if (safe_to_delete) { - FreeImage(cached_id); - } - return {}; + const int32_t requested_mip = requested.MipOf(cached.info); + if (requested_mip >= 0) { + const int32_t layer = requested.SliceOf(cached.info, requested_mip); + return {cached_id, requested_mip, layer}; } const int32_t mip = cached.info.MipOf(requested); if (mip >= 0) { const int32_t layer = cached.info.SliceOf(requested, mip); - if (layer >= 0) { - if (cached.binding.is_target) { - cached.binding.needs_rebind = true; - if (merged_id) { - m_slot_images[merged_id].binding.is_target = true; - } - FreeImage(cached_id); - return {merged_id}; - } - if (merged_id) { - CopyImageMip(merged_id, cached_id, static_cast(mip), - static_cast(layer)); - FreeImage(cached_id); - } + if (!merged_id) { + return {ExpandImage(requested, cached_id)}; } + cached.binding.needs_rebind |= cached.binding.is_bound || cached.binding.is_target; + m_slot_images[merged_id].binding.is_target |= cached.binding.is_target; + CopyImageMip(merged_id, cached_id, static_cast(mip), + static_cast(layer)); + FreeImage(cached_id); + return {merged_id}; + } + if (requested.data.address >= cached.info.data.address && safe_to_delete) { + FreeImage(cached_id); } return {merged_id}; } @@ -919,7 +901,14 @@ ImageId TextureCache::ExpandImage(const ImageInfo& info, ImageId source_id) { source.binding.needs_rebind = true; } InitializeImage(expanded_id); - CopyImage(expanded_id, source_id); + const int32_t mip = source.info.MipOf(info); + const int32_t layer = source.info.SliceOf(info, mip); + if (layer >= 0) { + CopyImageMip(expanded_id, source_id, static_cast(mip), + static_cast(layer)); + } else { + CopyImage(expanded_id, source_id); + } FreeImage(source_id); return expanded_id; } diff --git a/src/graphics/host_gpu/renderer/image/imageInfo.h b/src/graphics/host_gpu/renderer/image/imageInfo.h index a2c4d2c9..7919cb55 100644 --- a/src/graphics/host_gpu/renderer/image/imageInfo.h +++ b/src/graphics/host_gpu/renderer/image/imageInfo.h @@ -94,9 +94,24 @@ struct ImageInfo { bytes_per_block == other.bytes_per_block; } [[nodiscard]] int32_t MipOf(const ImageInfo& container) const noexcept { - if (!IsCompatible(container) || tile_mode != container.tile_mode || resources.levels != 1 || - container.resources.layers == 0 || - container.resources.levels > container.mip_layout.size()) { + if (container.resources.levels > container.mip_layout.size()) { + return -1; + } + for (uint32_t level = 0; level < container.resources.levels; level++) { + if (SliceOf(container, static_cast(level)) >= 0) { + return static_cast(level); + } + } + return -1; + } + [[nodiscard]] int32_t SliceOf(const ImageInfo& container, int32_t mip) const noexcept { + if (!IsCompatible(container) || tile_mode != container.tile_mode || type != container.type || + IsVolume() || resources.levels != 1 || resources.layers == 0 || + container.resources.layers == 0 || mip < 0 || + static_cast(mip) >= container.resources.levels || + container.resources.levels > container.mip_layout.size() || + !data.Valid() || !container.data.Valid() || data.address < container.data.address || + data.End() > container.data.End()) { return -1; } if (HasStencil() != container.HasStencil() || @@ -105,75 +120,36 @@ struct ImageInfo { return -1; } - int32_t mip = -1; - for (uint32_t level = 0; level < container.resources.levels; level++) { - const auto& layout = container.mip_layout[level]; - if (layout.size == 0 || layout.size % container.resources.layers != 0 || - container.data.address > UINT64_MAX - layout.offset) { - continue; - } - const auto mip_base = container.data.address + layout.offset; - const auto slice_size = layout.size / container.resources.layers; - if (slice_size == 0 || mip_base > UINT64_MAX - layout.size) { - continue; - } - const auto mip_end = mip_base + layout.size; - if (data.address >= mip_base && data.address < mip_end && - (data.address - mip_base) % slice_size == 0) { - mip = static_cast(level); - break; - } - } - if (mip < 0) { + const auto level = static_cast(mip); + const auto& layout = container.mip_layout[level]; + if (extent.width != std::max(container.extent.width >> level, 1u) || + extent.height != std::max(container.extent.height >> level, 1u) || + extent.depth != container.extent.depth || mip_layout[0].pitch != layout.pitch || + mip_layout[0].height != layout.height || layout.size == 0 || + layout.size % container.resources.layers != 0 || + container.data.size % container.resources.layers != 0 || + data.size % resources.layers != 0) { return -1; } - const auto level = static_cast(mip); - if (extent.width != std::max(container.extent.width >> level, 1u) || - extent.height != std::max(container.extent.height >> level, 1u)) { + // PS5 block slices contain the entire reversed mip chain. + const auto slice_stride = container.data.size / container.resources.layers; + const auto child_stride = data.size / resources.layers; + if (child_stride != layout.size / container.resources.layers || + layout.offset >= slice_stride || child_stride > slice_stride - layout.offset || + (resources.layers > 1 && child_stride != slice_stride)) { return -1; } - const auto mip_depth = std::max(container.extent.depth >> level, 1u); - if (container.type == Prospero::ImageType::kColor3D && - type == Prospero::ImageType::kColor2D) { - if (resources.layers != mip_depth) { - return -1; - } - } else if (type != container.type) { + const auto address_delta = data.address - container.data.address; + if (address_delta < layout.offset || (address_delta - layout.offset) % slice_stride != 0) { return -1; } - return mip; - } - [[nodiscard]] int32_t SliceOf(const ImageInfo& container, int32_t mip) const noexcept { - if (!IsCompatible(container) || type != container.type || mip < 0 || - static_cast(mip) >= container.resources.levels || - container.resources.levels > container.mip_layout.size() || - container.resources.layers == 0 || data.size == 0) { + const auto layer = (address_delta - layout.offset) / slice_stride; + if (layer > INT32_MAX || layer >= container.resources.layers || + resources.layers > container.resources.layers - layer) { return -1; } - const auto level = static_cast(mip); - if (extent.width != std::max(container.extent.width >> level, 1u) || - extent.height != std::max(container.extent.height >> level, 1u)) { - return -1; - } - const auto& layout = container.mip_layout[level]; - if (layout.size == 0 || layout.size % container.resources.layers != 0 || - container.data.address > UINT64_MAX - layout.offset) { - return -1; - } - const auto slice_size = layout.size / container.resources.layers; - if (slice_size == 0 || data.size % slice_size != 0) { - return -1; - } - const auto mip_base = container.data.address + layout.offset; - if (data.address < mip_base) { - return -1; - } - const auto address_delta = data.address - mip_base; - if (address_delta % data.size != 0 || address_delta / data.size > INT32_MAX) { - return -1; - } - return static_cast(address_delta / data.size); + return static_cast(layer); } }; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index c11e6b2a..c1e61703 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -4738,24 +4738,27 @@ public: "instead of being discarded and recreated"); ImageInfo chain = sampled.info; - chain.data = {0x10000, 0x8000}; - chain.extent = {8, 8, 1}; - chain.resources = {2, 4}; - chain.pitch = 8; - chain.mip_layout[0] = {0, 0x4000, 8, 8}; - chain.mip_layout[1] = {0x4000, 0x4000, 4, 4}; + chain.data = {0x2394c1000, 0x384000}; + chain.extent = {64, 64, 1}; + chain.resources = {7, 150}; + chain.pitch = 64; + chain.tile_mode = Prospero::TileMode::kStandard4KB; + chain.mip_layout[0] = {0x2000, 0x258000, 64, 64}; + chain.mip_layout[1] = {0x1000, 0x96000, 32, 32}; + for (uint32_t level = 2; level < chain.resources.levels; ++level) { + chain.mip_layout[level] = {0, 0x96000, 32, 32}; + } ImageInfo subresource = chain; - subresource.data = {0x16000, 0x1000}; - subresource.extent = {4, 4, 1}; + subresource.data = {0x2394c3000, 0x4000}; subresource.resources = {1, 1}; - subresource.pitch = 4; - subresource.mip_layout[0] = {0, 0x1000, 4, 4}; + subresource.mip_layout = {}; + subresource.mip_layout[0] = {0, 0x4000, 64, 64}; const auto mip = subresource.MipOf(chain); Require(name, "overlap resolution", - mip == 1 && subresource.SliceOf(chain, mip) == 2 && + mip == 0 && subresource.SliceOf(chain, mip) == 0 && ImageRangeOverlaps(chain.data, subresource.data) && ImagePageRangesOverlap(chain.data, subresource.data), - "normalized mip/slice overlap did not resolve"); + "captured PS5 atlas mip/slice overlap did not resolve"); auto exact_desc = compatible_desc; const auto exact = texture_cache.FindImage(exact_desc, true); @@ -10933,20 +10936,23 @@ public: stencil_layout.size, false), "depth prefetch performed final target acquisition"); - auto target_parent = - make_target_desc(base, target_mip_size * 2, {4, 4, 1}); + auto target_parent = make_target_desc(base, 1536, {4, 4, 1}); target_parent.type = BindingType::RenderTarget; target_parent.info.resources.levels = 2; - target_parent.info.mip_layout[0] = {0, target_mip_size, 4, 4}; - target_parent.info.mip_layout[1] = {target_mip_size, target_mip_size, 2, - 2}; + // PS5 linear rows align to 256 bytes; smaller mips precede larger ones. + target_parent.info.pitch = 64; + target_parent.info.mip_layout[0] = {512, 1024, 64, 4}; + target_parent.info.mip_layout[1] = {0, 512, 64, 2}; target_parent.view_info.usage = vk::ImageUsageFlagBits::eColorAttachment; auto target_base_subresource = - make_target_desc(base, target_mip_size, {4, 4, 1}); + make_target_desc(base + 512, 1024, {4, 4, 1}); + target_base_subresource.info.pitch = 64; + target_base_subresource.info.mip_layout[0] = {0, 1024, 64, 4}; const auto target_base_subresource_id = texture_cache.FindImage(target_base_subresource); - auto target_subresource = - make_target_desc(base + target_mip_size, target_mip_size, {2, 2, 1}); + auto target_subresource = make_target_desc(base, 512, {2, 2, 1}); + target_subresource.info.pitch = 64; + target_subresource.info.mip_layout[0] = {0, 512, 64, 2}; const auto target_subresource_id = texture_cache.FindImage(target_subresource); const auto *target_subresource_owner = @@ -10961,6 +10967,15 @@ public: !TextureCacheTestAccess::PendingDownload( texture_cache, target_subresource_id), "FindImage claimed RenderExecutor-owned render-target state"); + vk::ClearValue target_clear{}; + target_clear.color.uint32[0] = 0x12345678; + TextureCacheTestAccess::ClearImage( + texture_cache, scheduler.Current(), target_base_subresource_id, + {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, target_clear); + target_clear.color.uint32[0] = 0x89abcdef; + TextureCacheTestAccess::ClearImage( + texture_cache, scheduler.Current(), target_subresource_id, + {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, target_clear); RenderExecutorTestAccess::BindRenderTarget(executor, target_subresource_id); Require(name, "target prefetch purity", @@ -10982,6 +10997,34 @@ public: target_parent_id), "target overlap did not transfer target state to the merged " "owner"); + auto target_readback = CreateHostBuffer( + name, 2 * sizeof(u32), vk::BufferUsageFlagBits::eTransferDst, + {0xaaaaaaaa, 0xaaaaaaaa}); + const std::array target_probes{ + vk::BufferImageCopy{0, + 0, + 0, + {vk::ImageAspectFlagBits::eColor, 0, 0, 1}, + {}, + {1, 1, 1}}, + vk::BufferImageCopy{sizeof(u32), + 0, + 0, + {vk::ImageAspectFlagBits::eColor, 1, 0, 1}, + {}, + {1, 1, 1}}}; + texture_cache.GetImage(target_parent_id) + .Download(target_probes, target_readback.buffer, 0, + target_readback.size); + const vk::MemoryBarrier2 target_host_barrier{ + .srcStageMask = vk::PipelineStageFlagBits2::eCopy, + .srcAccessMask = vk::AccessFlagBits2::eTransferWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eHost, + .dstAccessMask = vk::AccessFlagBits2::eHostRead}; + vk::DependencyInfo target_dependency{}; + target_dependency.memoryBarrierCount = 1; + target_dependency.pMemoryBarriers = &target_host_barrier; + scheduler.Current().Handle().pipelineBarrier2(target_dependency); RenderColorInfo rebound_color{}; rebound_color.desc = target_subresource; rebound_color.image_id = target_subresource_id; @@ -10989,6 +11032,11 @@ public: rebound_depth.desc = depth; rebound_depth.image_id = depth_id; scheduler.Finish(); + Require(name, "promoted target mip contents", + ReadBuffer(name, target_readback, 2) == + std::vector{0x12345678, 0x89abcdef}, + "promotion discarded GPU contents from a child mip"); + DestroyBuffer(&target_readback); Require(name, "deferred target slot erasure", TextureCacheTestAccess::Owner(texture_cache, target_subresource_id) == nullptr, diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index 3aeaf626..75916773 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -1183,37 +1183,90 @@ void TestNativeShaderResourceDependencies() { } void TestNormalizedImageContracts() { + // Captured PS5 256x256 atlas: each layer is a 0x56000-byte mip chain. ImageInfo container{}; - container.data = {0x10000, 0x15000}; + container.data = {0x241741000, 0x2198000}; container.pixel_format = vk::Format::eR8G8B8A8Unorm; container.guest_format = Prospero::BufferFormat::k8_8_8_8UNorm; container.type = Prospero::ImageType::kColor2D; - container.extent = {64, 64, 1}; - container.resources = {3, 4}; - container.pitch = 64; + container.extent = {256, 256, 1}; + container.resources = {9, 100}; + container.pitch = 256; container.bytes_per_block = 4; container.samples = 1; - container.tile_mode = Prospero::TileMode::kStandard64KB; - container.mip_layout[0] = {0, 0x10000, 64, 64}; - container.mip_layout[1] = {0x10000, 0x4000, 32, 32}; - container.mip_layout[2] = {0x14000, 0x1000, 16, 16}; + container.tile_mode = Prospero::TileMode::kStandard4KB; + container.mip_layout[0] = {0x16000, 0x1900000, 256, 256}; + container.mip_layout[1] = {0x6000, 0x640000, 128, 128}; + container.mip_layout[2] = {0x2000, 0x190000, 64, 64}; + container.mip_layout[3] = {0x1000, 0x64000, 32, 32}; + for (uint32_t level = 4; level < container.resources.levels; ++level) { + container.mip_layout[level] = {0, 0x64000, 32, 32}; + } ImageInfo subresource = container; - subresource.data = {0x22000, 0x1000}; - subresource.extent = {32, 32, 1}; + subresource.data = {0x2417ad000, 0x40000}; subresource.resources = {1, 1}; - subresource.pitch = 32; subresource.mip_layout = {}; - subresource.mip_layout[0] = {0, 0x1000, 32, 32}; + subresource.mip_layout[0] = {0, 0x40000, 256, 256}; - Check(container.BlockExtent() == vk::Extent2D{64, 64}, + Check(container.BlockExtent() == vk::Extent2D{256, 256}, "normalized image block extent changed"); Check(subresource.IsCompatible(container), "normalized compatible image was rejected"); - Check(subresource.MipOf(container) == 1, - "normalized mip lookup missed a subresource"); - Check(subresource.SliceOf(container, 1) == 2, - "normalized slice lookup missed a subresource"); + Check(subresource.MipOf(container) == 0 && + subresource.SliceOf(container, 0) == 1, + "PS5 mip-chain stride did not resolve the captured second array layer"); + + auto smaller = subresource; + smaller.data = {0x241747000, 0x10000}; + smaller.extent = {128, 128, 1}; + smaller.pitch = 128; + smaller.mip_layout[0] = {0, 0x10000, 128, 128}; + Check(smaller.MipOf(container) == 1 && smaller.SliceOf(container, 1) == 0, + "PS5 reversed mip ordering did not resolve the captured smaller mip"); + + auto boundary = subresource; + boundary.data.address = container.data.address + 99 * 0x56000 + 0x16000; + Check(boundary.SliceOf(container, 0) == 99, + "PS5 array lookup rejected the final complete layer"); + boundary.data.address += 0x56000; + Check(boundary.MipOf(container) == -1, + "PS5 array lookup accepted a layer beyond the parent range"); + boundary = subresource; + boundary.data.address += 0x1000; + Check(boundary.MipOf(container) == -1, + "PS5 array lookup accepted a misaligned layer address"); + boundary = subresource; + boundary.resources.layers = 2; + boundary.data.size *= 2; + boundary.mip_layout[0].size *= 2; + Check(boundary.MipOf(container) == -1, + "PS5 array lookup accepted contiguous child layers with a different stride"); + boundary = subresource; + boundary.mip_layout[0].pitch *= 2; + Check(boundary.MipOf(container) == -1, + "PS5 array lookup accepted a different storage pitch"); + + auto small_atlas = container; + small_atlas.data = {0x239001000, 0x64000}; + small_atlas.extent = {32, 32, 1}; + small_atlas.pitch = 32; + small_atlas.resources = {6, 50}; + small_atlas.mip_layout[0] = {0x1000, 0x32000, 32, 32}; + for (uint32_t level = 1; level < small_atlas.resources.levels; ++level) { + small_atlas.mip_layout[level] = {0, 0x32000, 32, 32}; + } + auto small_child = subresource; + small_child.data = {0x23900e000, 0x1000}; + small_child.extent = {32, 32, 1}; + small_child.pitch = 32; + small_child.mip_layout[0] = {0, 0x1000, 32, 32}; + Check(small_child.MipOf(small_atlas) == 0 && + small_child.SliceOf(small_atlas, 0) == 6, + "PS5 array lookup doubled the captured small-atlas layer index"); + small_child.data.address = 0x23900d000; + Check(small_child.MipOf(small_atlas) == -1, + "PS5 array lookup treated a packed tail block as the full-size mip"); auto incompatible = subresource; incompatible.samples = 2;