test(agc): add texture resource and detiling coverage

This commit is contained in:
boykopovar 2026-09-23 10:57:27 +03:00
parent fa09114296
commit b521e1f811
8 changed files with 878 additions and 1 deletions

View File

@ -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

View File

@ -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 <spirv/unified1/spirv.hpp>
@ -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<VkPipelineLayout, VkDeviceSize> pipelineLayoutPushConstantSize;
std::uint32_t pipelineCreateCount = 0;
std::vector<std::array<std::uint32_t, 3>> pipelineSpecializations;
VkPipeline boundPipeline = VK_NULL_HANDLE;
std::vector<std::byte> 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<std::uintptr_t>(info->buffer) * 0x10000;
}
VKAPI_ATTR VkResult VKAPI_CALL mockCreatePipelineLayout(VkDevice, const VkPipelineLayoutCreateInfo* info, const VkAllocationCallbacks*, VkPipelineLayout* layout) {
*layout = makeHandle<VkPipelineLayout>();
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<VkShaderModule>();
++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<std::uint32_t, 3> 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<VkPipeline>();
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<const std::byte*>(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<std::string_view, PFN_vkVoidFunction> table{
{"vkGetBufferDeviceAddressKHR", reinterpret_cast<PFN_vkVoidFunction>(mockGetBufferDeviceAddress)},
@ -394,7 +453,16 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) {
{"vkDestroyDescriptorPool", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyDescriptorPool)},
{"vkAllocateDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockAllocateDescriptorSets)},
{"vkUpdateDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockUpdateDescriptorSets)},
{"vkCmdBindDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockCmdBindDescriptorSets)}
{"vkCmdBindDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockCmdBindDescriptorSets)},
{"vkCreatePipelineLayout", reinterpret_cast<PFN_vkVoidFunction>(mockCreatePipelineLayout)},
{"vkDestroyPipelineLayout", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyPipelineLayout)},
{"vkCreateShaderModule", reinterpret_cast<PFN_vkVoidFunction>(mockCreateShaderModule)},
{"vkDestroyShaderModule", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyShaderModule)},
{"vkCreateComputePipelines", reinterpret_cast<PFN_vkVoidFunction>(mockCreateComputePipelines)},
{"vkDestroyPipeline", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyPipeline)},
{"vkCmdBindPipeline", reinterpret_cast<PFN_vkVoidFunction>(mockCmdBindPipeline)},
{"vkCmdPushConstants", reinterpret_cast<PFN_vkVoidFunction>(mockCmdPushConstants)},
{"vkCmdDispatch", reinterpret_cast<PFN_vkVoidFunction>(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<std::byte>& { 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) {

View File

@ -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 <cstddef>
#include <cstdint>
#include <functional>
#include <vector>
struct DetilerCapture {
std::uint32_t groupsX;
std::uint32_t groupsY;
std::uint32_t groupsZ;
std::vector<std::byte> pushConstants;
VkBuffer sourceBuffer;
VkDeviceSize sourceOffset;
VkDeviceSize sourceRange;
VkBuffer destinationBuffer;
VkDeviceSize destinationOffset;
VkDeviceSize destinationRange;
};
struct TextureDetilerTestAccess {
std::function<VkBuffer(std::uint64_t)> makeBuffer;
std::function<std::vector<std::byte>&(VkBuffer)> bytes;
std::function<DetilerCapture()> lastDispatch;
std::function<std::uint32_t()> pipelineCount;
};
void RunTextureFormatTests();
void RunTextureTilingTests();
void RunGuestTextureResourceTests();
void RunGuestSamplerResourceTests();
void RunTextureDetilerTests(const AgcDriver::Graphics::Context& context, const TextureDetilerTestAccess& access);
#endif

View File

@ -0,0 +1,210 @@
#include "GraphicsTests.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp"
#include <array>
#include <cmath>
#include <string>
#include <string_view>
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<std::uint32_t, 4> pack(const Fields& f) {
std::array<std::uint32_t, 4> 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<typename TAction>
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<float>(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<std::uint32_t>(-256) & 0x3fffu;
Require(nearlyEqual(DecodeSamplerResource(pack(negativeBias)).lodBias, -1.0f), "negative LOD bias decoded incorrectly");
std::array<std::uint32_t, 3> shortWords{};
reject([&] { DecodeSamplerResource(shortWords); }, "4 dwords");
}

View File

@ -0,0 +1,267 @@
#include "GraphicsTests.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include <array>
#include <string>
#include <string_view>
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<std::uint32_t, 8> pack(const Fields& f) {
std::array<std::uint32_t, 8> words{};
const auto widthMinus1 = f.width - 1u;
const auto heightMinus1 = f.height - 1u;
words[0] = static_cast<std::uint32_t>(f.base40 & 0xffffffffull);
words[1] = static_cast<std::uint32_t>((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<std::uint32_t>((f.metaAddr & 0xffull) << 24u);
words[7] = static_cast<std::uint32_t>(f.metaAddr >> 8u);
return words;
}
template<typename TAction>
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<std::uint32_t, 4> 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");
}

View File

@ -0,0 +1,102 @@
#include "GraphicsTests.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include <cstring>
#include <string>
#include <string_view>
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<std::byte>& 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<typename TAction>
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<VkCommandBuffer>(static_cast<std::uintptr_t>(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");
}

View File

@ -0,0 +1,54 @@
#include "GraphicsTests.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
#include <string>
#include <string_view>
namespace {
using namespace AgcDriver::Graphics;
template<typename TAction>
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");
}

View File

@ -0,0 +1,91 @@
#include "GraphicsTests.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include <string>
#include <string_view>
namespace {
using namespace AgcDriver::Graphics;
template<typename TAction>
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");
}