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/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..a9954b1dad7 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"); @@ -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; @@ -176,6 +177,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/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/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/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); 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; + } +}