From 3d3e68797173b809a8bbaad359d2746a05cc6e2f Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 4 Oct 2026 19:50:47 +1300 Subject: [PATCH 1/2] fix(agc): address texture views that start past the surface's last mip PPSA21564 ends each of its two 1920x1080 RGBA16F bloom downsample chains (MAX_MIP 5) with a compute pass that stores level 6 through a view of levels 6..6, every frame from the PS logo on. The descriptor decode threw for such a view. GFX10 uses MAX_MIP only to place the levels: a level's address comes from the levels below it, and a level in the mip tail is a fixed slot of the tail block counted from the first tail level. A level past MAX_MIP is addressed the same way, so when the chain extended to the view's last level keeps every allocated level, the tail and the surface size unchanged, the view reaches that slot inside the surface's allocation. addrlib (GFX10, Navi10 with 16 pipes) computes the same 0x1640000-byte layout for 6, 7, 9 and 11 levels of this surface, with level 6 in the tail at (0, 32). The view then decodes with the extended chain over the same guest bytes. Views whose extra levels would move the allocated ones, such as linear chains or chains without a tail, still throw. --- .../Graphics/include/TextureTiling.hpp | 1 + .../Graphics/src/GuestTextureResource.cpp | 8 +- .../Graphics/src/TextureTiling.cpp | 19 +++ .../tests/GuestTextureResource.cpp | 52 ++++++++ .../tests/execution/ViewPastLastMip.cpp | 114 ++++++++++++++++++ 5 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ViewPastLastMip.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp index aad8c0ddc23..dbeeb0a9144 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp @@ -65,6 +65,7 @@ struct SurfaceGeometry { std::int32_t CopyDepth(std::uint32_t layer) const { return imageDepth > 1 ? static_cast(layer) : 0; } }; SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor); +bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index fc8dfba9acf..443517bba11 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -1,6 +1,7 @@ #include #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include #include #include @@ -129,10 +130,9 @@ GuestTextureResource DecodeTextureResource(std::span words) } catch (const std::exception& error) { throw std::runtime_error(std::string(error.what()) + describe()); } - Require(baseLevel <= maxMip, "guest texture descriptor starts past the surface's last mip level" + describe()); // Views may name levels past MAX_MIP (a 512x512 view through 1x1 over a 9-level surface); the // hardware never addresses them, so the view ends at the surface's last level. - lastLevel = std::min(lastLevel, maxMip); + if (baseLevel <= maxMip) lastLevel = std::min(lastLevel, maxMip); // XOR swizzles fold a pipe/bank XOR into the low address bits; only unmodified 64 KiB bases are modeled. Require(XorSwizzleMode(tileMode) == 0 || (baseAddress & 0xffffu) == 0, "guest texture descriptor combines an XOR swizzle with a pipe/bank XOR base which is not implemented"); @@ -176,6 +176,10 @@ GuestTextureResource DecodeTextureResource(std::span words) result.dccAddress = metaCompress ? metaAddr << 8u : 0u; result.dccAlphaOnMsb = dccAlphaPos; result.minLod = minLod; + if (baseLevel > maxMip) { + Require(LevelsFitAllocation(result, lastLevel + 1u), "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe()); + result.mipCount = lastLevel + 1u; + } return result; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 656239486e6..c5daed64e4c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -325,6 +325,25 @@ SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { return geometry; } +bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels) { + Require(levels >= surface.mipCount && levels <= 16u, "a view's mip chain must cover the surface's levels and at most 16"); + auto view = surface; + view.mipCount = levels; + const auto allocated = DescribeSurface(surface); + const auto extended = DescribeSurface(view); + if (allocated.guestBytes != extended.guestBytes || allocated.layerBytes != extended.layerBytes || allocated.layers != extended.layers) return false; + for (std::uint32_t level = 0; level < surface.mipCount; ++level) { + const auto& before = allocated.mips[level]; + const auto& after = extended.mips[level]; + if (before.tiledOffset != after.tiledOffset || before.tiledSize != after.tiledSize || before.blocksPerRow != after.blocksPerRow || before.tail != after.tail || before.tailX != after.tailX || before.tailY != after.tailY) return false; + } + for (auto level = surface.mipCount; level < levels; ++level) { + const auto& mip = extended.mips[level]; + if (mip.tiledOffset + mip.tiledSize > extended.layerBytes) return false; + } + return true; +} + std::vector ComputeElementMipLayout(TextureTileMode tileMode, std::uint32_t bytesPerElement, std::uint32_t width, std::uint32_t height, std::uint32_t mipCount) { Require(width != 0 && height != 0 && mipCount != 0 && mipCount <= 16u, "invalid surface mip chain"); if (tileMode == TextureTileMode::kLinear) return ComputeLinearMipLayout(bytesPerElement, 1u, 1u, width, height, mipCount); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 092a548c0cc..87e25e28ae7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -1,5 +1,6 @@ #include "GraphicsTests.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include #include #include @@ -269,6 +270,57 @@ void RunGuestTextureResourceTests() { const auto partial = DecodeTextureResource(pack(partialMips)); Require(partial.lastLevel == 1 && partial.mipCount == 3, "a view over part of the mip chain decoded wrongly"); + Fields pastLast = base; + pastLast.base40 = 0x55ea000ull; + pastLast.format = 71; + pastLast.width = 1920; + pastLast.height = 1080; + pastLast.tileModeRaw = 0x1b; + pastLast.maxMip = 5; + pastLast.baseLevel = 6; + pastLast.lastLevel = 6; + const auto tailView = DecodeTextureResource(pack(pastLast)); + Require(tailView.baseLevel == 6 && tailView.lastLevel == 6 && tailView.mipCount == 7, "a view one level past the last mip must address that level of the chain"); + auto allocated = tailView; + allocated.mipCount = 6; + const auto allocatedSurface = DescribeSurface(allocated); + const auto viewSurface = DescribeSurface(tailView); + Require(allocatedSurface.guestBytes == 0x1640000u && viewSurface.guestBytes == 0x1640000u, "a 1920x1080 64 bpp SW_64KB_R_X chain must take addrlib's 0x1640000 bytes with 6 and with 7 levels"); + constexpr std::array addrlibOffsets{0x650000u, 0x1d0000u, 0x90000u, 0x30000u, 0x10000u, 0u, 0u}; + for (std::uint32_t level = 0; level < 7; ++level) { + Require(viewSurface.mips[level].tiledOffset == addrlibOffsets[level] && viewSurface.mips[level].tail == (level >= 5), "the 7-level chain must place each level at its addrlib offset"); + if (level < 6) Require(allocatedSurface.mips[level].tiledOffset == addrlibOffsets[level] && allocatedSurface.mips[level].tail == viewSurface.mips[level].tail && allocatedSurface.mips[level].tailX == viewSurface.mips[level].tailX && allocatedSurface.mips[level].tailY == viewSurface.mips[level].tailY, "the level past the last mip must not move the allocated levels"); + } + Require(viewSurface.mips[5].tailX == 64 && viewSurface.mips[5].tailY == 0, "the last allocated level must sit in its addrlib tail slot"); + Require(viewSurface.mips[6].tailX == 0 && viewSurface.mips[6].tailY == 32, "the level past the last mip must sit in its addrlib tail slot"); + pastLast.lastLevel = 8; + const auto deeperView = DecodeTextureResource(pack(pastLast)); + Require(deeperView.mipCount == 9, "a view past the last mip must cover every level it names"); + const auto deeperSurface = DescribeSurface(deeperView); + Require(deeperSurface.guestBytes == 0x1640000u && deeperSurface.mips[7].tail && deeperSurface.mips[7].tailX == 32 && deeperSurface.mips[7].tailY == 0 && deeperSurface.mips[8].tail && deeperSurface.mips[8].tailX == 0 && deeperSurface.mips[8].tailY == 16, "levels 7 and 8 must sit in their addrlib tail slots"); + + Fields pastLinear = base; + pastLinear.maxMip = 1; + pastLinear.baseLevel = 2; + pastLinear.lastLevel = 2; + rejectFields(pastLinear, "would move the surface's own"); + auto linearChain = partial; + linearChain.mipCount = 2; + const auto linearAllocated = DescribeSurface(linearChain).guestBytes; + linearChain.mipCount = 3; + Require(linearAllocated == 0x1800u && DescribeSurface(linearChain).guestBytes == 0x1c00u, "a third level must grow a 16x16 32 bpp linear chain from addrlib's 0x1800 to 0x1c00 bytes"); + + Fields pastUntailed = pastLast; + pastUntailed.maxMip = 0; + pastUntailed.baseLevel = 1; + pastUntailed.lastLevel = 1; + rejectFields(pastUntailed, "would move the surface's own"); + auto untailedChain = tailView; + untailedChain.mipCount = 1; + const auto untailedAllocated = DescribeSurface(untailedChain).guestBytes; + untailedChain.mipCount = 2; + Require(untailedAllocated == 0xff0000u && DescribeSurface(untailedChain).guestBytes == 0x1470000u, "a second level must grow a 1920x1080 64 bpp SW_64KB_R_X surface from addrlib's 0xff0000 to 0x1470000 bytes"); + std::array shortWords{}; reject([&] { DecodeTextureResource(shortWords); }, "8 dwords"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ViewPastLastMip.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ViewPastLastMip.cpp new file mode 100644 index 00000000000..c2b535c5d3e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ViewPastLastMip.cpp @@ -0,0 +1,114 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 8; +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t MaxMip = 2; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t SurfaceBytes = 0x60000; +constexpr std::size_t TailBlockBytes = 0x10000; +constexpr std::uint32_t StoredPast = 0x12345678u; +constexpr std::uint32_t StoredLast = 0x9abcdef0u; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::array PastOffsets{0x4800, 0x4804, 0x4808, 0x480c, 0x4880, 0x4884, 0x4888, 0x488c}; +constexpr std::array LastOffsets{0x8400, 0x8404, 0x8408, 0x840c, 0x8480, 0x8484, 0x8488, 0x848c}; + +alignas(256) constexpr std::array StorePastCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, StoredPast, 0xf0200108, 0x00020102, 0xbf810000, +}; + +alignas(256) constexpr std::array StoreLastCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, StoredLast, 0xf0200108, 0x00020102, 0xbf810000, +}; + +std::array TextureDescriptor(const std::uint32_t* texels, std::uint32_t level) { + const auto address = static_cast(reinterpret_cast(texels)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (level << 12u) | (level << 16u) | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + MaxMip << 4u, + 0u, + 0u, + }; +} + +void Store(AgcDriver::VulkanDevice& device, const std::uint32_t* texels, std::uint32_t level, std::span code) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels, level); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::FlushCachedTextures(device.Device()); + } + device.WaitIdle(); +} + +void Check(const std::uint32_t* texels, bool last, const std::string& step) { + for (const auto offset : PastOffsets) Require(texels[offset / 4u] == StoredPast, step + ": byte offset " + std::to_string(offset) + " does not hold the store through level 3, past MAX_MIP 2, at its addrlib tail slot"); + for (const auto offset : LastOffsets) Require(texels[offset / 4u] == (last ? StoredLast : Untouched), step + ": byte offset " + std::to_string(offset) + " of level 2 holds an unexpected value"); + for (auto index = TailBlockBytes / 4u; index < SurfaceBytes / 4u; ++index) Require(texels[index] == Untouched, step + ": byte offset " + std::to_string(index * 4u) + " of levels 0 and 1 changed"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector storage((SurfaceBytes + 0x10000u) / 4u); + auto* texels = reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}); + std::fill(texels, texels + SurfaceBytes / 4u, Untouched); + { + GuestAllocations::Mutation mutation; + mutation.Add(texels, SurfaceBytes, true, true); + } + Store(*device, texels, 3u, StorePastCode); + Check(texels, false, "store through level 3"); + Store(*device, texels, 2u, StoreLastCode); + Check(texels, true, "store through level 2 after level 3"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(texels); + } + std::puts("view past the last mip tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From b6298bf738028817e28a18e19e6d4227d1b3d8fa Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 4 Oct 2026 19:50:47 +1300 Subject: [PATCH 2/2] fix(agc): keep one storage image for a surface viewed past its last mip The descriptors of one surface share its storage image through the surface key, which includes the level count. A view past MAX_MIP counts the extra levels, so its storage image was a second image of the same memory, and the two flushed and re-uploaded each other every frame (PPSA21564 writes level 6 of each 1920x1080 bloom chain after building levels 1 to 5 through MAX_MIP 5 views). A surface once viewed past its allocated levels now keeps a single image holding the extended chain, and its other descriptors look that image up; the allocated-level image it replaces is evicted with its results stored. The extra levels lie in the allocation (the decode proved that), so levels 0 to MAX_MIP keep their layout and the views of them read the same texels. --- .../Graphics/include/GuestTextureResource.hpp | 1 + .../Graphics/src/GuestTextureResource.cpp | 1 + .../Graphics/src/ShaderResources.cpp | 50 ++++++++++- .../prx/libSceAgcDriver/tests/Recorder.cpp | 86 +++++++++++++++++++ 4 files changed, 134 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index b09d3168c25..84583699ba3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -61,6 +61,7 @@ struct GuestTextureResource { std::uint64_t dccAddress = 0; bool dccAlphaOnMsb = false; std::uint32_t minLod = 0; + std::uint32_t allocatedMipCount = 0; }; float EffectiveMinLod(const GuestTextureResource& resource); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 443517bba11..a9954b1dad7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -164,6 +164,7 @@ GuestTextureResource DecodeTextureResource(std::span words) result.depthOrLastArray = depth; result.baseArray = baseArray; result.mipCount = maxMip + 1u; + result.allocatedMipCount = maxMip + 1u; result.baseLevel = baseLevel; result.lastLevel = lastLevel; result.tileMode = tileMode; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 5169e3ef870..60ab8c3ce9b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -511,22 +511,64 @@ std::array SurfaceKey(const Context& context, const GuestTextu return {static_cast(resource.baseAddress), static_cast(resource.baseAddress >> 32u), resource.width, resource.height, (resource.depthOrLastArray << 16u) | (resource.mipCount & 0xffffu), (static_cast(resource.tileMode) << 12u) | (static_cast(resource.dimension) << 20u), resource.baseArray, static_cast(StorageFormatForGuest(context, resource.format))}; } +struct ExtendedSurfaces { + std::mutex mutex; + std::map, std::uint32_t> levels; + std::atomic any{false}; +}; + +ExtendedSurfaces& ExtendedChains() { + static ExtendedSurfaces surfaces; + return surfaces; +} + +std::uint32_t AllocatedLevels(const GuestTextureResource& resource) { + return resource.allocatedMipCount != 0 ? resource.allocatedMipCount : resource.mipCount; +} + +GuestTextureResource StorageSurface(const Context& context, const GuestTextureResource& viewed) { + auto& surfaces = ExtendedChains(); + const bool extended = viewed.mipCount > AllocatedLevels(viewed); + if (!extended && !surfaces.any.load(std::memory_order_acquire)) return viewed; + auto surface = viewed; + surface.mipCount = AllocatedLevels(viewed); + const auto identity = SurfaceKey(context, surface); + std::lock_guard lock(surfaces.mutex); + if (extended) { + auto& levels = surfaces.levels[identity]; + levels = std::max(levels, viewed.mipCount); + surfaces.any.store(true, std::memory_order_release); + surface.mipCount = levels; + } else if (const auto it = surfaces.levels.find(identity); it != surfaces.levels.end()) { + surface.mipCount = it->second; + } else { + surface.mipCount = viewed.mipCount; + } + return surface; +} + // `guestBytes` is the surface size when the caller described the surface already (0: described here). -std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes) { - if (DepthSurfaceAt(resource.baseAddress)) { +std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) { + if (DepthSurfaceAt(viewed.baseAddress)) { char text[112]; - std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented", static_cast(resource.baseAddress)); + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented", static_cast(viewed.baseAddress)); throw std::runtime_error(text); } static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; - if (disabled) return std::make_shared(context, *context.detiler, resource, mip); + if (disabled) return std::make_shared(context, *context.detiler, viewed, mip); static_cast(words); const bool profile = LookupOutcomes::Profiled(); auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; auto& counters = TextureCounts(); + const auto resource = StorageSurface(context, viewed); const StorageKey key{context.device, SurfaceKey(context, resource)}; auto& cache = StorageTextures(); std::lock_guard lock(cache.mutex); + if (resource.mipCount > AllocatedLevels(resource)) { + auto allocated = resource; + allocated.mipCount = AllocatedLevels(resource); + if (const auto found = findStorage(cache, {context.device, SurfaceKey(context, allocated)}); found != cache.entries.end()) evictStorage(cache, found); + } auto it = findStorage(cache, key); if (it != cache.entries.end() && MetadataMoved(*it->texture, resource)) { evictStorage(cache, it); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 732e684a175..07a370f0e8b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -768,6 +768,91 @@ void movedMetadataTests(const Device& device, Recorder& recorder) { Require(holds(*back, {0x55, 0x55, 0x55, 0x55}), "the image remade under the first keys does not hold the stored texels"); } +void viewPastLastMipTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if (base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: views past the last mip not tested\n"; + return; + } + constexpr std::uint32_t side = 256; + constexpr std::size_t bytes = 0x60000; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(65536, bytes); +#endif + Require(block != nullptr, "cannot allocate the past-last-mip block"); + auto* texels = static_cast(block); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + ~Unregister() { + ClearCachedTextures(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + } + } unregister{base, block, address}; + if (HostImportFor(base, address, bytes) == nullptr) { + std::cout << "host import of the past-last-mip block refused: views past the last mip not tested\n"; + return; + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + const auto view = [&](std::uint32_t level) { + const std::array words{ + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (56u << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), + 0xfacu | (level << 12u) | (level << 16u) | (0x1bu << 20u) | (9u << 28u), + 0u, + 2u << 4u, + 0u, + 0u, + }; + return DecodeTextureResource(words); + }; + const auto texel = [&](const StorageTexture& image, std::uint32_t level) { + Buffer readback(context, 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, 1}; + copy.imageExtent = {1, 1, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image.Image(), VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + std::uint32_t value = 0; + std::memcpy(&value, readback.Bytes().data(), sizeof(value)); + return value; + }; + std::memset(texels, 0x55, bytes); + std::memset(texels + 0x8400, 0x66, 4); + std::memset(texels + 0x4800, 0x77, 4); + const auto allocatedView = view(2); + const auto pastView = view(3); + Require(allocatedView.mipCount == 3 && pastView.mipCount == 4 && DescribeSurface(pastView).guestBytes == bytes, "a 256x256 32 bpp SW_64KB_R_X surface viewed past MAX_MIP 2 decoded wrongly"); + const auto allocated = CachedStorageSurface(context, allocatedView); + Require(allocated->Descriptor().mipCount == 3, "the allocated levels' storage image has an unexpected level count"); + const auto past = CachedStorageSurface(context, pastView); + Require(past->Descriptor().mipCount == 4, "the storage image of a view past the last mip lacks the level it names"); + Require(!StorageImageCached(context, allocated.get()), "the allocated levels' image is still the surface's after a view past its last mip"); + Require(CachedStorageSurface(context, allocatedView) == past, "a view of the allocated levels took another image than the view past the last mip"); + Require(texel(*past, 2) == 0x66666666u, "level 2 was not read from its addrlib tail slot"); + Require(texel(*past, 3) == 0x77777777u, "the level past the last mip was not read from its addrlib tail slot"); +} + void resourceReadTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); if (context.hostImportAlignment == 0) { @@ -2455,6 +2540,7 @@ int main() { RunResidentPresentTests(device.GetContext()); storeRunTests(device, recorder); movedMetadataTests(device, recorder); + viewPastLastMipTests(device, recorder); keysFillTests(device, recorder); unitShadowTests(device, recorder); storageRefreshTests(device, recorder, false);