From b521e1f811ed6423ccc8d812e283ff644519132b Mon Sep 17 00:00:00 2001 From: boykopovar Date: Wed, 23 Sep 2026 10:57:27 +0300 Subject: [PATCH] test(agc): add texture resource and detiling coverage --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 114 +++++++- .../libSceAgcDriver/tests/GraphicsTests.hpp | 36 +++ .../tests/GuestSamplerResource.cpp | 210 ++++++++++++++ .../tests/GuestTextureResource.cpp | 267 ++++++++++++++++++ .../libSceAgcDriver/tests/TextureDetiler.cpp | 102 +++++++ .../libSceAgcDriver/tests/TextureFormat.cpp | 54 ++++ .../libSceAgcDriver/tests/TextureTiling.cpp | 91 ++++++ 8 files changed, 878 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 9ee0797d4..591c37025 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -184,6 +184,11 @@ add_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL tests/Graphics.cpp tests/BdaResources.cpp tests/GuestAllocations.cpp + tests/TextureFormat.cpp + tests/TextureTiling.cpp + tests/GuestTextureResource.cpp + tests/GuestSamplerResource.cpp + tests/TextureDetiler.cpp Graphics/src/State.cpp Graphics/src/Resources.cpp Graphics/src/ShaderResources.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 66e1ae06f..b09c1be64 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1,5 +1,7 @@ #include "BdaTests.hpp" +#include "GraphicsTests.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include @@ -269,6 +271,12 @@ struct MockVulkan { std::uint32_t boundSets = 0; std::uint32_t boundFirst = 0; VkPipelineBindPoint boundPoint = VK_PIPELINE_BIND_POINT_MAX_ENUM; + std::map pipelineLayoutPushConstantSize; + std::uint32_t pipelineCreateCount = 0; + std::vector> pipelineSpecializations; + VkPipeline boundPipeline = VK_NULL_HANDLE; + std::vector lastPushConstants; + struct { std::uint32_t x = 0, y = 0, z = 0; } lastDispatchGroups; }; MockVulkan mock; @@ -377,6 +385,57 @@ VKAPI_ATTR VkDeviceAddress VKAPI_CALL mockGetBufferDeviceAddress(VkDevice, const return 0x100000000000ULL + reinterpret_cast(info->buffer) * 0x10000; } +VKAPI_ATTR VkResult VKAPI_CALL mockCreatePipelineLayout(VkDevice, const VkPipelineLayoutCreateInfo* info, const VkAllocationCallbacks*, VkPipelineLayout* layout) { + *layout = makeHandle(); + mock.pipelineLayoutPushConstantSize[*layout] = info->pushConstantRangeCount > 0 ? info->pPushConstantRanges[0].size : 0; + ++mock.live; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockDestroyPipelineLayout(VkDevice, VkPipelineLayout, const VkAllocationCallbacks*) { + --mock.live; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockCreateShaderModule(VkDevice, const VkShaderModuleCreateInfo*, const VkAllocationCallbacks*, VkShaderModule* module) { + *module = makeHandle(); + ++mock.live; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockDestroyShaderModule(VkDevice, VkShaderModule, const VkAllocationCallbacks*) { + --mock.live; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockCreateComputePipelines(VkDevice, VkPipelineCache, std::uint32_t count, const VkComputePipelineCreateInfo* infos, const VkAllocationCallbacks*, VkPipeline* pipelines) { + Require(count == 1, "mock expects exactly one compute pipeline per call"); + Require(infos[0].stage.pSpecializationInfo != nullptr, "compute pipeline must provide specialization data"); + std::array values{}; + Require(infos[0].stage.pSpecializationInfo->dataSize == sizeof(values), "compute pipeline specialization data has an unexpected size"); + std::memcpy(values.data(), infos[0].stage.pSpecializationInfo->pData, sizeof(values)); + *pipelines = makeHandle(); + mock.pipelineSpecializations.push_back(values); + ++mock.pipelineCreateCount; + ++mock.live; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockDestroyPipeline(VkDevice, VkPipeline, const VkAllocationCallbacks*) { + --mock.live; +} + +VKAPI_ATTR void VKAPI_CALL mockCmdBindPipeline(VkCommandBuffer, VkPipelineBindPoint, VkPipeline pipeline) { + mock.boundPipeline = pipeline; +} + +VKAPI_ATTR void VKAPI_CALL mockCmdPushConstants(VkCommandBuffer, VkPipelineLayout, VkShaderStageFlags, std::uint32_t, std::uint32_t size, const void* values) { + const auto* bytes = static_cast(values); + mock.lastPushConstants.assign(bytes, bytes + size); +} + +VKAPI_ATTR void VKAPI_CALL mockCmdDispatch(VkCommandBuffer, std::uint32_t x, std::uint32_t y, std::uint32_t z) { + mock.lastDispatchGroups = {x, y, z}; +} + PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { static const std::map table{ {"vkGetBufferDeviceAddressKHR", reinterpret_cast(mockGetBufferDeviceAddress)}, @@ -394,7 +453,16 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { {"vkDestroyDescriptorPool", reinterpret_cast(mockDestroyDescriptorPool)}, {"vkAllocateDescriptorSets", reinterpret_cast(mockAllocateDescriptorSets)}, {"vkUpdateDescriptorSets", reinterpret_cast(mockUpdateDescriptorSets)}, - {"vkCmdBindDescriptorSets", reinterpret_cast(mockCmdBindDescriptorSets)} + {"vkCmdBindDescriptorSets", reinterpret_cast(mockCmdBindDescriptorSets)}, + {"vkCreatePipelineLayout", reinterpret_cast(mockCreatePipelineLayout)}, + {"vkDestroyPipelineLayout", reinterpret_cast(mockDestroyPipelineLayout)}, + {"vkCreateShaderModule", reinterpret_cast(mockCreateShaderModule)}, + {"vkDestroyShaderModule", reinterpret_cast(mockDestroyShaderModule)}, + {"vkCreateComputePipelines", reinterpret_cast(mockCreateComputePipelines)}, + {"vkDestroyPipeline", reinterpret_cast(mockDestroyPipeline)}, + {"vkCmdBindPipeline", reinterpret_cast(mockCmdBindPipeline)}, + {"vkCmdPushConstants", reinterpret_cast(mockCmdPushConstants)}, + {"vkCmdDispatch", reinterpret_cast(mockCmdDispatch)} }; const auto it = table.find(name); return it == table.end() ? nullptr : it->second; @@ -983,6 +1051,50 @@ int main() { }); Require(mock.live == 0, "BDA resources leaked Vulkan objects"); RunGuestAllocationTests(); + RunTextureFormatTests(); + RunTextureTilingTests(); + RunGuestTextureResourceTests(); + RunGuestSamplerResourceTests(); + mock = MockVulkan{}; + auto textureDetilerContext = mockContext(); + textureDetilerContext.limits.minStorageBufferOffsetAlignment = 16; + textureDetilerContext.limits.maxStorageBufferRange = 256; + RunTextureDetilerTests(textureDetilerContext, { + [](std::uint64_t size) { + VkBuffer buffer{}; + VkBufferCreateInfo bufferInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bufferInfo.size = size; + bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + mockCreateBuffer(VK_NULL_HANDLE, &bufferInfo, nullptr, &buffer); + VkMemoryRequirements requirements{}; + mockGetBufferMemoryRequirements(VK_NULL_HANDLE, buffer, &requirements); + VkMemoryAllocateInfo allocationInfo{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocationInfo.allocationSize = requirements.size; + VkDeviceMemory memory{}; + mockAllocateMemory(VK_NULL_HANDLE, &allocationInfo, nullptr, &memory); + mockBindBufferMemory(VK_NULL_HANDLE, buffer, memory, 0); + return buffer; + }, + [](VkBuffer buffer) -> std::vector& { return mock.memories.at(mock.bufferMemory.at(buffer)); }, + [] { + Require(mock.writes.size() >= 2, "expected texture detiling descriptor writes"); + const auto& destinationWrite = mock.writes.back(); + const auto& sourceWrite = mock.writes[mock.writes.size() - 2]; + DetilerCapture capture{}; + capture.groupsX = mock.lastDispatchGroups.x; + capture.groupsY = mock.lastDispatchGroups.y; + capture.groupsZ = mock.lastDispatchGroups.z; + capture.pushConstants = mock.lastPushConstants; + capture.sourceBuffer = sourceWrite.buffers.at(0).buffer; + capture.sourceOffset = sourceWrite.buffers.at(0).offset; + capture.sourceRange = sourceWrite.buffers.at(0).range; + capture.destinationBuffer = destinationWrite.buffers.at(0).buffer; + capture.destinationOffset = destinationWrite.buffers.at(0).offset; + capture.destinationRange = destinationWrite.buffers.at(0).range; + return capture; + }, + [] { return mock.pipelineCreateCount; } + }); std::cout << "Graphics validation tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp b/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp new file mode 100644 index 000000000..bad23cbd3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp @@ -0,0 +1,36 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_GRAPHICSTESTS_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_GRAPHICSTESTS_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include + +struct DetilerCapture { + std::uint32_t groupsX; + std::uint32_t groupsY; + std::uint32_t groupsZ; + std::vector pushConstants; + VkBuffer sourceBuffer; + VkDeviceSize sourceOffset; + VkDeviceSize sourceRange; + VkBuffer destinationBuffer; + VkDeviceSize destinationOffset; + VkDeviceSize destinationRange; +}; + +struct TextureDetilerTestAccess { + std::function makeBuffer; + std::function&(VkBuffer)> bytes; + std::function lastDispatch; + std::function pipelineCount; +}; + +void RunTextureFormatTests(); +void RunTextureTilingTests(); +void RunGuestTextureResourceTests(); +void RunGuestSamplerResourceTests(); +void RunTextureDetilerTests(const AgcDriver::Graphics::Context& context, const TextureDetilerTestAccess& access); + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp new file mode 100644 index 000000000..14fee717c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp @@ -0,0 +1,210 @@ +#include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp" +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +struct Fields { + std::uint32_t clampX = 2; + std::uint32_t clampY = 2; + std::uint32_t clampZ = 2; + std::uint32_t maxAnisoRatio = 0; + std::uint32_t depthCompareFunc = 0; + bool forceUnormCoords = false; + std::uint32_t anisoThreshold = 0; + bool forceSrgb = false; + std::uint32_t anisoBias = 0; + bool truncCoord = false; + bool disableCubeWrap = false; + std::uint32_t filterMode = 0; + bool disableDegamma = false; + std::uint32_t minLodRaw = 0; + std::uint32_t maxLodRaw = 0x3c00; + std::uint32_t perfMip = 0; + std::uint32_t perfZ = 0; + std::uint32_t lodBiasRaw = 0; + std::uint32_t lodBiasSec = 0; + std::uint32_t xyMagFilter = 1; + std::uint32_t xyMinFilter = 1; + std::uint32_t zFilter = 1; + std::uint32_t mipFilter = 2; + bool pointPreclamp = false; + bool anisoOverride = false; + bool blendZeroPrt = false; + std::uint32_t borderColorType = 0; +}; + +std::array pack(const Fields& f) { + std::array words{}; + words[0] = (f.clampX & 0x7u) | ((f.clampY & 0x7u) << 3u) | ((f.clampZ & 0x7u) << 6u) | ((f.maxAnisoRatio & 0x7u) << 9u) + | ((f.depthCompareFunc & 0x7u) << 12u) | ((f.forceUnormCoords ? 1u : 0u) << 15u) | ((f.anisoThreshold & 0x7u) << 16u) + | ((f.forceSrgb ? 1u : 0u) << 20u) | ((f.anisoBias & 0x3fu) << 21u) | ((f.truncCoord ? 1u : 0u) << 27u) + | ((f.disableCubeWrap ? 1u : 0u) << 28u) | ((f.filterMode & 0x3u) << 29u) | ((f.disableDegamma ? 1u : 0u) << 31u); + words[1] = (f.minLodRaw & 0xfffu) | ((f.maxLodRaw & 0xfffu) << 12u) | ((f.perfMip & 0xfu) << 24u) | ((f.perfZ & 0xfu) << 28u); + words[2] = (f.lodBiasRaw & 0x3fffu) | ((f.lodBiasSec & 0x3fu) << 14u) | ((f.xyMagFilter & 0x3u) << 20u) + | ((f.xyMinFilter & 0x3u) << 22u) | ((f.zFilter & 0x3u) << 24u) | ((f.mipFilter & 0x3u) << 26u) + | ((f.pointPreclamp ? 1u : 0u) << 28u) | ((f.anisoOverride ? 1u : 0u) << 29u) | ((f.blendZeroPrt ? 1u : 0u) << 30u); + words[3] = (f.borderColorType & 0x3u) << 30u; + return words; +} + +template +void reject(TAction action, std::string_view reason) { + try { + action(); + } catch (const std::runtime_error& error) { + Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string("unexpected guest sampler test error: ") + error.what()); + return; + } + throw std::runtime_error(std::string("expected guest sampler rejection: ") + std::string(reason)); +} + +void rejectFields(const Fields& f, std::string_view reason) { + const auto words = pack(f); + reject([&] { DecodeSamplerResource(words); }, reason); +} + +bool nearlyEqual(float a, float b) { + return std::fabs(a - b) < 0.001f; +} + +} + +void RunGuestSamplerResourceTests() { + Fields base; + auto result = DecodeSamplerResource(pack(base)); + Require(result.magFilter == VK_FILTER_LINEAR && result.minFilter == VK_FILTER_LINEAR, "linear filter fields decoded incorrectly"); + Require(result.mipmapMode == VK_SAMPLER_MIPMAP_MODE_LINEAR, "linear mip filter must decode to linear mipmap mode"); + Require(result.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, "clamp mode 2 must decode to clamp-to-edge"); + Require(!result.anisotropyEnable && result.maxAnisotropy == 1.0f, "non-anisotropic filter must leave anisotropy disabled"); + Require(nearlyEqual(result.maxLod, static_cast(0x3c00) / 256.0f), "max LOD decoded incorrectly"); + Require(result.borderColor == VK_BORDER_COLOR_INT_TRANSPARENT_BLACK, "border color type 0 must decode to transparent black"); + + Fields nearest = base; + nearest.xyMagFilter = 0; + nearest.xyMinFilter = 0; + nearest.zFilter = 0; + nearest.mipFilter = 0; + result = DecodeSamplerResource(pack(nearest)); + Require(result.magFilter == VK_FILTER_NEAREST && result.minFilter == VK_FILTER_NEAREST, "filter 0 must decode to nearest"); + Require(result.mipmapMode == VK_SAMPLER_MIPMAP_MODE_NEAREST, "mip filter 0 must decode to nearest mipmap mode"); + Require(result.minLod == 0.0f && result.maxLod == 0.0f, "disabled mip filtering must clear LOD range"); + + Fields addressModes = base; + addressModes.clampX = 0; + addressModes.clampY = 1; + addressModes.clampZ = 4; + result = DecodeSamplerResource(pack(addressModes)); + Require(result.addressModeU == VK_SAMPLER_ADDRESS_MODE_REPEAT, "clamp mode 0 must decode to repeat"); + Require(result.addressModeV == VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT, "clamp mode 1 must decode to mirrored repeat"); + Require(result.addressModeW == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, "clamp mode 4 must decode to clamp-to-border"); + + Fields aniso = base; + aniso.xyMagFilter = 2; + aniso.xyMinFilter = 2; + aniso.zFilter = 2; + aniso.maxAnisoRatio = 3; + result = DecodeSamplerResource(pack(aniso)); + Require(result.anisotropyEnable, "anisotropic filter must enable anisotropy"); + Require(nearlyEqual(result.maxAnisotropy, 8.0f), "anisotropy ratio 3 must decode to 8x"); + aniso.maxAnisoRatio = 5; + rejectFields(aniso, "unknown anisotropy ratio"); + + Fields badLod = base; + badLod.minLodRaw = 100; + badLod.maxLodRaw = 50; + rejectFields(badLod, "minimum LOD past its maximum LOD"); + + Fields badZFilter = base; + badZFilter.zFilter = 0; + rejectFields(badZFilter, "differs from the minification filter"); + + Fields badMipFilter = base; + badMipFilter.mipFilter = 3; + rejectFields(badMipFilter, "unknown mip filter"); + + Fields badDepthCompare = base; + badDepthCompare.depthCompareFunc = 1; + rejectFields(badDepthCompare, "depth comparison"); + + Fields badUnorm = base; + badUnorm.forceUnormCoords = true; + rejectFields(badUnorm, "unnormalized coordinates"); + + Fields badThreshold = base; + badThreshold.anisoThreshold = 1; + rejectFields(badThreshold, "anisotropy threshold override"); + + Fields badSrgb = base; + badSrgb.forceSrgb = true; + rejectFields(badSrgb, "forces sRGB decoding"); + + Fields badAnisoBias = base; + badAnisoBias.anisoBias = 1; + rejectFields(badAnisoBias, "anisotropy bias"); + + Fields badTrunc = base; + badTrunc.truncCoord = true; + rejectFields(badTrunc, "coordinate truncation"); + + Fields badCubeWrap = base; + badCubeWrap.disableCubeWrap = true; + rejectFields(badCubeWrap, "seamless cube filtering"); + + Fields badFilterMode = base; + badFilterMode.filterMode = 1; + rejectFields(badFilterMode, "reduction filter mode"); + + Fields badDegamma = base; + badDegamma.disableDegamma = true; + rejectFields(badDegamma, "disables degamma"); + + Fields badPerf = base; + badPerf.perfMip = 1; + rejectFields(badPerf, "performance counters"); + badPerf = base; + badPerf.perfZ = 1; + rejectFields(badPerf, "performance counters"); + + Fields badLodBiasSec = base; + badLodBiasSec.lodBiasSec = 1; + rejectFields(badLodBiasSec, "secondary LOD bias"); + + Fields badPreclamp = base; + badPreclamp.pointPreclamp = true; + rejectFields(badPreclamp, "point preclamping"); + + Fields badAnisoOverride = base; + badAnisoOverride.anisoOverride = true; + rejectFields(badAnisoOverride, "anisotropy override"); + + Fields badBlendZero = base; + badBlendZero.blendZeroPrt = true; + rejectFields(badBlendZero, "PRT blend-zero"); + + Fields badBorder = base; + badBorder.borderColorType = 3; + rejectFields(badBorder, "border color table"); + + Fields opaqueBlack = base; + opaqueBlack.borderColorType = 1; + Require(DecodeSamplerResource(pack(opaqueBlack)).borderColor == VK_BORDER_COLOR_INT_OPAQUE_BLACK, "border color type 1 must decode to opaque black"); + Fields opaqueWhite = base; + opaqueWhite.borderColorType = 2; + Require(DecodeSamplerResource(pack(opaqueWhite)).borderColor == VK_BORDER_COLOR_INT_OPAQUE_WHITE, "border color type 2 must decode to opaque white"); + + Fields positiveBias = base; + positiveBias.lodBiasRaw = 256; + Require(nearlyEqual(DecodeSamplerResource(pack(positiveBias)).lodBias, 1.0f), "positive LOD bias decoded incorrectly"); + Fields negativeBias = base; + negativeBias.lodBiasRaw = static_cast(-256) & 0x3fffu; + Require(nearlyEqual(DecodeSamplerResource(pack(negativeBias)).lodBias, -1.0f), "negative LOD bias decoded incorrectly"); + + std::array shortWords{}; + reject([&] { DecodeSamplerResource(shortWords); }, "4 dwords"); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp new file mode 100644 index 000000000..a24ca9196 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -0,0 +1,267 @@ +#include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using Shape = ShaderRecompiler::DescriptorImageShape; + +struct Fields { + std::uint64_t base40 = 0x123456ull; + std::uint32_t minLod = 0; + std::uint32_t format = 56; + std::uint32_t width = 16; + std::uint32_t height = 16; + std::uint32_t dstSelX = 4; + std::uint32_t dstSelY = 5; + std::uint32_t dstSelZ = 6; + std::uint32_t dstSelW = 7; + std::uint32_t baseLevel = 0; + std::uint32_t lastLevel = 0; + std::uint32_t tileModeRaw = 0x00; + std::uint32_t bcSwizzle = 0; + std::uint32_t typeRaw = 9; + std::uint32_t depth = 0; + std::uint32_t baseArray = 0; + std::uint32_t arrayPitch = 0; + std::uint32_t maxMip = 0; + std::uint32_t minLodWarn = 0; + std::uint32_t perfMod = 0; + bool cornerSample = false; + bool mipStatsCntEn = false; + bool prtDefColor = false; + std::uint32_t mipStatsCntId = 0; + bool msaaDepth = false; + std::uint32_t maxUncompBlkSize = 0; + std::uint32_t maxCompBlkSize = 0; + bool metaPipeAligned = false; + bool writeCompress = false; + bool metaCompress = false; + bool dccAlphaPos = false; + bool dccColorTransf = false; + std::uint64_t metaAddr = 0; +}; + +std::array pack(const Fields& f) { + std::array words{}; + const auto widthMinus1 = f.width - 1u; + const auto heightMinus1 = f.height - 1u; + words[0] = static_cast(f.base40 & 0xffffffffull); + words[1] = static_cast((f.base40 >> 32u) & 0xffull) + | ((f.minLod & 0xfffu) << 8u) + | ((f.format & 0x1ffu) << 20u) + | ((widthMinus1 & 0x3u) << 30u); + words[2] = ((widthMinus1 >> 2u) & 0xfffu) | ((heightMinus1 & 0x3fffu) << 14u); + words[3] = (f.dstSelX & 0x7u) | ((f.dstSelY & 0x7u) << 3u) | ((f.dstSelZ & 0x7u) << 6u) | ((f.dstSelW & 0x7u) << 9u) + | ((f.baseLevel & 0xfu) << 12u) | ((f.lastLevel & 0xfu) << 16u) | ((f.tileModeRaw & 0x1fu) << 20u) + | ((f.bcSwizzle & 0x7u) << 25u) | ((f.typeRaw & 0xfu) << 28u); + words[4] = (f.depth & 0x1fffu) | ((f.baseArray & 0x1fffu) << 16u); + words[5] = (f.arrayPitch & 0xfu) | ((f.maxMip & 0xfu) << 4u) | ((f.minLodWarn & 0xfffu) << 8u) + | ((f.perfMod & 0x7u) << 20u) | ((f.cornerSample ? 1u : 0u) << 23u) | ((f.mipStatsCntEn ? 1u : 0u) << 25u) + | ((f.prtDefColor ? 1u : 0u) << 26u); + words[6] = (f.mipStatsCntId & 0xffu) | ((f.msaaDepth ? 1u : 0u) << 10u) | ((f.maxUncompBlkSize & 0x3u) << 15u) + | ((f.maxCompBlkSize & 0x3u) << 17u) | ((f.metaPipeAligned ? 1u : 0u) << 19u) | ((f.writeCompress ? 1u : 0u) << 20u) + | ((f.metaCompress ? 1u : 0u) << 21u) | ((f.dccAlphaPos ? 1u : 0u) << 22u) | ((f.dccColorTransf ? 1u : 0u) << 23u) + | static_cast((f.metaAddr & 0xffull) << 24u); + words[7] = static_cast(f.metaAddr >> 8u); + return words; +} + +template +void reject(TAction action, std::string_view reason) { + try { + action(); + } catch (const std::runtime_error& error) { + Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string("unexpected guest texture test error: ") + error.what()); + return; + } + throw std::runtime_error(std::string("expected guest texture rejection: ") + std::string(reason)); +} + +void rejectFields(const Fields& f, std::string_view reason) { + const auto words = pack(f); + reject([&] { DecodeTextureResource(words); }, reason); +} + +} + +void RunGuestTextureResourceTests() { + Fields base; + auto result = DecodeTextureResource(pack(base)); + Require(result.baseAddress == (base.base40 << 8u), "decoded base address changed"); + Require(result.width == 16 && result.height == 16, "decoded width or height changed"); + Require(result.depthOrLastArray == 0 && result.baseArray == 0, "decoded depth or base array changed"); + Require(result.mipCount == 1 && result.baseLevel == 0, "decoded mip count or base level changed"); + Require(result.tileMode == TextureTileMode::kLinear, "decoded tile mode changed"); + Require(result.dimension == TextureDimension::k2D, "decoded dimension changed"); + Require(result.format == 56, "decoded format changed"); + Require(result.dstSelX == 4 && result.dstSelY == 5 && result.dstSelZ == 6 && result.dstSelW == 7, "decoded destination selectors changed"); + + Fields wide = base; + wide.width = 8192; + wide.height = 4096; + result = DecodeTextureResource(pack(wide)); + Require(result.width == 8192 && result.height == 4096, "wide texture dimensions were split across dwords incorrectly"); + + Fields tileModes = base; + tileModes.tileModeRaw = 0x01; + Require(DecodeTextureResource(pack(tileModes)).tileMode == TextureTileMode::kStandard256B, "tile mode 0x01 must decode to standard 256B"); + tileModes.tileModeRaw = 0x05; + Require(DecodeTextureResource(pack(tileModes)).tileMode == TextureTileMode::kStandard4KB, "tile mode 0x05 must decode to standard 4KB"); + tileModes.tileModeRaw = 0x09; + Require(DecodeTextureResource(pack(tileModes)).tileMode == TextureTileMode::kStandard64KB, "tile mode 0x09 must decode to standard 64KB"); + tileModes.tileModeRaw = 0x02; + rejectFields(tileModes, "unsupported tile mode"); + + Fields oneD = base; + oneD.typeRaw = 8; + oneD.height = 1; + result = DecodeTextureResource(pack(oneD)); + Require(result.dimension == TextureDimension::k1D, "1D descriptor did not decode to 1D dimension"); + oneD.height = 2; + rejectFields(oneD, "nonzero height, depth or base array"); + + Fields twoD = base; + twoD.depth = 1; + rejectFields(twoD, "nonzero depth or base array"); + twoD = base; + twoD.baseArray = 1; + rejectFields(twoD, "nonzero depth or base array"); + + Fields array = base; + array.typeRaw = 13; + array.depth = 3; + array.baseArray = 1; + result = DecodeTextureResource(pack(array)); + Require(result.dimension == TextureDimension::k2DArray && result.depthOrLastArray == 3 && result.baseArray == 1, "2D array descriptor decoded incorrectly"); + array.baseArray = 5; + rejectFields(array, "base array past its last array slice"); + + Fields cube = base; + cube.typeRaw = 11; + cube.width = 32; + cube.height = 32; + cube.depth = 5; + cube.baseArray = 0; + result = DecodeTextureResource(pack(cube)); + Require(result.dimension == TextureDimension::kCube, "cube descriptor did not decode to cube dimension"); + Fields cubeNotSquare = cube; + cubeNotSquare.height = 16; + rejectFields(cubeNotSquare, "not square"); + Fields cubeBadArray = cube; + cubeBadArray.baseArray = 6; + rejectFields(cubeBadArray, "base array past its last array slice"); + Fields cubeNotMultiple = cube; + cubeNotMultiple.depth = 4; + rejectFields(cubeNotMultiple, "multiple of 6"); + + Fields badType = base; + badType.typeRaw = 0; + rejectFields(badType, "unsupported image type"); + + Fields badSel = base; + badSel.dstSelX = 2; + rejectFields(badSel, "invalid destination channel selector"); + badSel = base; + badSel.dstSelW = 3; + rejectFields(badSel, "invalid destination channel selector"); + + Fields zeroAddress = base; + zeroAddress.base40 = 0; + rejectFields(zeroAddress, "null base address"); + + Fields badMinLod = base; + badMinLod.minLod = 1; + rejectFields(badMinLod, "nonzero minimum LOD clamp"); + + Fields badMinLodWarn = base; + badMinLodWarn.minLodWarn = 1; + rejectFields(badMinLodWarn, "minimum LOD warning threshold"); + + Fields badPerfMod = base; + badPerfMod.perfMod = 1; + rejectFields(badPerfMod, "performance modulation"); + + Fields badMipStats = base; + badMipStats.mipStatsCntId = 1; + rejectFields(badMipStats, "mip statistics counters"); + badMipStats = base; + badMipStats.mipStatsCntEn = true; + rejectFields(badMipStats, "mip statistics counters"); + + Fields badCorner = base; + badCorner.cornerSample = true; + rejectFields(badCorner, "corner sampling"); + + Fields badPrt = base; + badPrt.prtDefColor = true; + rejectFields(badPrt, "partially resident default color"); + + Fields badPitch = base; + badPitch.arrayPitch = 1; + rejectFields(badPitch, "nonzero array pitch"); + + Fields badMsaa = base; + badMsaa.msaaDepth = true; + rejectFields(badMsaa, "MSAA"); + + Fields badBlockSize = base; + badBlockSize.maxUncompBlkSize = 1; + rejectFields(badBlockSize, "DCC block size overrides"); + badBlockSize = base; + badBlockSize.maxCompBlkSize = 1; + rejectFields(badBlockSize, "DCC block size overrides"); + + Fields badMeta = base; + badMeta.metaPipeAligned = true; + rejectFields(badMeta, "metadata compression"); + badMeta = base; + badMeta.writeCompress = true; + rejectFields(badMeta, "metadata compression"); + badMeta = base; + badMeta.metaCompress = true; + rejectFields(badMeta, "metadata compression"); + badMeta = base; + badMeta.dccAlphaPos = true; + rejectFields(badMeta, "metadata compression"); + badMeta = base; + badMeta.dccColorTransf = true; + rejectFields(badMeta, "metadata compression"); + badMeta = base; + badMeta.metaAddr = 1; + rejectFields(badMeta, "metadata compression"); + + Fields badSwizzle = base; + badSwizzle.bcSwizzle = 1; + rejectFields(badSwizzle, "BC swizzle"); + + Fields badLevels = base; + badLevels.baseLevel = 2; + badLevels.lastLevel = 1; + badLevels.maxMip = 1; + rejectFields(badLevels, "base mip level past its last mip level"); + + Fields badMaxMip = base; + badMaxMip.lastLevel = 1; + badMaxMip.maxMip = 2; + rejectFields(badMaxMip, "must expose every mip level"); + + std::array shortWords{}; + reject([&] { DecodeTextureResource(shortWords); }, "8 dwords"); + + Require(MatchesGuestDimension(Shape::Image1D, TextureDimension::k1D), "1D shape must match 1D dimension"); + Require(!MatchesGuestDimension(Shape::Image1D, TextureDimension::k2D), "1D shape must not match 2D dimension"); + Require(MatchesGuestDimension(Shape::Image2D, TextureDimension::k2D), "2D shape must match 2D dimension"); + Require(!MatchesGuestDimension(Shape::Image2D, TextureDimension::k2DArray), "2D shape must not match 2D array dimension"); + Require(MatchesGuestDimension(Shape::Image2DArray, TextureDimension::k2DArray), "2D array shape must match 2D array dimension"); + Require(!MatchesGuestDimension(Shape::Image2DArray, TextureDimension::kCube), "2D array shape must not match cube dimension"); + Require(MatchesGuestDimension(Shape::ImageCube, TextureDimension::kCube), "cube shape must match cube dimension"); + Require(!MatchesGuestDimension(Shape::ImageCube, TextureDimension::k1D), "cube shape must not match 1D dimension"); + Require(!MatchesGuestDimension(Shape::Image3D, TextureDimension::k1D), "3D shape must never match a guest dimension"); + Require(!MatchesGuestDimension(Shape::Image3D, TextureDimension::k2D), "3D shape must never match a guest dimension"); + Require(!MatchesGuestDimension(Shape::Image3D, TextureDimension::k2DArray), "3D shape must never match a guest dimension"); + Require(!MatchesGuestDimension(Shape::Image3D, TextureDimension::kCube), "3D shape must never match a guest dimension"); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp new file mode 100644 index 000000000..f6105bb2d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp @@ -0,0 +1,102 @@ +#include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +struct Push { + std::uint32_t srcBase; + std::uint32_t dstBase; + std::uint32_t width; + std::uint32_t height; + std::uint32_t pitchBytes; + std::uint32_t blocksPerRow; + std::uint32_t tail; + std::uint32_t tailX; + std::uint32_t tailY; + std::uint32_t elementBytes; +}; + +Push decodePush(const std::vector& bytes) { + Push push{}; + Require(bytes.size() == sizeof(Push), "unexpected texture detiling push constant size"); + std::memcpy(&push, bytes.data(), sizeof(Push)); + return push; +} + +TileMipLayout makeLayout(std::uint32_t width, std::uint32_t height, std::uint32_t pitchBytes, std::uint32_t blocksPerRow, std::uint64_t tiledSize, std::uint64_t linearSize) { + TileMipLayout layout{}; + layout.width = width; + layout.height = height; + layout.pitchBytes = pitchBytes; + layout.blocksPerRow = blocksPerRow; + layout.tiledSize = tiledSize; + layout.linearSize = linearSize; + layout.tail = false; + layout.tailX = 0; + layout.tailY = 0; + return layout; +} + +template +void reject(TAction action, std::string_view reason) { + try { + action(); + } catch (const std::runtime_error& error) { + Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string("unexpected texture detiler test error: ") + error.what()); + return; + } + throw std::runtime_error(std::string("expected texture detiler rejection: ") + std::string(reason)); +} + +} + +void RunTextureDetilerTests(const Context& context, const TextureDetilerTestAccess& access) { + TextureDetiler detiler(context); + const auto commands = reinterpret_cast(static_cast(1)); + const auto source = access.makeBuffer(4096); + const auto destination = access.makeBuffer(4096); + + const auto layout = makeLayout(20, 12, 96, 5, 64, 48); + detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 20, destination, 40, layout); + auto capture = access.lastDispatch(); + Require(capture.groupsX == 3 && capture.groupsY == 2 && capture.groupsZ == 1, "dispatch group counts were computed incorrectly"); + const auto push = decodePush(capture.pushConstants); + Require(push.srcBase == 4 && push.dstBase == 8, "push constant buffer bases must account for storage buffer offset alignment"); + Require(push.width == 20 && push.height == 12, "push constant dimensions changed"); + Require(push.pitchBytes == 96 && push.blocksPerRow == 5, "push constant row layout changed"); + Require(push.tail == 0 && push.tailX == 0 && push.tailY == 0, "push constant tail fields must reflect a non-tail mip"); + Require(push.elementBytes == 4, "push constant element size changed"); + Require(capture.sourceBuffer == source && capture.destinationBuffer == destination, "dispatch bound the wrong source or destination buffer"); + Require(capture.sourceOffset == 16 && capture.sourceRange == 68, "source descriptor offset or range computed incorrectly"); + Require(capture.destinationOffset == 32 && capture.destinationRange == 56, "destination descriptor offset or range computed incorrectly"); + + const auto pipelinesAfterFirst = access.pipelineCount(); + Require(pipelinesAfterFirst == 1, "the first dispatch must create exactly one compute pipeline"); + + detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 0, destination, 0, makeLayout(8, 8, 32, 2, 32, 32)); + Require(access.pipelineCount() == pipelinesAfterFirst, "dispatching with the same tile mode and element size must reuse the cached pipeline"); + + detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 8, source, 0, destination, 0, makeLayout(8, 8, 32, 2, 32, 32)); + Require(access.pipelineCount() == pipelinesAfterFirst + 1, "a different element size must create a new compute pipeline"); + + detiler.Dispatch(commands, TextureTileMode::kLinear, 4, source, 0, destination, 0, makeLayout(8, 8, 32, 2, 32, 32)); + Require(access.pipelineCount() == pipelinesAfterFirst + 2, "a different tile mode must create a new compute pipeline"); + + detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 0, destination, 0, makeLayout(8, 8, 32, 2, 32, 32)); + Require(access.pipelineCount() == pipelinesAfterFirst + 2, "reusing an earlier tile mode and element size must not create another pipeline"); + + reject([&] { detiler.Dispatch(VK_NULL_HANDLE, TextureTileMode::kStandard4KB, 4, source, 0, destination, 0, layout); }, "active command buffer"); + reject([&] { detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, VK_NULL_HANDLE, 0, destination, 0, layout); }, "source and destination buffers"); + reject([&] { detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 0, VK_NULL_HANDLE, 0, layout); }, "source and destination buffers"); + reject([&] { detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 0, destination, 0, makeLayout(0, 12, 96, 5, 64, 48)); }, "non-empty mip layout"); + reject([&] { detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 0, destination, 0, makeLayout(20, 0, 96, 5, 64, 48)); }, "non-empty mip layout"); + reject([&] { detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 0, destination, 0, makeLayout(20, 12, 96, 5, 0, 48)); }, "non-empty mip layout"); + reject([&] { detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 0, destination, 0, makeLayout(20, 12, 96, 5, 64, 0)); }, "non-empty mip layout"); + reject([&] { detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 3, source, 0, destination, 0, layout); }, "unsupported element size"); + reject([&] { detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 20, destination, 40, makeLayout(20, 12, 96, 5, 1024, 48)); }, "buffer range exceeds device limits"); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp new file mode 100644 index 000000000..6fa05cd45 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -0,0 +1,54 @@ +#include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +template +void reject(TAction action, std::string_view reason) { + try { + action(); + } catch (const std::runtime_error& error) { + Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string("unexpected format test error: ") + error.what()); + return; + } + throw std::runtime_error(std::string("expected texture format rejection: ") + std::string(reason)); +} + +} + +void RunTextureFormatTests() { + Require(ResolveTextureFormat(1) == VK_FORMAT_R8_UNORM, "format 1 must resolve to R8_UNORM"); + Require(BytesPerElement(1) == 1u, "format 1 must be one byte wide"); + Require(!IsBlockCompressed(1), "format 1 must not be block compressed"); + Require(BlockWidth(1) == 1u && BlockHeight(1) == 1u, "format 1 must have a one-texel block"); + + Require(ResolveTextureFormat(56) == VK_FORMAT_R8G8B8A8_UNORM, "format 56 must resolve to R8G8B8A8_UNORM"); + Require(BytesPerElement(56) == 4u, "format 56 must be four bytes wide"); + + Require(ResolveTextureFormat(22) == VK_FORMAT_R32_SFLOAT, "format 22 must resolve to R32_SFLOAT"); + Require(BytesPerElement(22) == 4u, "format 22 must be four bytes wide"); + + Require(ResolveTextureFormat(77) == VK_FORMAT_R32G32B32A32_SFLOAT, "format 77 must resolve to R32G32B32A32_SFLOAT"); + Require(BytesPerElement(77) == 16u, "format 77 must be sixteen bytes wide"); + + Require(ResolveTextureFormat(169) == VK_FORMAT_BC1_RGBA_UNORM_BLOCK, "format 169 must resolve to BC1_RGBA_UNORM_BLOCK"); + Require(IsBlockCompressed(169), "format 169 must be block compressed"); + Require(BytesPerElement(169) == 8u, "BC1 blocks must be eight bytes"); + Require(BlockWidth(169) == 4u && BlockHeight(169) == 4u, "BC1 blocks must be four by four texels"); + + Require(ResolveTextureFormat(181) == VK_FORMAT_BC7_UNORM_BLOCK, "format 181 must resolve to BC7_UNORM_BLOCK"); + Require(BytesPerElement(181) == 16u, "BC7 blocks must be sixteen bytes"); + + Require(ResolveTextureFormat(34) == ResolveTextureFormat(20), "format 34 must remap to format 20"); + Require(BytesPerElement(34) == BytesPerElement(20), "remapped format 34 must share the width of format 20"); + + reject([] { ResolveTextureFormat(0); }, "unsupported guest texture format"); + reject([] { ResolveTextureFormat(183); }, "unsupported guest texture format"); + reject([] { ResolveTextureFormat(9999); }, "unsupported guest texture format"); + reject([] { BytesPerElement(2); }, "unsupported guest texture format"); + reject([] { IsBlockCompressed(200); }, "unsupported guest texture format"); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp new file mode 100644 index 000000000..bca14cc98 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -0,0 +1,91 @@ +#include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +template +void reject(TAction action, std::string_view reason) { + try { + action(); + } catch (const std::runtime_error& error) { + Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string("unexpected texture tiling test error: ") + error.what()); + return; + } + throw std::runtime_error(std::string("expected texture tiling rejection: ") + std::string(reason)); +} + +} + +void RunTextureTilingTests() { + { + const auto mips = ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 2); + Require(mips.size() == 2, "linear mip chain must contain the requested mip count"); + Require(mips[0].tiledOffset == 512 && mips[0].tiledSize == 1024, "linear mip 0 offset or size changed"); + Require(mips[0].width == 4 && mips[0].height == 4, "linear mip 0 dimensions changed"); + Require(mips[0].blocksPerRow == 256 && mips[0].pitchBytes == 256, "linear mip 0 row layout changed"); + Require(!mips[0].tail, "linear mips must never fall into a mip tail"); + Require(mips[1].tiledOffset == 0 && mips[1].tiledSize == 512, "linear mip 1 offset or size changed"); + Require(mips[1].width == 2 && mips[1].height == 2, "linear mip 1 dimensions changed"); + Require(mips[1].linearOffset == mips[1].tiledOffset && mips[1].linearSize == mips[1].tiledSize, "linear tiling must keep linear and tiled layout identical"); + } + { + const auto mips = ComputeMipLayout(TextureTileMode::kLinear, 169, 8, 8, 1); + Require(mips.size() == 1, "compressed linear layout must contain one mip"); + Require(mips[0].width == 2 && mips[0].height == 2, "compressed linear mip block dimensions changed"); + Require(mips[0].blocksPerRow == 32 && mips[0].pitchBytes == 256, "compressed linear mip row layout changed"); + Require(mips[0].tiledSize == 512 && mips[0].linearSize == 512, "compressed linear mip size changed"); + } + { + const auto mips = ComputeMipLayout(TextureTileMode::kStandard256B, 1, 64, 64, 1); + Require(mips.size() == 1, "standard 256B layout must contain one mip"); + Require(mips[0].tiledOffset == 0 && mips[0].tiledSize == 4096, "standard 256B mip 0 offset or size changed"); + Require(mips[0].width == 64 && mips[0].height == 64, "standard 256B mip 0 dimensions changed"); + Require(mips[0].blocksPerRow == 4 && mips[0].pitchBytes == 64, "standard 256B mip 0 row layout changed"); + Require(!mips[0].tail, "standard 256B textures must never use a mip tail"); + + const auto surfaceSize = ComputeSurfaceSize(mips, 3); + Require(surfaceSize == 4096ull * 3ull, "surface size must multiply the slice size by the array layer count"); + } + { + const auto mips = ComputeMipLayout(TextureTileMode::kStandard256B, 1, 32, 32, 6); + Require(mips.size() == 6, "standard 256B mip chain must contain the requested mip count"); + for (const auto& mip : mips) Require(!mip.tail, "standard 256B tile mode must never produce a mip tail"); + } + { + const auto mips = ComputeMipLayout(TextureTileMode::kStandard64KB, 1, 1024, 1024, 11); + Require(mips.size() == 11, "standard 64KB mip chain must contain the requested mip count"); + auto tailSeen = false; + for (const auto& mip : mips) { + Require(mip.width != 0 && mip.height != 0, "every standard 64KB mip must have nonzero dimensions"); + Require(mip.tiledSize != 0 && mip.linearSize != 0, "every standard 64KB mip must have a nonzero size"); + if (mip.tail) { + tailSeen = true; + Require(mip.blocksPerRow == 1, "mip tail levels must report a single block per row"); + Require(mip.tiledOffset == 0 && mip.linearOffset == 0, "mip tail levels must share the tail block offset"); + } + } + Require(tailSeen, "a deep standard 64KB mip chain must fall into the mip tail"); + Require(!mips.front().tail, "the base level of a deep mip chain must not be in the mip tail"); + } + { + const auto mips = ComputeMipLayout(TextureTileMode::kStandard4KB, 1, 512, 512, 10); + Require(mips.size() == 10, "standard 4KB mip chain must contain the requested mip count"); + auto tailSeen = false; + for (const auto& mip : mips) { + if (mip.tail) tailSeen = true; + } + Require(tailSeen, "a deep standard 4KB mip chain must fall into the mip tail"); + } + + reject([] { ComputeMipLayout(TextureTileMode::kLinear, 1, 0, 4, 1); }, "zero-sized texture"); + reject([] { ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 0, 1); }, "zero-sized texture"); + reject([] { ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 0); }, "mip count is out of range"); + reject([] { ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 17); }, "mip count is out of range"); + + reject([] { ComputeSurfaceSize({}, 1); }, "empty mip chain"); + reject([] { ComputeSurfaceSize(ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 1), 0); }, "zero array layers"); +}