968 lines
57 KiB
C++
968 lines
57 KiB
C++
#include "BdaTests.hpp"
|
|
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
|
|
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
|
|
#include <spirv/unified1/spirv.hpp>
|
|
#include <array>
|
|
#include <bit>
|
|
#include <cstring>
|
|
#include <initializer_list>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <string_view>
|
|
#include <type_traits>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
using AgcDriver::Graphics::Require;
|
|
|
|
alignas(256) std::array<std::byte, 1024> colorMemory{};
|
|
|
|
AgcDriver::QueueState makeState() {
|
|
AgcDriver::QueueState queue;
|
|
queue.userConfig[0x242] = 4;
|
|
queue.context = {
|
|
{0x2d5, 0x2000},
|
|
{0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800},
|
|
{0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0},
|
|
{0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa},
|
|
{0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff},
|
|
{0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240},
|
|
{0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010},
|
|
{0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028},
|
|
{0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u},
|
|
{0x3b8, 0x9000000}, {0x1e0, 0},
|
|
{0xc, 0}, {0xd, 0x40040},
|
|
{0x81, 0x80000000}, {0x82, 0x40040},
|
|
{0x90, 0x80000000}, {0x91, 0x40040},
|
|
{0x94, 0x80000000}, {0x95, 0x40040}
|
|
};
|
|
const auto address = reinterpret_cast<std::uintptr_t>(colorMemory.data());
|
|
queue.context[0x318] = static_cast<std::uint32_t>(address >> 8u);
|
|
queue.context[0x390] = static_cast<std::uint32_t>(address >> 40u);
|
|
queue.context[0x10f] = std::bit_cast<std::uint32_t>(32.0f);
|
|
queue.context[0x110] = std::bit_cast<std::uint32_t>(32.0f);
|
|
queue.context[0x111] = std::bit_cast<std::uint32_t>(-2.0f);
|
|
queue.context[0x112] = std::bit_cast<std::uint32_t>(2.0f);
|
|
queue.context[0x113] = std::bit_cast<std::uint32_t>(1.0f);
|
|
queue.context[0x114] = 0;
|
|
queue.context[0xb4] = 0;
|
|
queue.context[0xb5] = std::bit_cast<std::uint32_t>(1.0f);
|
|
return queue;
|
|
}
|
|
|
|
template<typename TAction>
|
|
void expectFailure(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 failure: ") + error.what());
|
|
return;
|
|
}
|
|
throw std::runtime_error("expected graphics rejection: " + std::string(reason));
|
|
}
|
|
|
|
void stateTests() {
|
|
AgcDriver::QueueState initial;
|
|
Require(initial.context.at(0x200) == 0 && initial.context.at(0x83) == 0xffff, "initial context state is missing");
|
|
initial.context[0x200] = 7;
|
|
initial.context[0xdead] = 1;
|
|
initial.ClearContext();
|
|
Require(initial.context.at(0x200) == 0 && !initial.context.contains(0xdead), "context reset did not restore defaults");
|
|
Require(initial.userConfig.at(0x24b) == 0, "primitive restart must be disabled in initial queue state");
|
|
initial.userConfig[0x24b] = 1;
|
|
initial.ClearContext();
|
|
Require(initial.userConfig.at(0x24b) == 1, "context clear must preserve user configuration");
|
|
initial = AgcDriver::QueueState{};
|
|
Require(initial.userConfig.at(0x24b) == 0, "queue reset must disable primitive restart");
|
|
auto queue = makeState();
|
|
auto state = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(state.color.address == reinterpret_cast<std::uintptr_t>(colorMemory.data()) && state.color.bytes == colorMemory.size(), "render-target address or size changed");
|
|
Require(state.viewport.y == 4 && state.viewport.height == -4, "negative viewport height was lost");
|
|
Require(state.color.format == VK_FORMAT_R8G8B8A8_UNORM, "RGBA format changed");
|
|
queue.userConfig[0x24b] = 1;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "GE_MULTI_PRIM_IB_RESET_EN");
|
|
queue = makeState();
|
|
queue.userConfig.erase(0x24b);
|
|
queue.context[0x2a5] = 0;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "user-config bank at DWORD 0x24b");
|
|
queue = makeState();
|
|
queue.context[0x90] = 0x80010003;
|
|
queue.context[0x91] = 0x30020;
|
|
state = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(state.scissor.offset.x == 3 && state.scissor.offset.y == 1 && state.scissor.extent.width == 29 && state.scissor.extent.height == 2, "scissor intersection changed");
|
|
queue.context[0x31c] |= 0x10000000;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DCC");
|
|
queue = makeState();
|
|
queue.context.erase(0x3b8);
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register");
|
|
queue = makeState();
|
|
queue.context[0x3b8] |= 27u << 14u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "only linear");
|
|
queue = makeState();
|
|
queue.context[0x3b0] = (62u << 14u) | 3u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "pitch");
|
|
queue = makeState();
|
|
queue.context[0x8e] = 0xff;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "target zero");
|
|
queue = makeState();
|
|
queue.context[0x200] = 2;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth");
|
|
queue = makeState();
|
|
queue.context[0x10f] = 0x7fc00000;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "non-finite");
|
|
}
|
|
|
|
void ShaderStageTests() {
|
|
auto queue = makeState();
|
|
for (const auto routing : {0x2000u, 0x2010u, 0x02002000u, 0x02002010u}) {
|
|
for (const auto vertexWave32 : {false, true}) {
|
|
for (const auto fragmentWave32 : {false, true}) {
|
|
queue.context[0x2d5] = routing | (vertexWave32 ? 0x00400000u : 0u);
|
|
queue.context[0x1b6] = fragmentWave32 ? 0x8000u : 0u;
|
|
const auto state = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Vertex, "vertex routing changed");
|
|
Require(state.stages.vertexWaveSize == (vertexWave32 ? 32u : 64u), "incorrect vertex wave size");
|
|
Require(state.stages.fragmentWaveSize == (fragmentWave32 ? 32u : 64u), "incorrect fragment wave size");
|
|
}
|
|
}
|
|
}
|
|
queue = makeState();
|
|
queue.context[0x2d5] = 0x2020;
|
|
queue.userConfig[0x25b] = (64u << 9u) | 21u;
|
|
queue.context[0x1ff] = 64;
|
|
queue.context[0x2ce] = 3;
|
|
queue.context[0x29b] = 2;
|
|
queue.shader[0x8a] = 3u << 29u;
|
|
queue.shader[0x8b] = 3u << 16u;
|
|
auto stages = AgcDriver::Graphics::DecodeState(queue).stages;
|
|
Require(stages.path == AgcDriver::Graphics::ShaderPath::Geometry && stages.mesh && stages.mesh->primitivesPerGroup == 21 && stages.mesh->verticesPerGroup == 63, "geometry assembly changed");
|
|
queue.userConfig[0x25b] = 0;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid geometry subgroup");
|
|
queue = makeState();
|
|
queue.context[0x2d5] = 0x200d;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "Patch topology and HS_EN disagree");
|
|
queue.userConfig[0x242] = 9;
|
|
queue.context[0x2d6] = (3u << 8u) | (3u << 14u);
|
|
queue.context[0x2db] = 1u | (2u << 2u) | (2u << 5u);
|
|
stages = AgcDriver::Graphics::DecodeState(queue).stages;
|
|
Require(stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && stages.tessellation && stages.tessellation->inputControlPoints == 3, "tessellation routing changed");
|
|
queue.context[0x2d5] = 0x202d;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "combined tessellation and geometry");
|
|
queue.context[0x2d5] = 0x200d;
|
|
queue.context[0x2d6] = 0;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "control-point counts");
|
|
queue = makeState();
|
|
for (const auto value : {0x2003u, 0x2018u, 0x20c0u, 0x80002000u}) {
|
|
queue.context[0x2d5] = value;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "reserved");
|
|
}
|
|
for (const auto bit : {1u, 8u, 0x40u, 0x100u, 0x200u, 0x400u, 0x1000u, 0x4000u, 0x8000u, 0x80000u, 0x200000u, 0x800000u, 0x1000000u}) {
|
|
queue.context[0x2d5] = 0x2000u | bit;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported vertex");
|
|
}
|
|
queue.context[0x2d5] = 0;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "without PRIMGEN_EN");
|
|
queue.context.erase(0x2d5);
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register");
|
|
queue.context[0x2d5] = 0x2000;
|
|
queue.context.erase(0x1b6);
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register");
|
|
}
|
|
|
|
void DisabledColorTests() {
|
|
auto queue = makeState();
|
|
queue.context[0x8e] = 0;
|
|
for (const auto offset : {0x31cu, 0x31bu, 0x31du, 0x3b0u, 0x3b8u, 0x390u, 0x318u, 0x1e0u}) queue.context.erase(offset);
|
|
const auto state = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(!state.hasColorTarget && state.color.address == 0 && state.color.bytes == 0, "disabled color writes accessed a color surface");
|
|
Require(state.renderExtent.width == 64 && state.renderExtent.height == 4, "attachment-free framebuffer lost screen scissor extent");
|
|
queue.context[0xd] = 0;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "empty framebuffer extent");
|
|
queue = makeState();
|
|
queue.context[0x8e] = 3;
|
|
const auto partial = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(partial.hasColorTarget && partial.blend.colorWriteMask == 3, "partial color write mask changed");
|
|
queue.context.erase(0x31c);
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register");
|
|
}
|
|
|
|
void DepthClipTests() {
|
|
auto queue = makeState();
|
|
const auto direct = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(!direct.negativeOneToOne && direct.viewport.minDepth == 0 && direct.viewport.maxDepth == 1, "zero-to-one depth transform changed");
|
|
queue.context[0x204] = 0;
|
|
queue.context[0x113] = std::bit_cast<std::uint32_t>(0.5f);
|
|
queue.context[0x114] = std::bit_cast<std::uint32_t>(0.5f);
|
|
const auto symmetric = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(symmetric.negativeOneToOne && symmetric.viewport.minDepth == 0 && symmetric.viewport.maxDepth == 1, "negative-one-to-one depth transform is incorrect");
|
|
queue.context[0x113] = std::bit_cast<std::uint32_t>(-0.5f);
|
|
const auto reversed = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(reversed.viewport.minDepth == 1 && reversed.viewport.maxDepth == 0, "reversed depth transform is incorrect");
|
|
queue.context[0x113] = std::bit_cast<std::uint32_t>(1.0f);
|
|
queue.context[0x114] = 0;
|
|
const auto unrestricted = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(unrestricted.viewport.minDepth == -1 && unrestricted.viewport.maxDepth == 1, "unrestricted viewport depth was normalized");
|
|
queue.context[0x113] = std::bit_cast<std::uint32_t>(-1.0f);
|
|
const auto unrestrictedReversed = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(unrestrictedReversed.viewport.minDepth == 1 && unrestrictedReversed.viewport.maxDepth == -1, "reversed unrestricted viewport depth changed");
|
|
queue.context[0x113] = 0x7f7fffff;
|
|
queue.context[0x114] = 0x7f7fffff;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported viewport transform");
|
|
queue.context[0x114] = std::bit_cast<std::uint32_t>(0.5f);
|
|
queue.context[0x113] = std::bit_cast<std::uint32_t>(0.5f);
|
|
queue.context[0xb4] = std::bit_cast<std::uint32_t>(2.0f);
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "inverted viewport depth clamp");
|
|
queue.context[0xb4] = 0;
|
|
for (std::uint32_t bit = 0; bit < 32; ++bit) {
|
|
if (bit == 19) continue;
|
|
queue.context[0x204] = 1u << bit;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_CL_CLIP_CNTL");
|
|
}
|
|
}
|
|
|
|
void InitialContextTests() {
|
|
const auto configured = makeState();
|
|
AgcDriver::QueueState queue;
|
|
Require(queue.context.at(0x3) == 0 && queue.context.at(0x8) == 0 && queue.context.at(0x9) == 0x3f800000, "depth bounds overlap render override");
|
|
Require(queue.context.at(0x2dc) == 0xaa00 && queue.context.at(0x313) == 0x6000 && queue.context.at(0x2f9) == 0x2d, "initial raster controls are incomplete");
|
|
Require(queue.context.at(0x30e) == 0xffffffff && queue.context.at(0x30f) == 0xffffffff, "initial sample mask excludes samples");
|
|
queue.userConfig[0x242] = 4;
|
|
for (const auto offset : {0x2d5u, 0x204u, 0x8eu, 0x8fu, 0x1c3u, 0x1c5u, 0x31cu, 0x3b0u, 0x3b8u, 0x318u, 0x390u, 0x10fu, 0x110u, 0x111u, 0x112u, 0x113u, 0x114u, 0xb4u, 0xb5u}) queue.context.at(offset) = configured.context.at(offset);
|
|
const auto state = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(state.color.address == reinterpret_cast<std::uintptr_t>(colorMemory.data()) && state.color.bytes == colorMemory.size(), "sparse guest setup lost its render target");
|
|
Require(queue.context.at(0x206) == 0x43f, "initial homogeneous viewport mode changed");
|
|
for (const auto control : {0x3fu, 0x43eu, 0x53fu, 0x63fu, 0x8000043fu}) {
|
|
queue.context[0x206] = control;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_CL_VTE_CNTL=0x");
|
|
}
|
|
queue.context[0x206] = 0x43f;
|
|
queue.context[0x2dc] |= 1;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "alpha-to-coverage");
|
|
queue.context.erase(0x2dc);
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register");
|
|
queue.ClearContext();
|
|
Require(queue.context.at(0x2dc) == 0xaa00 && queue.context.at(0x318) == 0 && queue.context.at(0x8e) == 0, "clear did not restore controls and discard target state");
|
|
Require(!queue.context.contains(0xdead), "unknown context register acquired a default");
|
|
}
|
|
|
|
struct MockDescriptorWrite {
|
|
std::uint32_t binding;
|
|
std::uint32_t count;
|
|
VkDescriptorType type;
|
|
std::vector<VkDescriptorBufferInfo> buffers;
|
|
};
|
|
|
|
struct MockVulkan {
|
|
std::uint64_t next = 1;
|
|
std::int64_t live = 0;
|
|
std::map<VkBuffer, VkDeviceSize> bufferSizes;
|
|
std::map<VkBuffer, VkBufferUsageFlags> bufferUsage;
|
|
std::map<VkDeviceMemory, VkMemoryAllocateFlags> allocationFlags;
|
|
std::map<VkBuffer, VkDeviceMemory> bufferMemory;
|
|
std::map<VkDeviceMemory, std::vector<std::byte>> memories;
|
|
std::vector<VkDescriptorSetLayoutBinding> layoutBindings;
|
|
std::vector<VkDescriptorPoolSize> poolSizes;
|
|
std::uint32_t poolMaxSets = 0;
|
|
std::vector<MockDescriptorWrite> writes;
|
|
std::uint32_t boundSets = 0;
|
|
std::uint32_t boundFirst = 0;
|
|
VkPipelineBindPoint boundPoint = VK_PIPELINE_BIND_POINT_MAX_ENUM;
|
|
};
|
|
|
|
MockVulkan mock;
|
|
|
|
template<typename THandle>
|
|
THandle makeHandle() {
|
|
const auto value = mock.next++;
|
|
if constexpr (std::is_pointer_v<THandle>) return reinterpret_cast<THandle>(static_cast<std::uintptr_t>(value));
|
|
else return static_cast<THandle>(value);
|
|
}
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateInfo* info, const VkAllocationCallbacks*, VkBuffer* buffer) {
|
|
*buffer = makeHandle<VkBuffer>();
|
|
mock.bufferSizes[*buffer] = info->size;
|
|
mock.bufferUsage[*buffer] = info->usage;
|
|
++mock.live;
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer buffer, VkMemoryRequirements* requirements) {
|
|
*requirements = {mock.bufferSizes.at(buffer), 1, 1};
|
|
}
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) {
|
|
*memory = makeHandle<VkDeviceMemory>();
|
|
mock.memories[*memory] = std::vector<std::byte>(info->allocationSize);
|
|
if (info->pNext != nullptr) {
|
|
const auto* flags = static_cast<const VkMemoryAllocateFlagsInfo*>(info->pNext);
|
|
mock.allocationFlags[*memory] = flags->flags;
|
|
Require(flags->sType == VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO && flags->flags == VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT, "invalid BDA allocation flags");
|
|
}
|
|
++mock.live;
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL mockBindBufferMemory(VkDevice, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) {
|
|
Require(offset == 0, "mock buffer memory must be bound at offset zero");
|
|
mock.bufferMemory[buffer] = memory;
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL mockMapMemory(VkDevice, VkDeviceMemory memory, VkDeviceSize offset, VkDeviceSize, VkMemoryMapFlags, void** data) {
|
|
Require(offset == 0, "mock memory must be mapped from offset zero");
|
|
*data = mock.memories.at(memory).data();
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockUnmapMemory(VkDevice, VkDeviceMemory) {}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer, const VkAllocationCallbacks*) {
|
|
--mock.live;
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockFreeMemory(VkDevice, VkDeviceMemory, const VkAllocationCallbacks*) {
|
|
--mock.live;
|
|
}
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL mockCreateDescriptorSetLayout(VkDevice, const VkDescriptorSetLayoutCreateInfo* info, const VkAllocationCallbacks*, VkDescriptorSetLayout* layout) {
|
|
*layout = makeHandle<VkDescriptorSetLayout>();
|
|
mock.layoutBindings.assign(info->pBindings, info->pBindings + info->bindingCount);
|
|
++mock.live;
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockDestroyDescriptorSetLayout(VkDevice, VkDescriptorSetLayout, const VkAllocationCallbacks*) {
|
|
--mock.live;
|
|
}
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL mockCreateDescriptorPool(VkDevice, const VkDescriptorPoolCreateInfo* info, const VkAllocationCallbacks*, VkDescriptorPool* pool) {
|
|
*pool = makeHandle<VkDescriptorPool>();
|
|
mock.poolSizes.assign(info->pPoolSizes, info->pPoolSizes + info->poolSizeCount);
|
|
mock.poolMaxSets = info->maxSets;
|
|
++mock.live;
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockDestroyDescriptorPool(VkDevice, VkDescriptorPool, const VkAllocationCallbacks*) {
|
|
--mock.live;
|
|
}
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL mockAllocateDescriptorSets(VkDevice, const VkDescriptorSetAllocateInfo* info, VkDescriptorSet* sets) {
|
|
Require(info->descriptorSetCount == 1, "exactly one descriptor set must be allocated");
|
|
sets[0] = makeHandle<VkDescriptorSet>();
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockUpdateDescriptorSets(VkDevice, std::uint32_t count, const VkWriteDescriptorSet* writes, std::uint32_t copyCount, const VkCopyDescriptorSet*) {
|
|
Require(copyCount == 0, "descriptor copies are not expected");
|
|
for (std::uint32_t i = 0; i < count; ++i) {
|
|
MockDescriptorWrite write{writes[i].dstBinding, writes[i].descriptorCount, writes[i].descriptorType, {}};
|
|
write.buffers.assign(writes[i].pBufferInfo, writes[i].pBufferInfo + writes[i].descriptorCount);
|
|
mock.writes.push_back(write);
|
|
}
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockCmdBindDescriptorSets(VkCommandBuffer, VkPipelineBindPoint point, VkPipelineLayout, std::uint32_t first, std::uint32_t count, const VkDescriptorSet*, std::uint32_t, const std::uint32_t*) {
|
|
mock.boundPoint = point;
|
|
mock.boundFirst = first;
|
|
mock.boundSets = count;
|
|
}
|
|
|
|
VKAPI_ATTR VkDeviceAddress VKAPI_CALL mockGetBufferDeviceAddress(VkDevice, const VkBufferDeviceAddressInfo* info) {
|
|
Require(mock.bufferMemory.contains(info->buffer), "BDA buffer was not bound");
|
|
Require((mock.bufferUsage.at(info->buffer) & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT) != 0, "BDA buffer usage is missing");
|
|
Require((mock.allocationFlags.at(mock.bufferMemory.at(info->buffer)) & VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT) != 0, "BDA allocation flags are missing");
|
|
return 0x100000000000ULL + reinterpret_cast<std::uintptr_t>(info->buffer) * 0x10000;
|
|
}
|
|
|
|
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)},
|
|
{"vkCreateBuffer", reinterpret_cast<PFN_vkVoidFunction>(mockCreateBuffer)},
|
|
{"vkGetBufferMemoryRequirements", reinterpret_cast<PFN_vkVoidFunction>(mockGetBufferMemoryRequirements)},
|
|
{"vkAllocateMemory", reinterpret_cast<PFN_vkVoidFunction>(mockAllocateMemory)},
|
|
{"vkBindBufferMemory", reinterpret_cast<PFN_vkVoidFunction>(mockBindBufferMemory)},
|
|
{"vkMapMemory", reinterpret_cast<PFN_vkVoidFunction>(mockMapMemory)},
|
|
{"vkUnmapMemory", reinterpret_cast<PFN_vkVoidFunction>(mockUnmapMemory)},
|
|
{"vkDestroyBuffer", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyBuffer)},
|
|
{"vkFreeMemory", reinterpret_cast<PFN_vkVoidFunction>(mockFreeMemory)},
|
|
{"vkCreateDescriptorSetLayout", reinterpret_cast<PFN_vkVoidFunction>(mockCreateDescriptorSetLayout)},
|
|
{"vkDestroyDescriptorSetLayout", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyDescriptorSetLayout)},
|
|
{"vkCreateDescriptorPool", reinterpret_cast<PFN_vkVoidFunction>(mockCreateDescriptorPool)},
|
|
{"vkDestroyDescriptorPool", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyDescriptorPool)},
|
|
{"vkAllocateDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockAllocateDescriptorSets)},
|
|
{"vkUpdateDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockUpdateDescriptorSets)},
|
|
{"vkCmdBindDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockCmdBindDescriptorSets)}
|
|
};
|
|
const auto it = table.find(name);
|
|
return it == table.end() ? nullptr : it->second;
|
|
}
|
|
|
|
AgcDriver::Graphics::Context mockContext() {
|
|
AgcDriver::Graphics::Context context{};
|
|
context.deviceProc = mockProc;
|
|
context.memory.memoryTypeCount = 1;
|
|
context.memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
|
context.limits.minStorageBufferOffsetAlignment = 1;
|
|
context.limits.maxBoundDescriptorSets = 1;
|
|
context.limits.maxStorageBufferRange = 4096;
|
|
context.limits.maxPerStageDescriptorStorageBuffers = 16;
|
|
context.limits.maxPerStageResources = 128;
|
|
context.limits.maxDescriptorSetStorageBuffers = 32;
|
|
return context;
|
|
}
|
|
|
|
using Role = ShaderRecompiler::DescriptorRole;
|
|
using Kind = ShaderRecompiler::DescriptorKind;
|
|
|
|
alignas(16) std::array<std::uint32_t, 4> guestFirst{0x11111111, 0x22222222, 0x33333333, 0x44444444};
|
|
alignas(16) std::array<std::uint32_t, 8> guestSecond{1, 2, 3, 4, 5, 6, 7, 8};
|
|
alignas(16) std::array<std::uint32_t, 2> guestThird{0xaaaaaaaa, 0xbbbbbbbb};
|
|
|
|
std::vector<std::uint32_t> vsharp(const void* pointer, std::uint32_t bytes) {
|
|
const auto address = reinterpret_cast<std::uintptr_t>(pointer);
|
|
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u) & 0xffffu, bytes, 0x31000000u};
|
|
}
|
|
|
|
std::vector<std::uint32_t> join(std::vector<std::uint32_t> first, const std::vector<std::uint32_t>& second) {
|
|
first.insert(first.end(), second.begin(), second.end());
|
|
return first;
|
|
}
|
|
|
|
ShaderRecompiler::DescriptorBinding makeBinding(Role role, std::uint32_t binding, std::uint32_t count, std::vector<std::uint32_t> words) {
|
|
ShaderRecompiler::DescriptorBinding result;
|
|
result.kind = Kind::StorageBuffer;
|
|
result.role = role;
|
|
result.descriptorSet = 0;
|
|
result.binding = binding;
|
|
result.count = count;
|
|
result.guestDescriptor = std::move(words);
|
|
return result;
|
|
}
|
|
|
|
bool sameBytes(const std::vector<std::byte>& memory, const void* expected, std::size_t bytes) {
|
|
return memory.size() >= bytes && std::memcmp(memory.data(), expected, bytes) == 0;
|
|
}
|
|
|
|
const MockDescriptorWrite& findWrite(std::uint32_t binding) {
|
|
for (const auto& write : mock.writes) {
|
|
if (write.binding == binding) return write;
|
|
}
|
|
throw std::runtime_error("expected descriptor write is missing for binding " + std::to_string(binding));
|
|
}
|
|
|
|
const VkDescriptorSetLayoutBinding& findLayoutBinding(std::uint32_t binding) {
|
|
for (const auto& item : mock.layoutBindings) {
|
|
if (item.binding == binding) return item;
|
|
}
|
|
throw std::runtime_error("expected descriptor set layout binding is missing for binding " + std::to_string(binding));
|
|
}
|
|
|
|
const std::vector<std::byte>& bufferBytes(VkBuffer buffer) {
|
|
return mock.memories.at(mock.bufferMemory.at(buffer));
|
|
}
|
|
|
|
void expectResourceFailure(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment, std::string_view reason) {
|
|
mock = MockVulkan{};
|
|
const auto context = mockContext();
|
|
const auto color = AgcDriver::Graphics::DecodeState(makeState()).color;
|
|
expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); }, reason);
|
|
Require(mock.live == 0, "failed shader resources leaked Vulkan objects");
|
|
}
|
|
|
|
void expectSingleFailure(const ShaderRecompiler::DescriptorBinding& binding, std::string_view reason) {
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
ShaderRecompiler::RecompileResult fragment;
|
|
vertex.bindings.push_back(binding);
|
|
expectResourceFailure(vertex, fragment, reason);
|
|
}
|
|
|
|
void pushConstantTests() {
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
ShaderRecompiler::RecompileResult fragment;
|
|
vertex.pushConstants.assign(8, std::byte{1});
|
|
fragment.pushConstants.assign(12, std::byte{2});
|
|
std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 8}}};
|
|
Require(AgcDriver::Graphics::PushConstantStages(shaders) == (VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT), "push constant stage union changed");
|
|
const auto bytes = AgcDriver::Graphics::AssemblePushConstants(shaders);
|
|
Require(bytes.size() == 128 && bytes[0] == std::byte{1} && bytes[7] == std::byte{1} && bytes[8] == std::byte{2} && bytes[19] == std::byte{2} && bytes[20] == std::byte{0} && bytes[127] == std::byte{0}, "assembled push constants are misplaced");
|
|
shaders[1].pushConstantOffset = 4;
|
|
expectFailure([&] { AgcDriver::Graphics::AssemblePushConstants(shaders); }, "overlap");
|
|
shaders[1].pushConstantOffset = 120;
|
|
expectFailure([&] { AgcDriver::Graphics::AssemblePushConstants(shaders); }, "outside the pipeline push constant block");
|
|
shaders[1].pushConstantOffset = 2;
|
|
expectFailure([&] { AgcDriver::Graphics::AssemblePushConstants(shaders); }, "DWORD aligned");
|
|
shaders[1].pushConstantOffset = 8;
|
|
fragment.pushConstants.assign(6, std::byte{2});
|
|
expectFailure([&] { AgcDriver::Graphics::AssemblePushConstants(shaders); }, "DWORD aligned");
|
|
fragment.pushConstants.clear();
|
|
shaders[1].pushConstantOffset = 999;
|
|
Require(AgcDriver::Graphics::PushConstantStages(shaders) == VK_SHADER_STAGE_VERTEX_BIT, "empty push constants contributed a stage");
|
|
Require(AgcDriver::Graphics::AssemblePushConstants(shaders)[8] == std::byte{0}, "empty push constants were copied");
|
|
shaders[1].program = nullptr;
|
|
expectFailure([&] { AgcDriver::Graphics::AssemblePushConstants(shaders); }, "missing compiled shader");
|
|
}
|
|
|
|
void resourceTests() {
|
|
mock = MockVulkan{};
|
|
const auto context = mockContext();
|
|
const auto state = AgcDriver::Graphics::DecodeState(makeState());
|
|
const auto commands = reinterpret_cast<VkCommandBuffer>(std::uintptr_t{1});
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
ShaderRecompiler::RecompileResult fragment;
|
|
vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32))));
|
|
vertex.bindings.push_back(makeBinding(Role::ShaderData, 5, 1, {7, 8, 9}));
|
|
fragment.bindings.push_back(makeBinding(Role::FlattenedSrt, 43, 1, {1, 2}));
|
|
fragment.bindings.push_back(makeBinding(Role::GuestBuffers, 44, 1, vsharp(guestThird.data(), 8)));
|
|
AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, state.color, 0, 0);
|
|
Require(resources.Layout() != VK_NULL_HANDLE, "descriptor set layout was not created");
|
|
Require(mock.layoutBindings.size() == 4 && mock.writes.size() == 4, "one layout binding and one write per shader binding are expected");
|
|
Require(findLayoutBinding(0).descriptorCount == 2 && findLayoutBinding(0).stageFlags == VK_SHADER_STAGE_VERTEX_BIT && findLayoutBinding(0).descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, "guest buffer array layout binding is incorrect");
|
|
Require(findLayoutBinding(5).descriptorCount == 1 && findLayoutBinding(5).stageFlags == VK_SHADER_STAGE_VERTEX_BIT, "shader data layout binding is incorrect");
|
|
Require(findLayoutBinding(43).descriptorCount == 1 && findLayoutBinding(43).stageFlags == VK_SHADER_STAGE_FRAGMENT_BIT, "flattened SRT layout binding is incorrect");
|
|
Require(findLayoutBinding(44).descriptorCount == 1 && findLayoutBinding(44).stageFlags == VK_SHADER_STAGE_FRAGMENT_BIT, "fragment guest buffer layout binding is incorrect");
|
|
Require(mock.poolMaxSets == 1 && mock.poolSizes.size() == 1 && mock.poolSizes[0].type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER && mock.poolSizes[0].descriptorCount == 5, "descriptor pool must hold one set with every storage descriptor");
|
|
const auto& array = findWrite(0);
|
|
Require(array.count == 2 && array.buffers.size() == 2 && array.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, "guest buffer array write is incorrect");
|
|
Require(array.buffers[0].offset == 0 && array.buffers[0].range == 16 && array.buffers[1].offset == 0 && array.buffers[1].range == 32, "guest buffers must be bound at zero offset with their descriptor size");
|
|
Require(sameBytes(bufferBytes(array.buffers[0].buffer), guestFirst.data(), 16) && sameBytes(bufferBytes(array.buffers[1].buffer), guestSecond.data(), 32), "guest buffer contents were not uploaded");
|
|
const std::array<std::uint32_t, 3> data{7, 8, 9};
|
|
Require(findWrite(5).buffers.size() == 1 && findWrite(5).buffers[0].range == 12 && sameBytes(bufferBytes(findWrite(5).buffers[0].buffer), data.data(), 12), "shader data buffer is incorrect");
|
|
const std::array<std::uint32_t, 2> srt{1, 2};
|
|
Require(findWrite(43).buffers.size() == 1 && findWrite(43).buffers[0].range == 8 && sameBytes(bufferBytes(findWrite(43).buffers[0].buffer), srt.data(), 8), "flattened SRT buffer is incorrect");
|
|
Require(findWrite(44).buffers.size() == 1 && findWrite(44).buffers[0].range == 8 && sameBytes(bufferBytes(findWrite(44).buffers[0].buffer), guestThird.data(), 8), "fragment guest buffer is incorrect");
|
|
resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, VK_NULL_HANDLE);
|
|
Require(mock.boundPoint == VK_PIPELINE_BIND_POINT_GRAPHICS && mock.boundFirst == 0 && mock.boundSets == 1, "exactly one descriptor set must be bound at set zero");
|
|
auto& first = mock.memories.at(mock.bufferMemory.at(array.buffers[0].buffer));
|
|
std::memset(first.data(), 0xab, 16);
|
|
auto& shaderData = mock.memories.at(mock.bufferMemory.at(findWrite(5).buffers[0].buffer));
|
|
std::memset(shaderData.data(), 0xcd, 12);
|
|
resources.WriteBack();
|
|
Require(guestFirst[0] == 0xabababab && guestFirst[3] == 0xabababab, "guest buffer was not written back");
|
|
Require(guestSecond[0] == 1 && guestSecond[7] == 8 && guestThird[0] == 0xaaaaaaaa, "unmodified guest buffers changed on write back");
|
|
}
|
|
Require(mock.live == 0, "shader resources leaked Vulkan objects");
|
|
mock = MockVulkan{};
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
ShaderRecompiler::RecompileResult fragment;
|
|
AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, state.color, 0, 0);
|
|
Require(resources.Layout() != VK_NULL_HANDLE && mock.layoutBindings.empty() && mock.poolSizes.empty() && mock.writes.empty(), "a shader without bindings must produce only an empty set layout");
|
|
resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, VK_NULL_HANDLE);
|
|
Require(mock.boundSets == 0, "an empty descriptor set was bound");
|
|
resources.WriteBack();
|
|
}
|
|
Require(mock.live == 0, "empty shader resources leaked Vulkan objects");
|
|
mock = MockVulkan{};
|
|
{
|
|
ShaderRecompiler::RecompileResult compute;
|
|
compute.bindings.push_back(makeBinding(Role::GuestBuffers, 3, 1, vsharp(guestThird.data(), 8)));
|
|
const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &compute, 0};
|
|
AgcDriver::Graphics::ShaderResources resources(context, shader);
|
|
Require(mock.layoutBindings.size() == 1 && findLayoutBinding(3).stageFlags == VK_SHADER_STAGE_COMPUTE_BIT && findLayoutBinding(3).descriptorCount == 1, "compute layout binding is incorrect");
|
|
resources.Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, VK_NULL_HANDLE);
|
|
Require(mock.boundPoint == VK_PIPELINE_BIND_POINT_COMPUTE && mock.boundSets == 1, "compute descriptors were bound to the wrong bind point");
|
|
guestThird = {0xaaaaaaaa, 0xbbbbbbbb};
|
|
std::memset(mock.memories.at(mock.bufferMemory.at(findWrite(3).buffers[0].buffer)).data(), 0x5a, 8);
|
|
resources.WriteBack();
|
|
Require(guestThird[0] == 0x5a5a5a5a && guestThird[1] == 0x5a5a5a5a, "compute buffer was not written back");
|
|
guestThird = {0xaaaaaaaa, 0xbbbbbbbb};
|
|
}
|
|
Require(mock.live == 0, "compute resources leaked Vulkan objects");
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0};
|
|
expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(context, shader); }, "compute resources require a compute shader");
|
|
}
|
|
const auto base = makeBinding(Role::GuestBuffers, 0, 1, vsharp(guestThird.data(), 8));
|
|
const auto changed = [&](auto mutate) {
|
|
auto binding = base;
|
|
mutate(binding);
|
|
return binding;
|
|
};
|
|
expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::SampledImage; }), "unsupported descriptor role GuestImages");
|
|
expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::StorageImage; }), "unsupported descriptor role GuestImages");
|
|
expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestSamplers; binding.kind = Kind::Sampler; }), "unsupported descriptor role GuestSamplers");
|
|
expectSingleFailure(changed([](auto& binding) { binding.role = Role::Gds; binding.guestDescriptor.clear(); }), "unsupported descriptor role Gds");
|
|
expectSingleFailure(changed([](auto& binding) { binding.role = Role::BdaPagetable; binding.guestDescriptor.clear(); }), "BDA table and fault descriptors");
|
|
expectSingleFailure(changed([](auto& binding) { binding.role = Role::FaultBuffer; binding.guestDescriptor.clear(); }), "BDA table and fault descriptors");
|
|
expectSingleFailure(changed([](auto& binding) { binding.kind = Kind::UniformBuffer; }), "unsupported descriptor kind UniformBuffer");
|
|
expectSingleFailure(changed([](auto& binding) { binding.kind = Kind::UniformTexelBuffer; }), "unsupported descriptor kind UniformTexelBuffer");
|
|
expectSingleFailure(changed([](auto& binding) { binding.kind = Kind::StorageTexelBuffer; }), "unsupported descriptor kind StorageTexelBuffer");
|
|
expectSingleFailure(changed([](auto& binding) { binding.role = Role::ShaderData; binding.kind = Kind::SampledImage; }), "unsupported descriptor kind SampledImage");
|
|
expectSingleFailure(changed([](auto& binding) { binding.descriptorSet = 1; }), "unexpected descriptor set");
|
|
expectSingleFailure(changed([](auto& binding) { binding.readOnly = true; }), "read-only descriptors are unsupported");
|
|
expectSingleFailure(changed([](auto& binding) { binding.count = 0; }), "empty descriptor binding");
|
|
expectSingleFailure(changed([](auto& binding) { binding.count = 2; }), "four DWORDs per array element");
|
|
expectSingleFailure(changed([](auto& binding) { binding.role = Role::ShaderData; binding.count = 2; binding.guestDescriptor = {1, 2}; }), "must not be arrays");
|
|
expectSingleFailure(changed([](auto& binding) { binding.role = Role::ShaderData; binding.guestDescriptor.clear(); }), "empty shader data descriptor");
|
|
expectSingleFailure(changed([](auto& binding) { binding.role = Role::FlattenedSrt; binding.guestDescriptor.clear(); }), "empty shader data descriptor");
|
|
expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[0] = 0; binding.guestDescriptor[1] = 0; }), "null shader buffer descriptor address");
|
|
expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[2] = 0; }), "empty shader buffer descriptor");
|
|
expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 16u << 16u; }), "strided");
|
|
expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[3] = 0; }), "only raw buffer bounds");
|
|
expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[2] = 8192; }), "descriptor range limit");
|
|
expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast<const void*>(0x1000), 8); }), "not readable");
|
|
expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast<const void*>(state.color.address), 64); }), "aliases the render target");
|
|
expectSingleFailure(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast<const void*>(0x1000), 8)); }), "not readable");
|
|
expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast<const void*>(state.color.address), 64)); }), "aliases the render target");
|
|
expectSingleFailure(changed([](auto& binding) { binding.count = 17; binding.guestDescriptor.assign(68, 0); }), "per-stage limits");
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
ShaderRecompiler::RecompileResult fragment;
|
|
vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, vsharp(guestFirst.data(), 16)));
|
|
fragment.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, vsharp(guestSecond.data(), 32)));
|
|
expectResourceFailure(vertex, fragment, "duplicate shader binding");
|
|
fragment.bindings.front().binding = 1;
|
|
fragment.bindings.front().guestDescriptor = vsharp(guestFirst.data(), 16);
|
|
}
|
|
}
|
|
|
|
struct ModuleShape {
|
|
bool fragment = false;
|
|
bool push = false;
|
|
std::uint32_t pushLength = 32;
|
|
std::uint32_t pushStride = 4;
|
|
std::uint32_t bufferArray = 0;
|
|
bool plainBuffer = false;
|
|
bool shaderData = false;
|
|
bool vertexInput = false;
|
|
bool barycentric = false;
|
|
bool barycentricNoPerspective = false;
|
|
std::uint32_t barycentricComponents = 3;
|
|
bool perVertex = false;
|
|
std::uint32_t perVertexLength = 3;
|
|
bool parameterOutput = false;
|
|
};
|
|
|
|
void emit(std::vector<std::uint32_t>& out, spv::Op op, std::initializer_list<std::uint32_t> operands) {
|
|
out.push_back((static_cast<std::uint32_t>(operands.size() + 1) << 16u) | static_cast<std::uint32_t>(op));
|
|
out.insert(out.end(), operands.begin(), operands.end());
|
|
}
|
|
|
|
std::vector<std::uint32_t> makeModule(const ModuleShape& shape) {
|
|
std::vector<std::uint32_t> annotations;
|
|
std::vector<std::uint32_t> declarations;
|
|
std::vector<std::uint32_t> function;
|
|
std::uint32_t next = 1;
|
|
const auto id = [&] { return next++; };
|
|
const auto voidType = id();
|
|
const auto functionType = id();
|
|
const auto floatType = id();
|
|
const auto vectorType = id();
|
|
const auto uintType = id();
|
|
const auto outputPointer = id();
|
|
const auto output = id();
|
|
const auto main = id();
|
|
const auto label = id();
|
|
const auto inputPointer = id();
|
|
const auto input = id();
|
|
std::vector<std::uint32_t> extraInterface;
|
|
emit(declarations, spv::OpTypeVoid, {voidType});
|
|
emit(declarations, spv::OpTypeFunction, {functionType, voidType});
|
|
emit(declarations, spv::OpTypeFloat, {floatType, 32});
|
|
emit(declarations, spv::OpTypeVector, {vectorType, floatType, 4});
|
|
emit(declarations, spv::OpTypeInt, {uintType, 32, 0});
|
|
emit(declarations, spv::OpTypePointer, {outputPointer, spv::StorageClassOutput, vectorType});
|
|
emit(declarations, spv::OpVariable, {outputPointer, output, spv::StorageClassOutput});
|
|
if (shape.parameterOutput) {
|
|
const auto parameter = id();
|
|
emit(declarations, spv::OpVariable, {outputPointer, parameter, spv::StorageClassOutput});
|
|
emit(annotations, spv::OpDecorate, {parameter, spv::DecorationLocation, 0});
|
|
extraInterface.push_back(parameter);
|
|
}
|
|
if (shape.barycentric) {
|
|
const auto vector = id();
|
|
const auto pointer = id();
|
|
const auto variable = id();
|
|
emit(declarations, spv::OpTypeVector, {vector, floatType, shape.barycentricComponents});
|
|
emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassInput, vector});
|
|
emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassInput});
|
|
emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, shape.barycentricNoPerspective ? spv::BuiltInBaryCoordNoPerspKHR : spv::BuiltInBaryCoordKHR});
|
|
extraInterface.push_back(variable);
|
|
}
|
|
if (shape.perVertex) {
|
|
const auto length = id();
|
|
const auto array = id();
|
|
const auto pointer = id();
|
|
const auto variable = id();
|
|
emit(declarations, spv::OpConstant, {uintType, length, shape.perVertexLength});
|
|
emit(declarations, spv::OpTypeArray, {array, vectorType, length});
|
|
emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassInput, array});
|
|
emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassInput});
|
|
emit(annotations, spv::OpDecorate, {variable, spv::DecorationLocation, 0});
|
|
emit(annotations, spv::OpDecorate, {variable, spv::DecorationPerVertexKHR});
|
|
extraInterface.push_back(variable);
|
|
}
|
|
if (shape.vertexInput) {
|
|
emit(declarations, spv::OpTypePointer, {inputPointer, spv::StorageClassInput, vectorType});
|
|
emit(declarations, spv::OpVariable, {inputPointer, input, spv::StorageClassInput});
|
|
emit(annotations, spv::OpDecorate, {input, spv::DecorationLocation, 0});
|
|
}
|
|
if (shape.fragment) emit(annotations, spv::OpDecorate, {output, spv::DecorationLocation, 0});
|
|
else emit(annotations, spv::OpDecorate, {output, spv::DecorationBuiltIn, spv::BuiltInPosition});
|
|
if (shape.push) {
|
|
const auto length = id();
|
|
const auto array = id();
|
|
const auto block = id();
|
|
const auto pointer = id();
|
|
const auto variable = id();
|
|
emit(declarations, spv::OpConstant, {uintType, length, shape.pushLength});
|
|
emit(declarations, spv::OpTypeArray, {array, uintType, length});
|
|
emit(declarations, spv::OpTypeStruct, {block, array});
|
|
emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassPushConstant, block});
|
|
emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassPushConstant});
|
|
emit(annotations, spv::OpDecorate, {array, spv::DecorationArrayStride, shape.pushStride});
|
|
emit(annotations, spv::OpDecorate, {block, spv::DecorationBlock});
|
|
emit(annotations, spv::OpMemberDecorate, {block, 0, spv::DecorationOffset, 0});
|
|
}
|
|
if (shape.bufferArray != 0 || shape.plainBuffer || shape.shaderData) {
|
|
const auto runtime = id();
|
|
const auto block = id();
|
|
emit(declarations, spv::OpTypeRuntimeArray, {runtime, uintType});
|
|
emit(declarations, spv::OpTypeStruct, {block, runtime});
|
|
emit(annotations, spv::OpDecorate, {runtime, spv::DecorationArrayStride, 4});
|
|
emit(annotations, spv::OpDecorate, {block, spv::DecorationBlock});
|
|
emit(annotations, spv::OpMemberDecorate, {block, 0, spv::DecorationOffset, 0});
|
|
const auto declare = [&](std::uint32_t type, std::uint32_t binding) {
|
|
const auto pointer = id();
|
|
const auto variable = id();
|
|
emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassStorageBuffer, type});
|
|
emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassStorageBuffer});
|
|
emit(annotations, spv::OpDecorate, {variable, spv::DecorationDescriptorSet, 0});
|
|
emit(annotations, spv::OpDecorate, {variable, spv::DecorationBinding, binding});
|
|
};
|
|
if (shape.bufferArray != 0) {
|
|
const auto length = id();
|
|
const auto array = id();
|
|
emit(declarations, spv::OpConstant, {uintType, length, shape.bufferArray});
|
|
emit(declarations, spv::OpTypeArray, {array, block, length});
|
|
declare(array, 0);
|
|
}
|
|
if (shape.plainBuffer) declare(block, 0);
|
|
if (shape.shaderData) declare(block, 5);
|
|
}
|
|
emit(function, spv::OpFunction, {voidType, main, 0, functionType});
|
|
emit(function, spv::OpLabel, {label});
|
|
emit(function, spv::OpReturn, {});
|
|
emit(function, spv::OpFunctionEnd, {});
|
|
std::vector<std::uint32_t> words{spv::MagicNumber, 0x10300, 0, next, 0};
|
|
emit(words, spv::OpCapability, {spv::CapabilityShader});
|
|
if (shape.barycentric) {
|
|
emit(words, spv::OpCapability, {spv::CapabilityFragmentBarycentricKHR});
|
|
const std::string extension = "SPV_KHR_fragment_shader_barycentric";
|
|
const auto count = (extension.size() + 4) / 4;
|
|
words.push_back((static_cast<std::uint32_t>(count + 1) << 16u) | spv::OpExtension);
|
|
const auto start = words.size();
|
|
words.resize(start + count, 0);
|
|
for (std::size_t i = 0; i < extension.size(); ++i) words[start + i / 4] |= static_cast<std::uint32_t>(static_cast<unsigned char>(extension[i])) << ((i % 4) * 8);
|
|
}
|
|
emit(words, spv::OpMemoryModel, {spv::AddressingModelLogical, spv::MemoryModelGLSL450});
|
|
const auto entryPointOffset = words.size();
|
|
if (shape.vertexInput) emit(words, spv::OpEntryPoint, {spv::ExecutionModelVertex, main, 0x6e69616du, 0, output, input});
|
|
else emit(words, spv::OpEntryPoint, {shape.fragment ? spv::ExecutionModelFragment : spv::ExecutionModelVertex, main, 0x6e69616du, 0, output});
|
|
words[entryPointOffset] += static_cast<std::uint32_t>(extraInterface.size()) << 16u;
|
|
words.insert(words.end(), extraInterface.begin(), extraInterface.end());
|
|
if (shape.fragment) emit(words, spv::OpExecutionMode, {main, spv::ExecutionModeOriginUpperLeft});
|
|
words.insert(words.end(), annotations.begin(), annotations.end());
|
|
words.insert(words.end(), declarations.begin(), declarations.end());
|
|
words.insert(words.end(), function.begin(), function.end());
|
|
return words;
|
|
}
|
|
|
|
void validationTests() {
|
|
AgcDriver::Graphics::State state{};
|
|
state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex;
|
|
ShaderRecompiler::RecompileResult fragment;
|
|
fragment.spirv = makeModule({.fragment = true});
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({.parameterOutput = true});
|
|
ShaderRecompiler::RecompileResult pixel;
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}};
|
|
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
|
for (const auto noPerspective : {false, true}) {
|
|
pixel.spirv = makeModule({.fragment = true, .barycentric = true, .barycentricNoPerspective = noPerspective, .perVertex = true});
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, true);
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "fragmentShaderBarycentric");
|
|
}
|
|
pixel.spirv = makeModule({.fragment = true, .barycentric = true, .barycentricComponents = 4});
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, true); }, "invalid barycentric built-in");
|
|
pixel.spirv = makeModule({.fragment = true, .barycentric = true, .perVertex = true, .perVertexLength = 2});
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, true); }, "three vertices");
|
|
pixel.spirv = makeModule({.fragment = true, .perVertex = true});
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, true); }, "PerVertexKHR requires");
|
|
vertex.spirv = makeModule({.barycentric = true});
|
|
pixel.spirv = makeModule({.fragment = true});
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, true); }, "requires a fragment shader");
|
|
}
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({.vertexInput = true});
|
|
ShaderRecompiler::VertexAttribute attribute{0, 4, {{0x1000, 32u << 16u, 3, 77u << 12u}}, 0};
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}};
|
|
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "missing attribute metadata");
|
|
vertex.vertexAttributes.push_back(attribute);
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false);
|
|
vertex.vertexAttributes[0].components = 2;
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "metadata disagrees");
|
|
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 80, "incorrect strided vertex range");
|
|
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1); }, "record count");
|
|
attribute.fetchIndex = 1;
|
|
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 48, "instance attributes used the vertex index");
|
|
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 4); }, "record count");
|
|
attribute.resource.fields[1] = 0;
|
|
attribute.resource.fields[2] = 16;
|
|
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "zero stride must repeat one value");
|
|
attribute.resource.fields[2] = 8;
|
|
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range");
|
|
attribute.resource.fields[3] = 113u << 12u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeVertexFormat(attribute); }, "unsupported vertex format");
|
|
attribute.resource.fields = {0x1000, 32u << 16u, 3, 77u << 12u};
|
|
attribute.components = 2;
|
|
AgcDriver::Graphics::Context context{};
|
|
context.limits.maxVertexInputBindings = 16;
|
|
context.limits.maxVertexInputAttributes = 16;
|
|
context.limits.maxVertexInputBindingStride = 2048;
|
|
context.formatProperties = [](VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) {
|
|
*properties = {};
|
|
if (format == VK_FORMAT_R32G32_SFLOAT) properties->bufferFeatures = VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT;
|
|
};
|
|
const auto layout = AgcDriver::Graphics::BuildVertexInputLayout(context, std::span(&attribute, 1));
|
|
Require(layout.bindings.size() == 1 && layout.bindings[0].stride == 32 && layout.bindings[0].inputRate == VK_VERTEX_INPUT_RATE_INSTANCE, "incorrect instance input binding");
|
|
Require(layout.attributes[0].format == VK_FORMAT_R32G32_SFLOAT && layout.attributes[0].offset == 0 && layout.attributes[0].location == 0, "incorrect vertex attribute format or offset");
|
|
attribute.components = 4;
|
|
expectFailure([&] { AgcDriver::Graphics::BuildVertexInputLayout(context, std::span(&attribute, 1)); }, "device does not support vertex format");
|
|
attribute.components = 2;
|
|
attribute.resource.fields[1] |= 0x80000000u;
|
|
expectFailure([&] { AgcDriver::Graphics::BuildVertexInputLayout(context, std::span(&attribute, 1)); }, "descriptor flags");
|
|
}
|
|
for (const auto capability : {spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle}) {
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({});
|
|
vertex.spirv.insert(vertex.spirv.begin() + 5, {(2u << 16u) | spv::OpCapability, static_cast<std::uint32_t>(capability)});
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}};
|
|
VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
|
subgroup.supportedStages = VK_SHADER_STAGE_VERTEX_BIT;
|
|
subgroup.supportedOperations = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_BALLOT_BIT | VK_SUBGROUP_FEATURE_SHUFFLE_BIT;
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false);
|
|
subgroup.supportedStages = VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported for shader stage");
|
|
subgroup.supportedStages = VK_SHADER_STAGE_VERTEX_BIT;
|
|
subgroup.supportedOperations = capability == spv::CapabilityGroupNonUniform ? 0u : VK_SUBGROUP_FEATURE_BASIC_BIT;
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "device lacks operations");
|
|
}
|
|
const std::vector<std::uint32_t> words(8, 0);
|
|
const auto validate = [&](const ShaderRecompiler::RecompileResult& vertex, std::uint32_t fragmentOffset) {
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, fragmentOffset}}};
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false);
|
|
};
|
|
const auto pushed = [&](const ModuleShape& shape) {
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule(shape);
|
|
vertex.pushConstants.assign(8, std::byte{1});
|
|
vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, words));
|
|
return vertex;
|
|
};
|
|
validate(pushed({.push = true, .bufferArray = 2}), 8);
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({.shaderData = true});
|
|
vertex.bindings.push_back(makeBinding(Role::ShaderData, 5, 1, {1, 2}));
|
|
validate(vertex, 0);
|
|
}
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({.bufferArray = 1, .shaderData = true});
|
|
vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, {1, 2, 3, 4}));
|
|
vertex.bindings.push_back(makeBinding(Role::ShaderData, 5, 1, {1, 2}));
|
|
validate(vertex, 0);
|
|
}
|
|
expectFailure([&] { validate(pushed({.push = true, .pushLength = 16, .bufferArray = 2}), 8); }, "32 elements");
|
|
expectFailure([&] { validate(pushed({.push = true, .pushStride = 8, .bufferArray = 2}), 8); }, "ArrayStride of 4");
|
|
expectFailure([&] { validate(pushed({.push = false, .bufferArray = 2}), 8); }, "push constant metadata disagrees with SPIR-V");
|
|
{
|
|
auto vertex = pushed({.push = true, .bufferArray = 2});
|
|
vertex.pushConstants.clear();
|
|
expectFailure([&] { validate(vertex, 8); }, "invalid push constant interface");
|
|
}
|
|
{
|
|
auto vertex = pushed({.push = true, .bufferArray = 3});
|
|
expectFailure([&] { validate(vertex, 8); }, "descriptor array length disagrees");
|
|
}
|
|
{
|
|
auto vertex = pushed({.push = true, .plainBuffer = true});
|
|
expectFailure([&] { validate(vertex, 8); }, "must be declared as a descriptor array");
|
|
}
|
|
{
|
|
auto vertex = pushed({.push = true, .bufferArray = 2});
|
|
vertex.bindings.front().readOnly = true;
|
|
expectFailure([&] { validate(vertex, 8); }, "read-only descriptor metadata is unsupported");
|
|
}
|
|
{
|
|
auto vertex = pushed({.push = true, .bufferArray = 2});
|
|
vertex.bindings.front().descriptorSet = 1;
|
|
expectFailure([&] { validate(vertex, 8); }, "descriptor set other than zero");
|
|
}
|
|
{
|
|
auto vertex = pushed({.push = true, .bufferArray = 2});
|
|
vertex.bindings.front().kind = Kind::UniformBuffer;
|
|
expectFailure([&] { validate(vertex, 8); }, "disagrees with recompiler binding metadata");
|
|
}
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({.shaderData = true});
|
|
vertex.bindings.push_back(makeBinding(Role::ShaderData, 5, 2, {1, 2}));
|
|
expectFailure([&] { validate(vertex, 0); }, "binding count of one");
|
|
}
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({.shaderData = true});
|
|
vertex.bindings.push_back(makeBinding(Role::GuestSamplers, 5, 1, {1, 2}));
|
|
expectFailure([&] { validate(vertex, 0); }, "descriptor role is unsupported");
|
|
}
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({.shaderData = true});
|
|
expectFailure([&] { validate(vertex, 0); }, "absent from recompiler binding metadata");
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
int main() {
|
|
try {
|
|
stateTests();
|
|
DepthClipTests();
|
|
DisabledColorTests();
|
|
ShaderStageTests();
|
|
InitialContextTests();
|
|
pushConstantTests();
|
|
resourceTests();
|
|
validationTests();
|
|
mock = MockVulkan{};
|
|
auto bdaContext = mockContext();
|
|
bdaContext.bufferDeviceAddress = true;
|
|
RunBdaResourceTests(bdaContext, {
|
|
[](VkBuffer buffer) -> std::span<std::byte> { return mock.memories.at(mock.bufferMemory.at(buffer)); },
|
|
[](std::uint32_t binding) {
|
|
for (auto it = mock.writes.rbegin(); it != mock.writes.rend(); ++it) {
|
|
if (it->binding == binding) return it->buffers.at(0);
|
|
}
|
|
throw std::runtime_error("missing BDA test descriptor");
|
|
}
|
|
});
|
|
Require(mock.live == 0, "BDA resources leaked Vulkan objects");
|
|
RunGuestAllocationTests();
|
|
std::cout << "Graphics validation tests passed\n";
|
|
return 0;
|
|
} catch (const std::exception& error) {
|
|
std::cerr << error.what() << '\n';
|
|
return 1;
|
|
}
|
|
}
|