#include "BdaTests.hpp" #include "GraphicsTests.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.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 "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "SceShaders.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" #include "ControlFlow/RequestSerializer.hpp" #include "CacheKey.hpp" #include "Optimization/ResourceProgram.hpp" #include "BdaAbi.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern "C" std::terminate_handler APS5_VABI _ZSt13set_terminatePFvvE_nid_postfix(std::terminate_handler handler); namespace { using AgcDriver::Graphics::Require; alignas(256) std::array colorMemory{}; alignas(256) std::array sliceMemory{}; 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(colorMemory.data()); queue.context[0x318] = static_cast(address >> 8u); queue.context[0x390] = static_cast(address >> 40u); queue.context[0x10f] = std::bit_cast(32.0f); queue.context[0x110] = std::bit_cast(32.0f); queue.context[0x111] = std::bit_cast(-2.0f); queue.context[0x112] = std::bit_cast(2.0f); queue.context[0x113] = std::bit_cast(1.0f); queue.context[0x114] = 0; queue.context[0xb4] = 0; queue.context[0xb5] = std::bit_cast(1.0f); queue.shader[0x008] = 0x100; queue.shader[0x009] = 0; return queue; } template 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 hdrComponentSwapTests() { using namespace AgcDriver::Graphics; for (const auto number : {0u, 1u, 4u, 7u}) { auto queue = makeState(); queue.context[0x31c] = (queue.context[0x31c] & ~0x1f7cu) | (12u << 2u) | (number << 8u) | (1u << 11u); const auto address = reinterpret_cast(sliceMemory.data()); queue.context[0x318] = static_cast(address >> 8u); queue.context[0x390] = static_cast(address >> 40u); queue.context[0x1b3] = queue.context[0x1b4] = 2; auto state = DecodeState(queue); const auto expected = number == 0 ? VK_FORMAT_R16G16B16A16_UNORM : number == 1 ? VK_FORMAT_R16G16B16A16_SNORM : number == 4 ? VK_FORMAT_R16G16B16A16_UINT : VK_FORMAT_R16G16B16A16_SFLOAT; Require(state.color.format == expected && state.color.elementBytes == 8, "alternate 16-bit color format was not decoded"); Require(state.color.componentMapping == 0xc6u && ExportMappings(state)[0] == 0xc6u, "alternate 16-bit color export did not swap red and blue"); Require(DecodePixelStageInfo(queue.context, ExportMappings(state)).targetExportMapping[0] == 0xc6u, "pixel export lost the alternate color mapping"); queue.context[0x8e] = 1; state = DecodeState(queue); Require(state.blend.colorWriteMask == VK_COLOR_COMPONENT_B_BIT, "alternate red-only write did not select the third storage component"); queue.context[0x8e] = 0xf; queue.context[0x1e0] = (1u << 30u) | 13u; for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = std::bit_cast(float(i + 1) / 8.0f); state = DecodeState(queue); Require(state.blendConstants == std::array{0.375f, 0.25f, 0.125f, 0.5f}, "alternate color blend constants were not swizzled with the export"); queue.context[0x31c] &= ~(1u << 11u); state = DecodeState(queue); Require(state.color.componentMapping == 0xe4u && state.blendConstants == std::array{0.125f, 0.25f, 0.375f, 0.5f}, "standard color mapping changed"); queue.context[0x31c] |= 2u << 11u; expectFailure([&] { DecodeState(queue); }, "reversed component order"); } auto queue = makeState(); queue.context[0x31c] = (queue.context[0x31c] & ~0x1f7cu) | (12u << 2u) | (7u << 8u) | (1u << 11u); for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x31du}) queue.context[offset + 0xfu] = queue.context.at(offset); for (const auto offset : {0x390u, 0x3b0u, 0x3b8u}) queue.context[offset + 1u] = queue.context.at(offset); queue.context[0x31c + 0xfu] &= ~(1u << 11u); queue.context[0x1c5] = 0x99u; queue.context[0x8e] = queue.context[0x8f] = 0xffu; queue.context[0x1e0] = queue.context[0x1e1] = (1u << 30u) | 13u; for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = std::bit_cast(float(i + 1) / 8.0f); expectFailure([&] { DecodeState(queue); }, "conflicting blend constants"); queue.context[0x107] = queue.context[0x105]; Require(DecodeState(queue).colors.size() == 2, "compatible constants rejected mixed component orders"); } 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"); for (std::uint32_t slot = 0; slot < 8; ++slot) { const auto info = 0x31cu + 0xfu * slot; Require(initial.context.at(info) == 0, "reset color target was not disabled"); initial.context[info] = 10u << 2u; } initial.ClearContext(); initial.context[0x8e] = initial.context[0x8f] = 0xffffffffu; Require(AgcDriver::Graphics::ColorWriteMask(initial.context) == 0, "context clear kept old color targets enabled"); 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(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; queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; (void)AgcDriver::Graphics::DecodeState(queue); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "primitive restart rejected a non-indexed draw"); queue.userConfig[0x242] = 9; Require(AgcDriver::Graphics::DrawRejection(queue, true).find("point, line and triangle") != std::string::npos, "primitive restart was accepted for patches"); queue.userConfig[0x242] = 6; queue.context[0x103] = 0xffffffffu; Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "primitive restart was rejected for an indexed strip"); Require(AgcDriver::Graphics::DecodeState(queue).primitiveRestart, "primitive restart was not decoded for a strip"); queue.context[0x103] = 5; Require(AgcDriver::Graphics::DrawRejection(queue, true).find("all ones") != std::string::npos, "a restart index other than all ones was accepted"); queue = makeState(); queue.context[0x293] = 0x06020000u; (void)AgcDriver::Graphics::DecodeState(queue); Require(AgcDriver::Graphics::DrawRejection(queue, false).find("sample iteration") == std::string::npos, "per-engine primitive discard was rejected"); queue.context[0x293] = 0x760201bcu; (void)AgcDriver::Graphics::DecodeState(queue); Require(AgcDriver::Graphics::DrawRejection(queue, false).find("sample iteration") == std::string::npos, "an out-of-order watermark without out-of-order rasterization was rejected"); queue.context[0x293] = 0x7e0201bcu; Require(AgcDriver::Graphics::DrawRejection(queue, false).find("sample iteration") != std::string::npos, "out-of-order rasterization was accepted"); queue.context[0x293] = 0x06030000u; Require(AgcDriver::Graphics::DrawRejection(queue, false).find("sample iteration") != std::string::npos, "per-sample shading was accepted"); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "sample iteration"); queue.context[0x293] = 0; for (const auto disabled : {0x6000u, 0x00100000u, 0u, 0x000e6000u}) { queue.context[0x313] = disabled; Require(AgcDriver::Graphics::DecodeState(queue).conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT, "disabled conservative rasterization decoded as enabled"); Require(AgcDriver::Graphics::DrawRejection(queue, false).find("PA_SC_CONSERVATIVE") == std::string::npos, "disabled conservative rasterization was rejected"); } for (const auto enabled : {0x00e00001u, 0x01e00022u, 0x000e6001u, 0x000e6020u}) { queue.context[0x313] = enabled; Require(AgcDriver::Graphics::DrawRejection(queue, false).find("PA_SC_CONSERVATIVE") != std::string::npos, "conservative rasterization was accepted"); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_SC_CONSERVATIVE"); } queue.context[0x313] = 0x6000u; 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[0x90] = 0x10003; 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, "a scissor that applies the zero window offset changed"); queue.context[0x90] = 0x80008000; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "scissor reserved bits"); queue.context[0x90] = 0x80010003; queue.context[0x31c] |= 0x10000000; queue.context[0x325] = 0x1234; Require(AgcDriver::Graphics::DecodeState(queue).color.dccAddress == 0x123400, "DCC key address decode changed"); queue = makeState(); queue.context.erase(0x3b8); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); queue = makeState(); queue.context[0x3b8] |= 1u << 14u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color tile mode"); queue = makeState(); queue.context[0x3b0] = (62u << 14u) | 3u; Require(AgcDriver::Graphics::DecodeState(queue).color.bytes == 64u * 4u * 4u, "padded linear pitch changed"); queue = makeState(); queue.context[0x8e] = 0xff; queue.context[0x8f] = 0xff; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 0"); queue = makeState(); queue.context[0x200] = 2; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth"); queue = makeState(); queue.context[0x200] = 0x007007b4; queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; (void)AgcDriver::Graphics::DecodeState(queue); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a depth write without the depth test was rejected"); queue = makeState(); queue.context[0x200] = 0x007007b6; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth"); queue = makeState(); queue.context[0x010] = 0x80000180; queue.context[0x011] = 0x20000180; queue.context[0x200] = 0x007007b3; queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; state = AgcDriver::Graphics::DecodeState(queue); Require(!state.depthTest && !state.stencilTest, "tests on absent depth and stencil planes were kept"); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "tests on absent depth and stencil planes were rejected"); queue.context[0x200] = 0x007007bbu; state = AgcDriver::Graphics::DecodeState(queue); Require(!state.depth.has_value() && !state.depthTest && !state.depthBoundsTest && !state.stencilTest, "a depth bounds test on absent depth and stencil planes was kept"); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a depth bounds test on absent depth and stencil planes was rejected"); queue.context[0x200] = 8u; Require(!AgcDriver::Graphics::DecodeState(queue).depthBoundsTest && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a depth bounds test alone on absent depth and stencil planes was kept or rejected"); queue.context[0x200] = 0x007007b3; queue.context[0x011] = 0x20000181; queue.context[0x012] = 0x00001000; queue.context[0x013] = 0x00002000; queue.context[0x015] = 0x00002000; queue.context[0x007] = 0x003f003f; queue.context[0x000] = 0; queue.context[0x002] = 0; queue.context[0x00a] = 0; queue.context[0x00b] = 0x3f800000; queue.context[0x10b] = 0; queue.context[0x10c] = 0x01ffff00; queue.context[0x10d] = 0x01ffff00; state =AgcDriver::Graphics::DecodeState(queue); Require(!state.depthTest && state.stencilTest, "a depth test on an absent depth plane was kept beside a stencil plane"); queue.context[0x200] = 0x007007bbu; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth bounds without a depth plane"); queue = makeState(); queue.context[0x010] = 0x80000183; queue.context[0x011] = 0x20000180; queue.context[0x012] = 0x00001000; queue.context[0x014] = 0x00001000; queue.context[0x007] = 0x0001001f; queue.context[0x000] = 0; queue.context[0x002] = 0; queue.context[0x00a] = 0; queue.context[0x00b] = 0x3f800000; queue.context[0x200] = 0x007007b1; queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; state = AgcDriver::Graphics::DecodeState(queue); Require(!state.depth.has_value() && !state.stencilTest && state.renderExtent.width == state.color.extent.width && state.renderExtent.height == state.color.extent.height, "a stencil test without a stencil plane bound the depth plane it does not test"); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a stencil test without a stencil plane beside an untested depth plane was rejected"); queue.context[0x200] = 0x00700231; state = AgcDriver::Graphics::DecodeState(queue); Require(!state.depth.has_value() && !state.stencilTest && state.renderExtent.width == state.color.extent.width && state.renderExtent.height == state.color.extent.height, "a failing stencil test without a stencil plane bound the depth plane it does not test"); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a failing stencil test without a stencil plane beside an untested depth plane was rejected"); queue.context[0x200] = 0x007007b3; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depth.has_value() && state.depthTest && !state.stencilTest && state.renderExtent.width == 32 && state.renderExtent.height == 2, "a depth test on a bound depth plane lost its surface"); queue = makeState(); queue.context[0x10f] = 0x7fc00000; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "non-finite"); // The register facade: every register the decoders read is in DrawKeyRegisters (a wrong hit of // the draw key otherwise), and nothing is recorded without a log pointer. queue = makeState(); std::vector log; AgcDriver::Graphics::RegisterReadLog() = &log; queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; state = AgcDriver::Graphics::DecodeState(queue); Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "precheck rejected the reference state"); static_cast(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state))); AgcDriver::Graphics::RegisterReadLog() = nullptr; Require(!log.empty(), "the register facade recorded nothing"); for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a register the decoders read: " + std::string(AgcDriver::Graphics::RegisterBankName(read.bank)) + " " + std::to_string(read.offset)); Require(!AgcDriver::Graphics::DrawKeyCovers({AgcDriver::Graphics::RegisterBank::Context, 0x100}) && AgcDriver::Graphics::DrawKeyCovers({AgcDriver::Graphics::RegisterBank::Shader, 0xab}) && !AgcDriver::Graphics::DrawKeyCovers({AgcDriver::Graphics::RegisterBank::Shader, 0xac}), "DrawKeyRegisters coverage changed"); log.clear(); static_cast(AgcDriver::Graphics::DecodeState(queue)); Require(log.empty(), "the register facade recorded without a log"); queue = makeState(); queue.context.erase(0x1b3); queue.context.erase(0x1b4); queue.context.erase(0x1c5); queue.shader[0x008] = 0x100; queue.shader[0x009] = 0; Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program address was read as unset"); Require(AgcDriver::Graphics::DrawRejection(queue, true).find("missing register at DWORD 0x1b3") != std::string::npos, "a real pixel program without SPI_PS_INPUT_ENA was accepted"); expectFailure([&] { AgcDriver::Graphics::DecodePixelStageInfo(queue.context, std::array{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); }, "missing register"); AgcDriver::QueueState cleared; cleared.context[0x1b3] = 2; cleared.context[0x1b4] = 2; cleared.context[0x1b6] = 2; cleared.context[0x192] = 7; const auto clearedPixel = AgcDriver::Graphics::DecodePixelStageInfo(cleared.context, std::array{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); Require(clearedPixel.interpolatorSettings[0] == 0 && clearedPixel.interpolatorSettings[1] == 7, "an unwritten SPI_PS_INPUT_CNTL_0 did not read as its clear-state value"); cleared.ClearContext(); for (std::uint32_t i = 0; i < 32; ++i) Require(cleared.context.at(0x191 + i) == 0, "CLEAR_STATE did not reset SPI_PS_INPUT_CNTL"); queue.shader[0x008] = 0; Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "a zero pixel program address was not read as unset"); Require(AgcDriver::Graphics::DrawRejection(queue, true).find("writes color") != std::string::npos, "a draw without a pixel program that writes color was accepted"); queue.shader.erase(0x008); queue.shader.erase(0x009); Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "an unwritten pixel program address was not read as unset"); Require(AgcDriver::Graphics::DrawRejection(queue, true).find("writes color") != std::string::npos, "a draw with an unwritten pixel program that writes color was accepted"); queue.context[0x8e] = 0; Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a depth-only draw with an unwritten pixel program was rejected"); queue.shader[0x008] = 0; queue.shader[0x009] = 0; Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a depth-only draw without a pixel program was rejected"); state = AgcDriver::Graphics::DecodeState(queue); Require(!state.hasColorTarget, "a depth-only draw without a pixel program decoded a color target"); const auto unset = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state), true); Require(unset.inputAddr == ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCenter), "unset pixel inputs of the null program did not read as PERSP_CENTER_ENA"); for (const auto mode : unset.targetOutputMode) Require(mode == 0, "an unset SPI_SHADER_COL_FORMAT exported a color"); queue.shader[0x008] = 0x100; queue.context[0x1c4] = 0; queue.context[0x203] = 0; Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program that writes nothing and cannot run was not skipped"); Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a depth-only draw whose pixel program cannot run was rejected"); for (const auto [control, what] : {std::pair{0x400u, "EXEC_ON_NOOP"}, std::pair{0x40u, "KILL_ENABLE"}}) { queue.context[0x203] = control; Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), std::string("a pixel program with ") + what + " was skipped"); Require(AgcDriver::Graphics::DrawRejection(queue, true).find("missing register at DWORD 0x1b3") != std::string::npos, std::string("a pixel program with ") + what + " ran without SPI_PS_INPUT_ENA"); } queue.context[0x203] = 1; Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program with Z_EXPORT_ENABLE was skipped"); queue.context[0x203] = 0; queue.context[0x1c4] = 1; Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program exporting depth was skipped"); queue.context[0x1c4] = 0; queue.context[0x8e] = 0xf; Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program writing color was skipped"); queue.context[0x1b3] = queue.context[0x1b4] = queue.context[0x1b6] = 0xffffffffu; queue.context[0x1c5] = queue.context[0x203] = 0xffffffffu; const auto disabled = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state), true); Require(disabled.interpolatorCount == 0 && disabled.inputAddr == ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCenter), "the null pixel program inherited stale input state"); Require(!disabled.pixelKillEnable && !disabled.depthExportEnable && !disabled.sampleMaskExportEnable, "the null pixel program inherited stale exports"); for (const auto mode : disabled.targetOutputMode) Require(mode == 0, "the null pixel program inherited stale color exports"); expectFailure([&] { AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state), false); }, "input count exceeds 32"); } VkFormatFeatureFlags srgb8Features = 0; void srgb8FormatProperties(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { *properties = {}; if (format == VK_FORMAT_R8_SRGB) properties->optimalTilingFeatures = srgb8Features; } void setProxyVariable(const char* value) { #ifdef _WIN32 _putenv_s("APS5_SRGB_ATTACHMENT_PROXY", value); #else if (*value == 0) unsetenv("APS5_SRGB_ATTACHMENT_PROXY"); else setenv("APS5_SRGB_ATTACHMENT_PROXY", value, 1); #endif } void srgb8TargetTests() { setProxyVariable(""); auto queue = makeState(); queue.context[0x31c] = 0x8604; const auto color = AgcDriver::Graphics::DecodeState(queue).color; Require(color.format == VK_FORMAT_R8_SRGB && color.elementBytes == 1 && color.componentMapping == 0xe4u, "the 8_SRGB color target did not decode as R8_SRGB"); for (std::uint32_t swap = 1; swap < 4; ++swap) { queue.context[0x31c] = 0x8604 | (swap << 11u); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color format 1 number type 6 component swap " + std::to_string(swap)); } queue.context[0x31c] = 0x860c; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color format 3 number type 6 component swap 0"); AgcDriver::Graphics::Context context{}; context.formatProperties = srgb8FormatProperties; const auto proxyOn = [&](std::uintptr_t device, VkFormatFeatureFlags features, VkFormat format = VK_FORMAT_R8_SRGB) { srgb8Features = features; context.physical = reinterpret_cast(device); return AgcDriver::Graphics::AttachmentProxyFormat(context, format); }; constexpr VkFormatFeatureFlags attachment = VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT; constexpr VkFormatFeatureFlags blend = VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT; Require(proxyOn(0x1000, VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == VK_FORMAT_R8G8B8A8_SRGB, "a device without R8_SRGB attachments must render 8_SRGB through RGBA8_SRGB"); Require(proxyOn(0x2000, attachment) == VK_FORMAT_R8G8B8A8_SRGB, "a device without R8_SRGB blending must render 8_SRGB through RGBA8_SRGB"); Require(proxyOn(0x3000, attachment | blend) == VK_FORMAT_UNDEFINED, "a device with R8_SRGB attachments must render 8_SRGB directly"); Require(proxyOn(0x3000, 0) == VK_FORMAT_UNDEFINED, "the attachment proxy decision is not kept per device"); Require(proxyOn(0x4000, 0, VK_FORMAT_R8_UNORM) == VK_FORMAT_UNDEFINED && proxyOn(0x4000, 0, VK_FORMAT_R8G8_SRGB) == VK_FORMAT_UNDEFINED, "only 8_SRGB targets may render through a proxy"); setProxyVariable("1"); Require(proxyOn(0x5000, attachment | blend) == VK_FORMAT_R8G8B8A8_SRGB, "APS5_SRGB_ATTACHMENT_PROXY=1 did not force the proxy"); setProxyVariable(""); } void hardwareScreenOffsetTests() { auto queue = makeState(); queue.context[0x90] = 0x80010003; queue.context[0x91] = 0x30020; const auto reference = AgcDriver::Graphics::DecodeState(queue); for (const auto offset : {0u, 1u, 0x10000u, 0x0020003cu, 0x01ff0000u, 0x000001ffu, 0x01ff01ffu}) { queue.context[0x8d] = offset; const auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.viewport.x == reference.viewport.x && state.viewport.y == reference.viewport.y && state.viewport.width == reference.viewport.width && state.viewport.height == reference.viewport.height && state.viewport.minDepth == reference.viewport.minDepth && state.viewport.maxDepth == reference.viewport.maxDepth, "hardware guard-band offset changed the viewport"); Require(state.scissor.offset.x == reference.scissor.offset.x && state.scissor.offset.y == reference.scissor.offset.y && state.scissor.extent.width == reference.scissor.extent.width && state.scissor.extent.height == reference.scissor.extent.height, "hardware guard-band offset changed the scissor"); Require(state.renderExtent.width == reference.renderExtent.width && state.renderExtent.height == reference.renderExtent.height, "hardware guard-band offset changed the framebuffer extent"); } for (std::uint32_t bit = 0; bit < 32; ++bit) { if (bit < 9 || (bit >= 16 && bit < 25)) continue; queue.context[0x8d] = 1u << bit; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); } queue.context.erase(0x8d); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } void MsaaEnableSingleSampleTests() { using AgcDriver::Graphics::DecodeState; using AgcDriver::Graphics::DrawRejection; const auto enabled = [] { auto queue = makeState(); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; queue.context[0x292] = 3; for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0; return queue; }; const auto refused = [](const AgcDriver::QueueState& queue, std::string_view reason) { expectFailure([&] { DecodeState(queue); }, reason); const auto rejection = DrawRejection(queue, false); Require(rejection.find(reason) != std::string::npos, "the precheck did not refuse MSAA_ENABLE on a single-sample target (" + std::string(reason) + "): " + rejection); }; auto queue = enabled(); auto clear = queue; clear.context[0x292] = 2; const auto baseline = DecodeState(clear); for (const auto locations : {0x00000000u, 0xcc44cc00u}) { for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = locations; const auto state = DecodeState(queue); Require(state.samples == 1 && state.colors.size() == 1 && state.colors[0].samples == 1 && !state.alphaToCoverage, "MSAA_ENABLE on a single-sample target did not decode as single-sampled"); Require(state.topology == baseline.topology && state.conservativeRasterization == baseline.conservativeRasterization && state.cullMode == baseline.cullMode && state.frontFace == baseline.frontFace, "MSAA_ENABLE on a single-sample target changed the rasterizer state"); Require(state.scissor.offset.x == baseline.scissor.offset.x && state.scissor.offset.y == baseline.scissor.offset.y && state.scissor.extent.width == baseline.scissor.extent.width && state.scissor.extent.height == baseline.scissor.extent.height, "MSAA_ENABLE on a single-sample target changed the scissor"); Require(state.colors[0].address == baseline.colors[0].address && state.colors[0].format == baseline.colors[0].format && state.blend.colorWriteMask == baseline.blend.colorWriteMask, "MSAA_ENABLE on a single-sample target changed the color target"); const auto rejection = DrawRejection(queue, false); Require(rejection.empty(), "the precheck refused MSAA_ENABLE on a single-sample target: " + rejection); } for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { for (const auto location : {0x04u, 0x40u, 0xc4u}) { queue = enabled(); queue.context[0x2fe + pixel * 4u] = location; refused(queue, "off the pixel center"); } } queue = enabled(); queue.context.erase(0x306); expectFailure([&] { DecodeState(queue); }, "missing register"); queue = enabled(); queue.context[0x2dc] = 0xaa01; refused(queue, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); queue = enabled(); queue.context[0x313] = 0x6001; refused(queue, "MSAA_ENABLE on a single-sample target with conservative rasterization"); for (const auto primitive : {2u, 7u, 17u}) { queue = enabled(); queue.userConfig[0x242] = primitive; refused(queue, "primitives other than triangles"); } queue = enabled(); queue.userConfig[0x242] = 1; Require(DrawRejection(queue, false).find("primitives other than triangles") != std::string::npos, "the precheck accepted MSAA_ENABLE on a single-sample target for points"); for (const auto primitive : {5u, 6u}) { queue = enabled(); queue.userConfig[0x242] = primitive; Require(DecodeState(queue).samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target was refused for a triangle fan or strip"); } queue = enabled(); queue.context[0x292] = 7; refused(queue, "scan conversion mode"); clear.context[0x2fe] = 0x44; clear.context[0x2dc] = 0xaa01; Require(DecodeState(clear).alphaToCoverage && DrawRejection(clear, false).empty(), "a single-sample target without MSAA_ENABLE was refused over its sample locations or alpha-to-coverage"); } 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.context[0x2ab] = 4; 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"); Require(stages.mesh->maxVertices == 64 && stages.mesh->maxPrimitives == 21 && stages.mesh->threadsPerGroup == 64 && stages.mesh->esgsItemSize == 4, "geometry subgroup outputs changed"); { auto fan = makeState(); fan.userConfig[0x242] = 5; fan.userConfig[0x24b] = 1; fan.context[0x103] = 0xffffffffu; fan.context[0x2d5] = 0x2030; fan.userConfig[0x25b] = 0x4020; fan.context[0x1ff] = 256; fan.context[0x2ce] = 8; fan.context[0x29b] = 2; fan.context[0x2ab] = 4; fan.shader[0x8a] = 3u << 29u; fan.shader[0x8b] = 3u << 16u; fan.context[0x1b3] = 2; fan.context[0x1b4] = 2; const auto state = AgcDriver::Graphics::DecodeState(fan); Require(AgcDriver::Graphics::DrawRejection(fan, true).empty(), "the precheck rejected an indexed triangle fan with restart into a geometry shader"); Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN && state.primitiveRestart && state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.mesh, "a triangle fan did not decode as geometry input"); const auto& mesh = *state.stages.mesh; Require(mesh.inputPrimitive == 5 && mesh.primitivesPerGroup == 30 && mesh.verticesPerGroup == 32 && mesh.maxVertices == 256 && mesh.maxPrimitives == 192 && mesh.threadsPerGroup == 256 && mesh.esgsItemSize == 4, "triangle fan subgroup assembly changed"); fan.userConfig[0x25b] = (3u << 9u) | 3u; Require(AgcDriver::Graphics::DecodeState(fan).stages.mesh->primitivesPerGroup == 1, "a three-vertex subgroup did not take one fan triangle"); fan.userConfig[0x25b] = (2u << 9u) | 3u; expectFailure([&] { AgcDriver::Graphics::DecodeState(fan); }, "invalid geometry subgroup"); fan.userConfig[0x25b] = 0x4020; fan.userConfig[0x242] = 3; expectFailure([&] { AgcDriver::Graphics::DecodeState(fan); }, "unsupported geometry input or output assembly"); } queue.context[0x2ab] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid VGT_ESGS_RING_ITEMSIZE"); queue.context[0x2ab] = 4; 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] = 0xc355; queue.context[0x2db] = 0x40049; queue.context[0x286] = 0x42800000; queue.context[0x287] = 0x3f800000; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported tessellation routing"); queue.context[0x2d5] = 0x0200010d; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported tessellation routing"); queue.context[0x2d5] = 0x0200210d; { std::vector tessellationLog; AgcDriver::Graphics::RegisterReadLog() = &tessellationLog; stages = AgcDriver::Graphics::DecodeState(queue).stages; AgcDriver::Graphics::RegisterReadLog() = nullptr; for (const auto read : tessellationLog) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a tessellation register the decoder reads: " + std::to_string(read.offset)); } Require(stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && stages.tessellation, "tessellation routing changed"); const auto& tessellation = *stages.tessellation; Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2, "the passthrough triangle-patch configuration decoded differently"); for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "only triangular"}, std::pair{0x40069u, "only triangular"}, std::pair{0x4006au, "only triangular"}}) { queue.context[0x2db] = parameters; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); } queue.context[0x2db] = 0x40049; for (const auto [maximum, minimum] : {std::pair{0x41100000u, 0x3f800000u}, std::pair{0x42800000u, 0x40000000u}}) { queue.context[0x286] = maximum; queue.context[0x287] = minimum; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "tessellation level clamps"); } queue.context[0x286] = 0x42800000; queue.context[0x287] = 0x3f800000; queue.context[0x2d5] = 0x202d; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "combined tessellation and geometry"); queue.context[0x2d5] = 0x0200210d; 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, 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 NggVertexWorkgroupTests() { auto queue = makeState(); queue.context[0x2d5] = 0x12010; queue.userConfig[0x242] = 4; queue.userConfig[0x25b] = 0x10080; queue.context[0x1ff] = 0x80; queue.context[0x2ce] = 0; queue.context[0x29b] = 2; queue.context[0x2ab] = 1; queue.shader[0x8a] = 0x222c0009; queue.shader[0x8b] = 0x43001e; queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; Require(AgcDriver::Graphics::DecodeState(queue).stages.path == AgcDriver::Graphics::ShaderPath::Vertex, "an NGG vertex program without a workgroup left the vertex path"); auto state = AgcDriver::Graphics::DecodeState(queue, true); Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.mesh, "an NGG vertex program that needs a workgroup did not take the mesh path"); auto mesh = *state.stages.mesh; Require(mesh.inputPrimitive == 4 && mesh.primitivesPerGroup == 42 && mesh.verticesPerGroup == 126 && mesh.maxVertices == 128 && mesh.maxPrimitives == 128 && mesh.threadsPerGroup == 128 && mesh.ldsSizeDwords == 1024 && mesh.esgsItemSize == 1, "the NGG vertex subgroup of a triangle list changed"); queue.userConfig[0x242] = 6; mesh = *AgcDriver::Graphics::DecodeState(queue, true).stages.mesh; Require(mesh.primitivesPerGroup == 126 && mesh.verticesPerGroup == 128 && mesh.maxPrimitives == 128, "the NGG vertex subgroup of a triangle strip is not bounded by its vertex budget"); queue.shader[0x8a] = 0x222c0005; queue.shader[0x8b] = 0x400030; Require(AgcDriver::Graphics::DecodeState(queue, true).stages.mesh.has_value(), "an NGG vertex program that loads only its vertex ID was rejected"); queue.shader[0x8a] = 0x222c0009; queue.shader[0x8b] = 0x43001e; queue.context[0x1ff] = 2; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "invalid geometry subgroup output"); queue.context[0x1ff] = 0x80; for (const auto stages : {0x02012010u, 0x00012030u, 0x00010010u}) { queue.context[0x2d5] = stages; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "requires PRIMGEN_EN without GS_EN, PRIMGEN_PASSTHRU_EN or tessellation"); } } constexpr std::array IdentityExports{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}; std::vector pixelBuiltinsRead(std::uint32_t ena, std::uint32_t addr, std::uint32_t source) { auto queue = makeState(); queue.context[0x1b3] = ena; queue.context[0x1b4] = addr; const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); const std::array code{0xf800180fu, source * 0x01010101u, 0xbf810000u}; ShaderRecompiler::RecompileRequest request{}; request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; request.context.waveSize = 64; request.context.pixel = pixel; request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; request.target.fragmentShaderBarycentricEnabled = true; request.layout.pushConstantSizeBytes = 128; request.useCache = false; const auto result = ShaderRecompiler::Recompile(request); const auto& words = result.spirv.Words(); std::map builtins; std::vector read; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { const auto op = static_cast(words[at] & 0xffffu); if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn) builtins[words[at + 1]] = static_cast(words[at + 3]); if (op != spv::OpLoad && op != spv::OpAccessChain && op != spv::OpInBoundsAccessChain) continue; const auto found = builtins.find(words[at + 3]); if (found != builtins.end() && std::find(read.begin(), read.end(), found->second) == read.end()) read.push_back(found->second); } return read; } bool readsBuiltin(const std::vector& read, spv::BuiltIn builtin) { return std::find(read.begin(), read.end(), builtin) != read.end(); } std::vector pixelNoPerspectiveLocations(bool barycentricEnabled) { auto queue = makeState(); queue.context[0x1b3] = 0x22u; queue.context[0x1b4] = 0x22u; queue.context[0x1b6] = 2u; queue.context[0x191] = 0u; queue.context[0x192] = 1u; const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); const std::array code{0xc8100000u, 0xc8110001u, 0xc8140402u, 0xc8150403u, 0xf800180fu, 0x05040504u, 0xbf810000u, 0xbf810000u}; ShaderRecompiler::RecompileRequest request{}; request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; request.context.waveSize = 64; request.context.pixel = pixel; request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; request.target.fragmentShaderBarycentricEnabled = barycentricEnabled; request.layout.pushConstantSizeBytes = 128; request.useCache = false; const auto result = ShaderRecompiler::Recompile(request); const auto& words = result.spirv.Words(); std::map locations; std::vector noPerspective; std::uint32_t perVertex = 0; bool perspectiveBarycentrics = false; bool linearBarycentrics = false; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { if (static_cast(words[at] & 0xffffu) != spv::OpDecorate) continue; if (words[at + 2] == spv::DecorationLocation) locations[words[at + 1]] = words[at + 3]; if (words[at + 2] == spv::DecorationNoPerspective) noPerspective.push_back(words[at + 1]); if (words[at + 2] == spv::DecorationPerVertexKHR) perVertex++; if (words[at + 2] == spv::DecorationBuiltIn) { perspectiveBarycentrics |= words[at + 3] == spv::BuiltInBaryCoordKHR; linearBarycentrics |= words[at + 3] == spv::BuiltInBaryCoordNoPerspKHR; } } if (barycentricEnabled) Require(perVertex == 2u && perspectiveBarycentrics && linearBarycentrics, "explicit interpolation must preserve both parameter vertices and both barycentric inputs"); std::vector result2; for (const auto id : noPerspective) result2.push_back(locations.count(id) != 0 ? locations.at(id) : 0xffffffffu); return result2; } void ComputeScratchTests() { auto queue = makeState(); auto& shader = queue.shader; shader[0x207] = 64u; shader[0x208] = 1u; shader[0x209] = 1u; shader[0x213] = 0x1u; std::vector header(sizeof(Shader)); Shader agc{}; agc.scratch_size_dw_per_thread = 24; std::memcpy(header.data(), &agc, sizeof(Shader)); const auto compute = AgcDriver::Graphics::DecodeComputeStageInfo(shader, header); Require(compute.scratchDwords == 24u, "SCRATCH_EN did not take the AGC header's per-thread scratch size"); agc.scratch_size_dw_per_thread = 0; std::memcpy(header.data(), &agc, sizeof(Shader)); expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeComputeStageInfo(shader, header)); }, "zero scratch size"); shader[0x213] = 0u; Require(AgcDriver::Graphics::DecodeComputeStageInfo(shader, {}).scratchDwords == 0u, "a dispatch without SCRATCH_EN got scratch"); shader[0x207] = 0x00100040u; Require(AgcDriver::Graphics::DecodeComputeStageInfo(shader, {}).numThreads[0] == 64u, "the partial group size leaked into the group size"); shader[0x207] = 64u; ShaderRecompiler::RecompileRequest request{}; const std::array code{0xbf810000u}; request.shader = {ShaderRecompiler::ShaderStage::Compute, 0x30000u, code, 0, {}}; request.context.waveSize = 64; request.context.compute = compute; const ShaderRecompiler::RequestSerializer serializer; const auto back = serializer.Deserialize(serializer.Serialize(request)); Require(back.request.context.compute->scratchDwords == 24u, "the compute scratch size did not survive serialization"); } void shaderUserDataTailPaddingTests() { constexpr std::size_t userDataOffset = 288; constexpr auto userDataBytes = offsetof(ShaderUserData, sharp_resource_count) + sizeof(ShaderUserData{}.sharp_resource_count); static_assert(userDataBytes == 54); std::vector header(userDataOffset + userDataBytes); Shader shader{}; shader.user_data = reinterpret_cast(header.data() + userDataOffset); std::memcpy(header.data(), &shader, sizeof(shader)); const auto headerAddress = reinterpret_cast(header.data()); const auto info = AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, {}, nullptr, true); Require(!info.fetchEmbedded, "a ShaderUserData block without trailing struct padding was rejected"); } void ColorPipeBankXorTests() { constexpr std::uint32_t side = 128; constexpr std::size_t blockBytes = 65536; static std::vector storage(2 * blockBytes + side * side * 4); const auto block = (reinterpret_cast(storage.data()) + blockBytes - 1) / blockBytes * blockBytes; auto queue = makeState(); queue.context[0x3b8] |= static_cast(AgcDriver::Graphics::ColorTileMode::RenderTarget) << 14u; queue.context[0x3b0] = ((side - 1u) << 14u) | (side - 1u); for (const auto offset : {0xdu, 0x82u, 0x91u, 0x95u}) queue.context[offset] = (side << 16u) | side; for (const std::uint32_t pipeBankXor : {0u, 0x5600u, 0xff00u}) { const auto address = block + pipeBankXor; queue.context[0x318] = static_cast(address >> 8u); queue.context[0x390] = static_cast(address >> 40u); const auto color = AgcDriver::Graphics::DecodeState(queue).color; Require(color.address == block && color.surfaceAddress == block && color.pipeBankXor == pipeBankXor, "a SW_64KB_R_X color base did not split into its block base and pipe/bank XOR " + std::to_string(pipeBankXor)); } queue.context[0x3b8] = 0x9000000u | (0x16u << 14u) | (0x14u << 19u); for (const std::uint32_t pipeBankXor : {0u, 0xa00u, 0xf00u}) { const auto address = block + 0x3000u + pipeBankXor; queue.context[0x318] = static_cast(address >> 8u); queue.context[0x390] = static_cast(address >> 40u); const auto color = AgcDriver::Graphics::DecodeState(queue).color; Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::D4KBX && color.address == block + 0x3000u && color.pipeBankXor == pipeBankXor, "a SW_4KB_D_X color base did not split into its 4 KiB block base and pipe/bank XOR " + std::to_string(pipeBankXor)); } } void HiddenUserWordVertexTablesTests() { constexpr std::size_t userDataOffset = 288; constexpr std::size_t directOffset = 352; constexpr std::size_t semanticOffset = 384; static std::vector header(semanticOffset + sizeof(ShaderSemantic)); static std::array bufferTable{0x1000u, 0x20000u, 64u, 0x3000u}; static std::array attribTable{(0x2cu << 5u)}; std::array direct{}; direct.fill(ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET); direct[static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexBufferTable)] = 4; direct[static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexAttribDescTable)] = 6; std::memcpy(header.data() + directOffset, direct.data(), sizeof(direct)); ShaderSemantic semantic{}; semantic.size_in_elements = 1; std::memcpy(header.data() + semanticOffset, &semantic, sizeof(semantic)); ShaderUserData userData{}; userData.direct_resource_offset = reinterpret_cast(header.data() + directOffset); userData.direct_resource_count = ShaderRegs::AGC_DIRECT_RESOURCE_TYPE_COUNT; std::memcpy(header.data() + userDataOffset, &userData, sizeof(userData)); Shader shader{}; shader.user_data = reinterpret_cast(header.data() + userDataOffset); shader.input_semantics = reinterpret_cast(header.data() + semanticOffset); shader.num_input_semantics = 1; std::memcpy(header.data(), &shader, sizeof(shader)); const auto headerAddress = reinterpret_cast(header.data()); const auto low = [](const void* pointer) { return static_cast(reinterpret_cast(pointer)); }; const auto high = [](const void* pointer) { return static_cast(reinterpret_cast(pointer) >> 32u); }; const std::array registers{0x31016facu, 0x7efff390u, 0x100002u, 0x49u, low(bufferTable.data()), high(bufferTable.data()), low(attribTable.data()), high(attribTable.data())}; std::vector merged(8, 0u); merged.insert(merged.end(), registers.begin(), registers.end()); const auto info = AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, merged, nullptr, false, 8); Require(info.fetchEmbedded && info.fetchBufferReg == 4u && info.fetchAttribReg == 6u && info.resourcesNum == 1 && info.resources[0].fields[0] == 0x1000u && info.resources[0].fields[2] == 64u, "a merged program's vertex tables were not read past its eight hidden user words"); expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, merged, nullptr, false, 0)); }, "null vertex attribute table"); const auto plain = AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, registers, nullptr, false, 0); Require(plain.fetchEmbedded && plain.fetchBufferReg == 4u && plain.resourcesNum == 1 && plain.resources[0].fields[0] == 0x1000u, "a vertex program's tables moved"); } void HeaderlessVertexStageTests() { const std::array userData{1, 2, 3, 4}; const auto info = AgcDriver::Graphics::DecodeVertexStageInfo({}, 0, userData); Require(!info.fetchEmbedded && info.resourcesNum == 0, "a vertex program without an AGC header received an embedded fetch table"); const std::array truncated{}; expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeVertexStageInfo(truncated, 0x1000, userData)); }, "smaller than the fixed AGC header"); } void UnexportedPixelInputTests() { ShaderRecompiler::ShaderPixelStageInfo pixel{}; pixel.interpolatorCount = 5; pixel.interpolatorSettings[0] = 0x000u; pixel.interpolatorSettings[1] = 0x401u; pixel.interpolatorSettings[2] = 0x302u; pixel.interpolatorSettings[3] = 0x123u; pixel.interpolatorSettings[4] = 0x680004u; pixel.interpolatorSettings[5] = 0x005u; const std::array exported{0u}; const auto adjusted = AgcDriver::Graphics::DefaultUnexportedInputs(pixel, exported); Require(adjusted.interpolatorSettings[0] == 0x000u, "an input of an exported parameter was changed"); Require(adjusted.interpolatorSettings[1] == 0x021u, "a flat input of an unexported parameter did not read zero"); Require(adjusted.interpolatorSettings[2] == 0x022u, "an input of an unexported parameter did not read zero"); Require(adjusted.interpolatorSettings[3] == 0x123u, "an input that already reads its default value was changed"); Require(adjusted.interpolatorSettings[4] == 0x180024u, "an FP16 input of an unexported parameter did not read zero in both halves"); Require(adjusted.interpolatorSettings[5] == 0x005u, "a setting past SPI_PS_IN_CONTROL's input count was changed"); const std::array all{0u, 1u, 2u, 4u, 5u}; const auto unchanged = AgcDriver::Graphics::DefaultUnexportedInputs(pixel, all); Require(std::ranges::equal(unchanged.interpolatorSettings, pixel.interpolatorSettings), "inputs of exported parameters were changed"); } void PixelInputLayoutTests() { using ShaderRecompiler::PixelInput; using ShaderRecompiler::PixelInputVgpr; auto queue = makeState(); const auto decode = [&](std::uint32_t ena, std::uint32_t addr) { queue.context[0x1b3] = ena; queue.context[0x1b4] = addr; return AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); }; auto pixel = decode(0x326u, 0x326u); Require(pixel.inputAddr == 0x326u && pixel.hasPerspectiveCenterVgpr && pixel.perspectiveCentroid && pixel.noPerspective && !pixel.linearCentroid && pixel.posX && pixel.posY && !pixel.posZ, "the centroid input flags were not decoded"); Require(PixelInputVgpr(pixel.inputAddr, PixelInput::PerspectiveCenter) == 0u && PixelInputVgpr(pixel.inputAddr, PixelInput::PerspectiveCentroid) == 2u && PixelInputVgpr(pixel.inputAddr, PixelInput::LinearCenter) == 4u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionX) == 6u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionY) == 7u, "the centroid layout moved the inputs"); pixel = decode(0x1146u, 0x1146u); Require(pixel.linearCentroid && !pixel.noPerspective && PixelInputVgpr(pixel.inputAddr, PixelInput::LinearCentroid) == 4u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionX) == 6u && PixelInputVgpr(pixel.inputAddr, PixelInput::FrontFace) == 7u, "the linear centroid layout moved the inputs"); pixel = decode(0x506u, 0x7afu); Require(pixel.inputAddr == 0x7afu && !pixel.posY && pixel.posZ && PixelInputVgpr(pixel.inputAddr, PixelInput::PerspectiveCentroid) == 4u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionX) == 12u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionZ) == 14u, "ADDR-only inputs did not reserve their VGPRs"); for (const auto bit : {0x8u, 0x4000u}) { expectFailure([&] { static_cast(decode(0x2u | bit, 0x2u | bit)); }, "unsupported SPI_PS_INPUT_ENA/ADDR"); } pixel = decode(0x80u, 0x80u); Require(pixel.inputAddr == 0x80u && PixelInputVgpr(pixel.inputAddr, PixelInput::LineStipple) == 0u, "a pixel shader with only LINE_STIPPLE_TEX enabled was rejected"); pixel = decode(0x8082u, 0x8082u); Require(PixelInputVgpr(pixel.inputAddr, PixelInput::LineStipple) == 2u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionFixedPoint) == 3u, "LINE_STIPPLE_TEX and POS_FIXED_PT did not take their VGPRs after the perspective center"); Require(readsBuiltin(pixelBuiltinsRead(0x8082u, 0x8082u, 3u), spv::BuiltInFragCoord), "POS_FIXED_PT is not read from the fragment coordinate"); Require(!readsBuiltin(pixelBuiltinsRead(0x82u, 0x82u, 2u), spv::BuiltInFragCoord), "LINE_STIPPLE_TEX read the fragment coordinate"); pixel = decode(0x546u, 0x7c7u); { ShaderRecompiler::RecompileRequest request{}; const std::array code{0xbf810000u}; request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; request.context.waveSize = 64; request.context.pixel = pixel; const ShaderRecompiler::RequestSerializer serializer; const auto back = serializer.Deserialize(serializer.Serialize(request)); const auto& p = *back.request.context.pixel; Require(p.inputAddr == 0x7c7u && p.hasPerspectiveCenterVgpr && p.perspectiveCentroid && p.linearCentroid && !p.noPerspective && p.posX && !p.posY && p.posZ, "the pixel input layout did not survive serialization"); } for (const auto source : {0u, 2u, 3u}) { const auto read = pixelBuiltinsRead(0x106u, 0x106u, source); Require(readsBuiltin(read, spv::BuiltInBaryCoordKHR) && !readsBuiltin(read, spv::BuiltInFragCoord), "a centroid-layout I/J VGPR does not hold the barycentrics: v" + std::to_string(source)); } const auto noPerspective = pixelNoPerspectiveLocations(false); Require(noPerspective.size() == 1 && noPerspective[0] == 1u, "only the parameter interpolated through the linear pair must be NoPerspective"); Require(pixelNoPerspectiveLocations(true).empty(), "explicit interpolation must not interpolate parameter arrays a second time"); auto read = pixelBuiltinsRead(0x106u, 0x106u, 4u); Require(readsBuiltin(read, spv::BuiltInFragCoord) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "POS_X is not in v4 after the center and centroid pairs"); read = pixelBuiltinsRead(0x326u, 0x326u, 5u); Require(readsBuiltin(read, spv::BuiltInBaryCoordNoPerspKHR) && !readsBuiltin(read, spv::BuiltInFragCoord), "LINEAR_CENTER's J is not v5"); read = pixelBuiltinsRead(0x326u, 0x326u, 6u); Require(readsBuiltin(read, spv::BuiltInFragCoord) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR) && !readsBuiltin(read, spv::BuiltInBaryCoordNoPerspKHR), "POS_X is not in v6 after three I/J pairs"); read = pixelBuiltinsRead(0x102u, 0x106u, 4u); Require(readsBuiltin(read, spv::BuiltInFragCoord), "an ADDR-only centroid pair did not reserve v2/v3"); read = pixelBuiltinsRead(0x102u, 0x106u, 2u); Require(!readsBuiltin(read, spv::BuiltInFragCoord) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "an ADDR-only centroid pair was loaded"); } void OpaqueDestinationAlphaTests() { auto queue = makeState(); for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0; queue.context[0x1e0] = (1u << 30u) | (1u << 29u) | 6u | (7u << 8u) | (8u << 16u) | (10u << 24u); auto blend = AgcDriver::Graphics::DecodeState(queue).blend; Require(blend.srcColorBlendFactor == VK_BLEND_FACTOR_DST_ALPHA && blend.dstColorBlendFactor == VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA, "color blend factors changed"); Require(blend.srcAlphaBlendFactor == VK_BLEND_FACTOR_DST_COLOR && blend.dstAlphaBlendFactor == VK_BLEND_FACTOR_SRC_ALPHA_SATURATE, "alpha blend factors changed"); queue.context[0x31d] = 0x20000u; blend = AgcDriver::Graphics::DecodeState(queue).blend; Require(blend.srcColorBlendFactor == VK_BLEND_FACTOR_ONE && blend.dstColorBlendFactor == VK_BLEND_FACTOR_ZERO, "an opaque destination left a destination-alpha color factor"); Require(blend.srcAlphaBlendFactor == VK_BLEND_FACTOR_ONE && blend.dstAlphaBlendFactor == VK_BLEND_FACTOR_SRC_ALPHA_SATURATE, "an opaque destination left a destination alpha factor"); queue.context[0x1e0] = (1u << 30u) | 10u | (9u << 8u); blend = AgcDriver::Graphics::DecodeState(queue).blend; Require(blend.srcColorBlendFactor == VK_BLEND_FACTOR_ZERO && blend.dstColorBlendFactor == VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR, "alpha saturation with an opaque destination"); Require(blend.dstAlphaBlendFactor == VK_BLEND_FACTOR_ZERO, "an opaque destination left a destination-color alpha factor"); queue.context[0x31d] = 0x40000u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "destination alpha override"); } 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"); queue = makeState(); queue.context[0x31c] = 0; for (const auto offset : {0x31bu, 0x31du, 0x3b0u, 0x3b8u, 0x390u, 0x318u, 0x1e0u}) queue.context.erase(offset); const auto disabled = AgcDriver::Graphics::DecodeState(queue); Require(!disabled.hasColorTarget && disabled.colors.empty() && disabled.blends.empty(), "COLOR_INVALID retained an attachment despite the disabled buffer"); Require(disabled.renderExtent.width == 64 && disabled.renderExtent.height == 4, "COLOR_INVALID lost the attachment-free render extent"); queue.shader[0x008] = 0; queue.shader[0x009] = 0; Require(AgcDriver::Graphics::NullPixelProgramRejection(queue).empty(), "COLOR_INVALID rejected a draw without a pixel shader"); queue.context[0x200] = 0x36; queue.context[0x000] = 0; queue.context[0x002] = 0; queue.context[0x010] = 0x80000181; queue.context[0x011] = 0x20000180; queue.context[0x012] = 0x100; queue.context[0x014] = 0x100; queue.context[0x007] = 0x003f003f; queue.context[0x00a] = 0; queue.context[0x00b] = std::bit_cast(1.0f); const auto depthOnly = AgcDriver::Graphics::DecodeState(queue); Require(!depthOnly.hasColorTarget && depthOnly.depth && depthOnly.depthTest && depthOnly.depthWrite && depthOnly.renderExtent.height == 64, "COLOR_INVALID discarded a depth-only draw"); for (const auto colorControl : {0x0u, 0xcc0000u}) { queue = makeState(); queue.context[0x202] = colorControl; const auto unwritten = AgcDriver::Graphics::DecodeState(queue); Require(!unwritten.hasColorTarget && unwritten.colors.empty() && unwritten.color.address == 0, "CB_COLOR_CONTROL mode disable kept a color attachment"); Require(unwritten.renderExtent.width == 64 && unwritten.renderExtent.height == 4, "CB_COLOR_CONTROL mode disable lost the screen scissor extent"); } } void TuningFieldTests() { auto queue = makeState(); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; const auto baseline = AgcDriver::Graphics::DecodeState(queue); queue.context[0x292] = 0x22; auto state = AgcDriver::Graphics::DecodeState(queue); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "ALTERNATE_RBS_PER_TILE was rejected"); Require(state.scissor.offset.x == baseline.scissor.offset.x && state.scissor.extent.width == baseline.scissor.extent.width && state.scissor.extent.height == baseline.scissor.extent.height, "ALTERNATE_RBS_PER_TILE changed the scissor"); queue.context[0x292] = 0x26; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "scan conversion mode"); queue.context[0x292] = 2; queue.context[0x202] = 0xcc0011; state = AgcDriver::Graphics::DecodeState(queue); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "DISABLE_DUAL_QUAD was rejected"); Require(state.hasColorTarget && state.blend.colorWriteMask == baseline.blend.colorWriteMask, "DISABLE_DUAL_QUAD changed color output"); queue.context[0x202] = 0xcc0013; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "copy ROP"); queue.context[0x202] = 0xcc0010; for (const auto groups : {1u, 2u, 15u}) { queue.context[0x2d5] = 0x2000u | (groups << 15u); state = AgcDriver::Graphics::DecodeState(queue); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "MAX_PRIMGRP_IN_WAVE was rejected"); Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Vertex && state.stages.vertexWaveSize == 64u, "MAX_PRIMGRP_IN_WAVE changed vertex routing"); } } void ShadersWriteMemoryTests() { const auto binding = [](ShaderRecompiler::DescriptorKind kind, ShaderRecompiler::DescriptorRole role, std::vector written) { ShaderRecompiler::DescriptorBinding result{}; result.kind = kind; result.role = role; result.count = 1; if (role == ShaderRecompiler::DescriptorRole::GuestImages) result.imageWritten = std::move(written); else result.bufferWritten = std::move(written); return result; }; ShaderRecompiler::RecompileResult reads; reads.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::GuestBuffers, {false})); reads.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageImage, ShaderRecompiler::DescriptorRole::GuestImages, {false})); ShaderRecompiler::RecompileResult stores; stores.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::GuestBuffers, {true})); using Stage = ShaderRecompiler::ShaderStage; const auto writes = [](std::initializer_list shaders) { return AgcDriver::Graphics::ShadersWriteMemory(std::vector(shaders)); }; Require(!writes({{Stage::Vertex, &reads, 0}, {Stage::Fragment, &reads, 0}}), "shaders that only read memory were reported as writing it"); Require(writes({{Stage::Vertex, &reads, 0}, {Stage::Fragment, &stores, 0}}), "a pixel shader that stores to a buffer was not reported"); Require(writes({{Stage::Vertex, &stores, 0}, {Stage::Fragment, &reads, 0}}), "a vertex shader that stores to a buffer was not reported"); Require(writes({{Stage::Geometry, &stores, 0}, {Stage::Fragment, &reads, 0}}), "a geometry shader that stores to a buffer was not reported"); } void ReversedComponentOrderTests() { { auto queue = makeState(); queue.context[0x31c] = (queue.context[0x31c] & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (3u << 2u) | (1u << 11u); const auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.colors.size() == 1 && state.colors[0].format == VK_FORMAT_R8G8_UNORM && state.colors[0].componentMapping == 0xecu, "an 8_8 target with SWAP_ALT did not store the x and w exports in its two channels"); queue.context[0x1e0] = 0x40010001u; for (const auto offset : {0x105u, 0x106u, 0x107u, 0x108u}) queue.context[offset] = 0; const auto blended = AgcDriver::Graphics::DecodeState(queue); Require(blended.blends.size() == 1 && blended.blends[0].blendEnable, "blending into an 8_8 SWAP_ALT target with one equation for color and alpha was rejected"); Require(!AgcDriver::Graphics::SplitsAlternateAlpha(blended), "one blend equation for color and alpha split an 8_8 SWAP_ALT draw"); queue.context[0x1e0] = 0x60000001u; Require(AgcDriver::Graphics::SplitsAlternateAlpha(AgcDriver::Graphics::DecodeState(queue)), "a separate alpha blend into an 8_8 SWAP_ALT target did not split its second channel"); queue.context[0x1e0] = (1u << 30u) | 13u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "constant color factor"); queue.context[0x1e0] = (1u << 30u) | (1u << 29u) | 1u | (1u << 16u) | (14u << 24u); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "constant color factor"); queue.context[0x1e0] = (1u << 30u) | (2u << 5u) | 13u; Require(AgcDriver::Graphics::DecodeState(queue).blends[0].colorBlendOp == VK_BLEND_OP_MIN, "a constant color factor of a MIN blend into an 8_8 SWAP_ALT target was rejected"); } for (const auto& [swap, mapping] : {std::pair{2u, 0x1bu}, std::pair{3u, 0x93u}}) { auto queue = makeState(); queue.context[0x31c] = (queue.context[0x31c] & ~(3u << 11u)) | (swap << 11u); const auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.colors.size() == 1 && state.colors[0].format == VK_FORMAT_R8G8B8A8_UNORM && state.colors[0].componentMapping == mapping, "an 8_8_8_8 target with a reversed component order did not map exports onto RGBA8"); Require(AgcDriver::Graphics::ExportMappings(state)[0] == mapping && state.blends[0].colorWriteMask == 0xfu, "a reversed 8_8_8_8 target did not write all four channels through its export mapping"); queue.context[0x1e0] = 0x40010001u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "reversed component order"); queue.context[0x1e0] = 0; queue.context[0x31c] = (queue.context[0x31c] & ~((0x1fu << 2u) | (7u << 8u))) | (12u << 2u) | (7u << 8u); const auto wide = AgcDriver::Graphics::DecodeState(queue); Require(wide.colors.size() == 1 && wide.colors[0].format == VK_FORMAT_R16G16B16A16_SFLOAT && wide.colors[0].componentMapping == mapping && wide.blends[0].colorWriteMask == 0xfu, "a 16_16_16_16 float target with a reversed component order did not map exports onto RGBA16"); } } void Color1555TargetTests() { auto queue = makeState(); queue.context[0x31c] = (queue.context[0x31c] & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (17u << 2u); const auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.colors.size() == 1 && state.colors[0].format == VK_FORMAT_A1R5G5B5_UNORM_PACK16 && state.colors[0].elementBytes == 2 && state.colors[0].componentMapping == 0xc6u, "a 1_5_5_5 target did not render through A1R5G5B5 with X and Z exchanged"); Require(AgcDriver::Graphics::ExportMappings(state)[0] == 0xc6u && state.blends[0].colorWriteMask == 0xfu, "a 1_5_5_5 target did not write all four channels through its export mapping"); queue.context[0x8e] = 0x1u; Require(AgcDriver::Graphics::DecodeState(queue).blends[0].colorWriteMask == VK_COLOR_COMPONENT_B_BIT, "a 1_5_5_5 target writing only X did not write the A1R5G5B5 channel in its low bits"); queue.context[0x8e] = 0xfu; for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = 0; queue.context[0x1e0] = 0x40000504u; const auto blended = AgcDriver::Graphics::DecodeState(queue); Require(blended.blends[0].blendEnable && blended.blends[0].srcColorBlendFactor == VK_BLEND_FACTOR_SRC_ALPHA && blended.blends[0].dstColorBlendFactor == VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, "alpha blending into a 1_5_5_5 target was not decoded"); queue.context[0x1e0] = 0; for (const auto& [number, swap] : {std::pair{1u, 0u}, std::pair{4u, 0u}, std::pair{0u, 1u}}) { queue.context[0x31c] = (queue.context[0x31c] & ~((7u << 8u) | (3u << 11u))) | (number << 8u) | (swap << 11u); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color format 17 number type " + std::to_string(number) + " component swap " + std::to_string(swap)); } } void CompactedExportTests() { alignas(256) static std::array slotFourMemory{}; const auto slotFour = reinterpret_cast(slotFourMemory.data()); auto queue = makeState(); for (const auto offset : {0x31bu, 0x31cu, 0x31du}) queue.context[offset + 4u * 0xfu] = queue.context.at(offset); for (const auto offset : {0x3b0u, 0x3b8u}) queue.context[offset + 4u] = queue.context.at(offset); queue.context[0x318 + 4u * 0xfu] = static_cast(slotFour >> 8u); queue.context[0x390 + 4u] = static_cast(slotFour >> 40u); queue.context[0x1e4] = 0x40010001u; for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = 0; queue.context[0x8e] = 0x3000fu; queue.context[0x8f] = 0xf000fu; queue.context[0x1c5] = 0x44u; auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.colors.size() == 2 && state.blends.size() == 2, "two compacted exports did not give two attachments"); Require(state.colors[0].slot == 0 && state.colors[0].exportIndex == 0 && state.colors[0].address == reinterpret_cast(colorMemory.data()), "export 0 did not reach MRT slot 0"); Require(state.colors[1].slot == 4 && state.colors[1].exportIndex == 1 && state.colors[1].address == slotFour, "export 1 did not reach MRT slot 4, the second slot CB_SHADER_MASK enables"); Require(!state.blends[0].blendEnable && state.blends[1].blendEnable && state.blends[1].colorWriteMask == (VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT), "export 1 did not take MRT slot 4's blend control and target mask"); Require(AgcDriver::Graphics::ExportMappings(state)[1] == state.colors[1].componentMapping, "export 1 did not take MRT slot 4's component mapping"); auto disabledFirst = queue; disabledFirst.context[0x31c] = 0; const auto withHole = AgcDriver::Graphics::DecodeState(disabledFirst); Require(withHole.colors.size() == 1 && withHole.colors[0].slot == 4 && withHole.colors[0].exportIndex == 1 && withHole.blends.size() == 2 && withHole.blends[0].colorWriteMask == 0, "COLOR_INVALID shifted a later MRT export"); queue.context[0x1c5] = 0x90009u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 0"); queue.context[0x8e] = 0xf000fu; queue.context[0x8f] = 0xf0f0fu; queue.context[0x1c5] = 0x999u; state = AgcDriver::Graphics::DecodeState(queue); Require(state.colors.size() == 2 && state.blends.size() == 3, "a write-masked export between written ones changed the attachment count"); Require(state.colors[0].exportIndex == 0 && state.colors[1].slot == 4 && state.colors[1].exportIndex == 2, "export 2 did not reach MRT slot 4, the third slot CB_SHADER_MASK enables"); Require(state.blends[1].colorWriteMask == 0 && !state.blends[1].blendEnable && state.blends[2].blendEnable, "the write-masked export 1 was not left unused"); } void DepthStencilTests() { auto queue = makeState(); queue.context[0x000] = 0; queue.context[0x002] = 0; queue.context[0x007] = (1u << 16u) | 3u; queue.context[0x00a] = 7; queue.context[0x00b] = 0; queue.context[0x010] = 0x22900983; queue.context[0x011] = 0x20000181; for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; queue.context[0x005] = 0x300; queue.context[0x01e] = 0x1; queue.context[0x10b] = 0x00050050; queue.context[0x10c] = 0x01ffff01; queue.context[0x10d] = 0x01000001; queue.context[0x200] = 0x00700711; std::vector log; AgcDriver::Graphics::RegisterReadLog() = &log; auto state = AgcDriver::Graphics::DecodeState(queue); AgcDriver::Graphics::RegisterReadLog() = nullptr; for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a depth register the decoder reads: " + std::to_string(read.offset)); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); Require(rejection.empty(), "precheck rejected a stencil draw with a surface: " + rejection); Require(state.depth && state.depth->address == 0x10000 && state.depth->stencilAddress == 0x20000 && state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT && state.depth->clearStencil == 7 && state.depth->htileAddress == 0x10000030000ull && !state.depth->htileStencil, "depth surface decode changed"); { queue.context[0x011] &= ~(1u << 29u); const auto stencilTiled = AgcDriver::Graphics::DecodeState(queue); Require(stencilTiled.depth && stencilTiled.depth->htileStencil, "HTILE holds the stencil state without TILE_STENCIL_DISABLE"); queue.context[0x011] |= 1u << 29u; queue.context[0x010] &= ~(1u << 29u); const auto untiled = AgcDriver::Graphics::DecodeState(queue); Require(untiled.depth && untiled.depth->htileAddress == 0, "HTILE must be ignored without TILE_SURFACE_ENABLE"); queue.context[0x010] |= 1u << 29u; using AgcDriver::Graphics::HtileFillClears; using AgcDriver::Graphics::HtileFillCovers; constexpr VkImageAspectFlags both = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; Require(HtileFillClears(0x0003fff0u, false) == VK_IMAGE_ASPECT_DEPTH_BIT, "a depth-only HTILE fill with ZMask 0 clears the depth"); Require(HtileFillClears(0x000000f0u, true) == both, "a depth and stencil fast clear (ZMask 0, SMem 0) clears both aspects"); Require(HtileFillClears(0x000003f0u, true) == VK_IMAGE_ASPECT_DEPTH_BIT, "SMem 3 leaves the stencil uncleared"); Require(HtileFillClears(0xfffff0ffu, true) == VK_IMAGE_ASPECT_STENCIL_BIT, "ZMask 0xf with SMem 0 clears only the stencil"); Require(HtileFillClears(0xffffffffu, true) == 0 && HtileFillClears(0xffffffffu, false) == 0, "an expanded fill clears nothing"); const VkExtent2D extent{16, 8}; Require(HtileFillCovers(0x10000, extent, 0x10000, 0x8000) && HtileFillCovers(0x10000, extent, 0xfff0, 0x8010), "a fill over the whole 32 KiB metadata block covers the surface"); Require(!HtileFillCovers(0x10000, extent, 0x10000, 0x7ffc) && !HtileFillCovers(0x10000, extent, 0x10004, 0x8000) && !HtileFillCovers(0, extent, 0, 0x10000), "a partial fill, one starting past the HTILE base, or no HTILE covers nothing"); // Pipe-aligned SW_64KB_Z_X places a 16x16 surface's HTILE words across its metadata block (a valid word at offset // 260, AMD PAL), so a fill of the words a linear layout would hold covers nothing. Require(!HtileFillCovers(0x10000, {16, 16}, 0x10000, 16) && !HtileFillCovers(0x10000, {16, 16}, 0x10000, 264), "a fill of the linear HTILE size of a 16x16 surface misses its pipe-aligned words"); Require(AgcDriver::Graphics::HtileMetadataBytes({1024, 512}) == 0x8000u && AgcDriver::Graphics::HtileMetadataBytes({1025, 512}) == 0x10000u && AgcDriver::Graphics::HtileMetadataBytes({3840, 2160}) == 20u * 0x8000u, "the HTILE metadata is 32 KiB per 1024x512 block"); } Require(state.renderExtent.width == 4 && state.renderExtent.height == 2, "render extent ignores the depth surface"); Require(!state.depthTest && !state.depthWrite && state.stencilTest, "depth/stencil enables changed"); queue.context.erase(0x005); Require(AgcDriver::Graphics::DecodeState(queue).depth->htileAddress == 0, "an HTILE surface without DB_HTILE_DATA_BASE decoded an HTILE address"); queue.context[0x005] = 0x300; queue.context[0x01e] = 0x01; log.clear(); AgcDriver::Graphics::RegisterReadLog() = &log; state = AgcDriver::Graphics::DecodeState(queue); AgcDriver::Graphics::RegisterReadLog() = nullptr; for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks an HTILE register the decoder reads: " + std::to_string(read.offset)); Require(state.depth->htileAddress == (0x30000ull | (1ull << 40u)), "the HTILE address was not decoded from DB_HTILE_DATA_BASE and its high byte"); queue.context[0x010] &= ~0x20000000u; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depth->htileAddress == 0, "a surface without TILE_SURFACE_ENABLE decoded an HTILE address"); queue.context[0x010] |= 0x20000000u; const auto& front = state.stencilFront; Require(front.compareOp == VK_COMPARE_OP_ALWAYS && front.passOp == VK_STENCIL_OP_INCREMENT_AND_CLAMP && front.failOp == VK_STENCIL_OP_KEEP && front.reference == 1 && front.writeMask == 0xff, "stencil mask pass changed"); Require(std::memcmp(&state.stencilBack, &front, sizeof(front)) == 0, "back faces without BACKFACE_ENABLE must use the front state"); queue.context[0x10b] = 0; queue.context[0x10c] = 0x01ffff02; queue.context[0x200] = 0x00200211; state = AgcDriver::Graphics::DecodeState(queue); Require(state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL && state.stencilFront.reference == 2 && state.stencilFront.passOp == VK_STENCIL_OP_KEEP, "stencil content pass changed"); queue.context[0x10b] = 0x00030030; queue.context[0x10c] = 0x01ffff00; queue.context[0x200] = 0x00700771; state = AgcDriver::Graphics::DecodeState(queue); Require(state.stencilFront.passOp == VK_STENCIL_OP_REPLACE && state.stencilFront.reference == 0 && std::memcmp(&state.stencilBack, &state.stencilFront, sizeof(state.stencilFront)) == 0, "stencil clear pass changed"); queue.context[0x200] = 0x007007f1; state = AgcDriver::Graphics::DecodeState(queue); Require(state.stencilBack.compareOp == VK_COMPARE_OP_ALWAYS && state.stencilBack.passOp == VK_STENCIL_OP_KEEP && state.stencilBack.writeMask == 0 && state.stencilBack.reference == 1, "back-face stencil state changed"); queue.context[0x10b] = 0x40; queue.context[0x10c] = 0x05ffff02; queue.context[0x200] = 0x00200211; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement"); queue.context[0x10b] = 0x50; queue.context[0x200] = 0x00700711; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil add/subtract"); queue.context[0x10b] = 0; queue.context[0x000] = 1; queue.context[0x00b] = std::bit_cast(0.25f); state = AgcDriver::Graphics::DecodeState(queue); Require(state.depth && state.depthTest && state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS && !state.depthBoundsTest && !state.depthBias && state.viewport.minDepth == 0.25f && state.viewport.maxDepth == 0.25f, "a DEPTH_CLEAR_ENABLE draw does not store DB_DEPTH_CLEAR everywhere it covers"); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected a depth clear"); queue.context[0x002] = 0x01000000; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "writable depth plane"); Require(AgcDriver::Graphics::DrawRejection(queue, false).find("writable depth plane") != std::string::npos, "precheck accepted read-only depth clear"); queue.context[0x002] = 0; queue.context[0x00b] = 0; queue.context[0x000] = 0x22; state = AgcDriver::Graphics::DecodeState(queue); const auto clears = [](const VkStencilOpState& face) { return face.compareOp == VK_COMPARE_OP_ALWAYS && face.passOp == VK_STENCIL_OP_REPLACE && face.failOp == VK_STENCIL_OP_REPLACE && face.depthFailOp == VK_STENCIL_OP_REPLACE && face.writeMask == 0xff && face.reference == 7; }; Require(state.stencilTest && clears(state.stencilFront) && clears(state.stencilBack), "a STENCIL_CLEAR_ENABLE draw does not store DB_STENCIL_CLEAR"); queue.context[0x200] = 0; state = AgcDriver::Graphics::DecodeState(queue); Require(state.stencilTest && clears(state.stencilFront), "a STENCIL_CLEAR_ENABLE draw without a stencil test does not store DB_STENCIL_CLEAR"); queue.context[0x002] = 0x02000000; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "writable stencil plane"); Require(AgcDriver::Graphics::DrawRejection(queue, false).find("writable stencil plane") != std::string::npos, "precheck accepted read-only stencil clear"); queue.context[0x002] = 0; for (const auto offset : {0x31bu, 0x31cu, 0x31du}) queue.context[offset + 0xfu] = queue.context.at(offset); for (const auto offset : {0x3b0u, 0x3b8u}) queue.context[offset + 1u] = queue.context.at(offset); const auto secondColor = reinterpret_cast(sliceMemory.data()); queue.context[0x327] = static_cast(secondColor >> 8u); queue.context[0x391] = static_cast(secondColor >> 40u); queue.context[0x1e1] = 0; queue.context[0x8e] = 0xf3; queue.context[0x8f] = 0xff; queue.context[0x1c5] = 0x99; queue.context[0x90] = 0x80000001; queue.context[0x91] = 0x00020003; queue.context[0x10b] = 0x00050050; queue.context[0x10c] = 0x05ffff02; queue.context[0x10d] = 0x090000ff; for (const auto control : {0u, 1u, 0x81u}) { queue.context[0x200] = control; state = AgcDriver::Graphics::DecodeState(queue); Require(state.stencilTest && clears(state.stencilFront) && clears(state.stencilBack), "stencil clear used the overridden stencil operations"); Require(state.depth && !state.depthTest && !state.depthWrite, "stencil clear changed depth state"); Require(state.colors.size() == 2 && state.blends.size() == 2 && state.colors[1].address == secondColor && state.blends[0].colorWriteMask == 3 && state.blends[1].colorWriteMask == 0xf, "stencil clear lost MRT color writes"); Require(state.scissor.offset.x == 1 && state.scissor.offset.y == 0 && state.scissor.extent.width == 2 && state.scissor.extent.height == 2, "stencil clear changed scissor coverage"); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected combined color/stencil clear"); } queue.context[0x002] = 0x01000000; state = AgcDriver::Graphics::DecodeState(queue); Require(!state.depthWrite && clears(state.stencilFront), "read-only depth prevented stencil clear"); queue.context[0x002] = 0; queue.context[0x000] = 0x23; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS && clears(state.stencilFront), "a combined depth and stencil clear draw does not clear both planes"); for (const auto control : {4u, 8u}) { queue.context[0x000] = 0x22u | control; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DB_RENDER_CONTROL"); } queue.context[0x000] = 0x22; queue.context[0x011] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "writable stencil plane"); Require(AgcDriver::Graphics::DrawRejection(queue, false).find("writable stencil plane") != std::string::npos, "precheck accepted clear without a stencil plane"); queue.context[0x010] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "writable stencil plane"); queue = makeState(); queue.context[0x31b] = 1u << 26u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "mip exceeds"); queue.context[0x31b] = 1u << 13u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "several array slices"); const auto sliced = reinterpret_cast(sliceMemory.data()); queue.context[0x318] = static_cast(sliced >> 8u); queue.context[0x390] = static_cast(sliced >> 40u); queue.context[0x31b] = 1u | (1u << 13u); const auto slice = AgcDriver::Graphics::DecodeState(queue); Require(slice.color.address == sliced + 1024u && slice.color.bytes == 1024u, "a color view of one slice did not move the target by one slice"); queue.context[0x3b8] = 0x09000001; const auto arraySlice = AgcDriver::Graphics::DecodeState(queue); Require(arraySlice.color.address == sliced + 1024u && arraySlice.color.bytes == 1024u && arraySlice.color.depth == 1u, "a color view of one slice of a 2D array target did not move the target by one slice"); queue.context[0x31b] = 2u | (2u << 13u); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beyond the array surface"); queue.context[0x3b8] = 0x0a000003; queue.context[0x31b] = 2u | (2u << 13u); const auto volume = AgcDriver::Graphics::DecodeState(queue); Require(volume.color.address == sliced && volume.color.depth == 4u && volume.color.depthSlice == 2u, "a color view of one 3D depth slice did not keep the surface address and select the slice"); queue.context[0x31b] = 4u | (4u << 13u); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beyond the 3D surface"); queue.context[0x31b] = 0; queue.context[0x31c] |= 0x10000000; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DCC 3D color targets"); } void OneDimensionalColorTests() { auto queue = makeState(); queue.context[0x3b8] = 0x08000000; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "taller than one row"); queue.context[0x3b0] = 63u << 14u; const auto line = AgcDriver::Graphics::DecodeState(queue); Require(line.color.extent.width == 64u && line.color.extent.height == 1u && line.color.address == reinterpret_cast(colorMemory.data()) && line.color.depth == 1u, "a 1D color target did not decode as one row"); } void htileDepthClearTests() { static constexpr std::array kernel{0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, 0xbf810000u}; const std::array wave{64, 1, 1}; const auto address = [&](std::span code, std::array userData, std::array threads) { return AgcDriver::Graphics::HtileDepthClearAddress(code, userData, threads); }; Require(address(kernel, {0, 0, 0x0dc50000u, 0x40u, 0u, 0x00000ff0u}, wave) == 0x400dc50000ull, "RADV's HTILE depth fast clear (ZMASK to 0) was not recognized"); Require(address(kernel, {0, 0, 0x0dc50000u, 0x40u, 0xfffff00fu, 0x00000ff0u}, wave) == 0, "an HTILE initialization to expanded (ZMASK 0xf) was taken for a clear"); Require(address(kernel, {0, 0, 0x0dc50000u, 0x40u, 0x000003f0u, 0xfffffc0fu}, wave) == 0, "a stencil-only HTILE update that keeps ZMASK was taken for a depth clear"); Require(address(kernel, {0, 0, 0x0dc50000u, 0x40u, 0u, 0x00000ff0u}, {32, 1, 1}) == 0, "the HTILE kernel matched with another group size"); auto other = kernel; other[6] ^= 1u; Require(address(other, {0, 0, 0x0dc50000u, 0x40u, 0u, 0x00000ff0u}, wave) == 0, "a different kernel was taken for RADV's HTILE update"); Require(address(std::span(kernel).first(16), {0, 0, 0x0dc50000u, 0x40u, 0u, 0x00000ff0u}, wave) == 0, "a truncated kernel was taken for RADV's HTILE update"); } void depthMaintenanceTests() { for (const auto mode : {0x4u, 0x8u, 0x80u, 0x100u, 0x1000u, 0x4000u}) { for (const auto clear : {0u, 1u, 2u, 3u}) { auto queue = makeState(); queue.context[0x000] = mode | clear; queue.context[0x200] = 0; queue.context[0x8e] = 0; queue.context[0x8f] = 0; queue.shader.erase(0x8); const auto reason = AgcDriver::Graphics::DepthMaintenanceRejection(queue); Require(reason.find("DB_RENDER_CONTROL") != std::string::npos, "depth maintenance passed without depth tests or color writes"); Require(AgcDriver::Graphics::DrawRejection(queue, false) == reason, "draw precheck did not reject depth maintenance first"); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); } } for (const auto control : {0u, 1u, 2u, 3u, 0x20u, 0x40u, 0x2000u, 0x2063u}) { auto queue = makeState(); queue.context[0x000] = control; Require(AgcDriver::Graphics::DepthMaintenanceRejection(queue).empty(), "ordinary depth controls were mistaken for maintenance"); } for (const auto clear : {0u, 1u, 2u, 3u}) { auto queue = makeState(); queue.context[0x000] = 0x10u | clear; queue.context[0x200] = 0; queue.context[0x8e] = 0; queue.context[0x8f] = 0; Require(AgcDriver::Graphics::DepthMaintenanceRejection(queue).empty(), "a resummarize draw without depth, stencil or color work was rejected"); queue.context[0x200] = 2; Require(AgcDriver::Graphics::DepthMaintenanceRejection(queue).find("DB_RENDER_CONTROL") != std::string::npos, "a resummarize draw with a depth test passed"); queue.context[0x200] = 0; queue.context[0x8e] = 0xf; queue.context[0x8f] = 0xf; Require(AgcDriver::Graphics::DepthMaintenanceRejection(queue).find("DB_RENDER_CONTROL") != std::string::npos, "a resummarize draw with color writes passed"); } auto absent = makeState(); absent.context.erase(0x000); Require(AgcDriver::Graphics::DepthMaintenanceRejection(absent).empty(), "an absent depth control produced a maintenance verdict"); auto readOnly = makeState(); readOnly.context[0x000] = 0x10; readOnly.context[0x200] = 0x007007f3; readOnly.context[0x010] = 0xa0000183; readOnly.context[0x011] = 0x20000180; readOnly.context[0x8e] = 0xffffffff; readOnly.context[0x8f] = 0; readOnly.shader.erase(0x8); readOnly.shader.erase(0x9); Require(AgcDriver::Graphics::DepthMaintenanceRejection(readOnly).empty(), "a resummarize draw with pass-through read-only depth and no stencil plane was rejected"); for (const auto [offset, value] : {std::pair{0x200u, 0x007007f7u}, {0x200u, 0x007007fbu}, {0x200u, 0x007007e3u}, {0x011u, 0x20000181u}, {0x8fu, 0xfu}, {0x000u, 0x11u}, {0x010u, 0xa0000182u}}) { auto changed = readOnly; changed.context[offset] = value; Require(!AgcDriver::Graphics::DepthMaintenanceRejection(changed).empty(), "a resummarize draw with surface work or unsupported depth passed"); } auto pixel = readOnly; pixel.shader[0x8] = 1; pixel.shader[0x9] = 0; pixel.context[0x1c4] = 0; pixel.context[0x203] = 0x40; Require(!AgcDriver::Graphics::DepthMaintenanceRejection(pixel).empty(), "a resummarize draw with a running pixel stage passed"); for (const auto offset : {0x200u, 0x010u, 0x011u, 0x8eu, 0x8fu}) { auto missing = readOnly; missing.context.erase(offset); Require(!AgcDriver::Graphics::DepthMaintenanceRejection(missing).empty(), "a resummarize draw without a required register passed"); } } // SPI_SHADER_Z_FORMAT (0x1c4) and the export enables of DB_SHADER_CONTROL (0x203): Z export needs a // format with a depth channel (1, 2, 3 or 32_ABGR 9), the sample mask needs 32_ABGR, and the // formats the export path does not lay out are refused. A missing 0x1c4 gives no verdict here. void ZExportTests() { constexpr std::uint32_t zExportEnable = 0x1u; constexpr std::uint32_t maskExportEnable = 0x100u; const auto withExports = [](std::uint32_t format, std::uint32_t exports) { auto queue = makeState(); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; queue.context[0x1c4] = format; queue.context[0x203] = 0x800u | exports; return queue; }; for (const std::uint32_t format : {1u, 2u, 3u, 9u}) { const auto queue = withExports(format, zExportEnable); Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a Z export with format " + std::to_string(format) + " was rejected"); } Require(!AgcDriver::Graphics::DrawRejection(withExports(0, zExportEnable), true).empty(), "a Z export without a Z format was accepted"); Require(AgcDriver::Graphics::DrawRejection(withExports(9, zExportEnable | maskExportEnable), true).empty(), "a sample-mask export with 32_ABGR was rejected"); Require(!AgcDriver::Graphics::DrawRejection(withExports(1, zExportEnable | maskExportEnable), true).empty(), "a sample-mask export with 32_R was accepted"); for (std::uint32_t format = 4; format <= 8; ++format) { Require(!AgcDriver::Graphics::DrawRejection(withExports(format, 0), true).empty(), "Z format " + std::to_string(format) + " was accepted"); } auto absent = withExports(0, 0); absent.context.erase(0x1c4); Require(AgcDriver::Graphics::DrawRejection(absent, true).empty(), "a missing SPI_SHADER_Z_FORMAT was rejected (or threw) in the precheck"); } void DepthBoundsBiasTests() { const auto bits = [](float value) { std::uint32_t word = 0; std::memcpy(&word, &value, sizeof(word)); return word; }; auto queue = makeState(); queue.context[0x000] = 0; queue.context[0x002] = 0; queue.context[0x007] = (1u << 16u) | 3u; queue.context[0x00a] = 0; queue.context[0x00b] = 0; queue.context[0x010] = 0x22900983; queue.context[0x011] = 0x20000180; for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; queue.context[0x200] = 0x0000006e; queue.context[0x008] = bits(0.25f); queue.context[0x009] = bits(0.75f); auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthTest && state.depthBoundsTest && state.minDepthBounds == 0.25f && state.maxDepthBounds == 0.75f, "depth bounds decode changed"); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); Require(rejection.empty(), "precheck rejected depth bounds with a depth surface: " + rejection); queue.context[0x205] = 0x00001a48u; queue.context[0x2df] = bits(0.5f); for (const auto offset : {0x2e0u, 0x2e2u}) queue.context[offset] = bits(32.0f); for (const auto offset : {0x2e1u, 0x2e3u}) queue.context[offset] = bits(4.0f); state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthBias && state.depthBiasSlope == 2.0f && state.depthBiasConstant == 4.0f && state.depthBiasClamp == 0.5f, "depth bias decode changed"); Require(!state.depthBiasPerFace, "equal front and back depth bias must draw both faces in one pass"); queue.context[0x2e2] = bits(-64.0f); queue.context[0x2e3] = bits(8.0f); state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthBias && state.depthBiasPerFace && state.cullMode == VK_CULL_MODE_NONE, "differing front and back depth bias must draw each face with its own bias"); Require(state.depthBiasSlope == 2.0f && state.depthBiasConstant == 4.0f && state.backDepthBiasSlope == -4.0f && state.backDepthBiasConstant == 8.0f && state.depthBiasClamp == 0.5f, "per-face depth bias decode changed"); queue.context[0x205] = 0x00000a48u; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthBias && state.depthBiasPerFace && state.depthBiasConstant == 4.0f && state.backDepthBiasSlope == 0.0f && state.backDepthBiasConstant == 0.0f, "back faces without depth bias must draw unbiased"); queue.context[0x205] = 0x00001248u; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthBias && state.depthBiasPerFace && state.depthBiasSlope == 0.0f && state.depthBiasConstant == 0.0f && state.backDepthBiasConstant == 8.0f, "front faces without depth bias must draw unbiased"); queue.context[0x205] = 0x00001a4au; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthBias && !state.depthBiasPerFace && state.depthBiasConstant == 4.0f, "culled back faces must not constrain the front depth bias"); queue.context[0x205] = 0x00001a49u; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthBias && !state.depthBiasPerFace && state.depthBiasSlope == -4.0f && state.depthBiasConstant == 8.0f, "culled front faces must draw with the back depth bias"); queue.context[0x205] = 0x00003a46u; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthBias && state.depthBiasConstant == 4.0f && state.cullMode == VK_CULL_MODE_BACK_BIT, "a triangle draw with the point and line offset enable lost its depth bias"); queue.context[0x205] = 0x00013a46u; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthBias && state.depthBiasConstant == 4.0f && state.cullMode == VK_CULL_MODE_BACK_BIT, "the window offset enable bit changed the raster decode while the window offset is zero"); queue.context[0x80] = 1; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "window offset"); queue.context[0x80] = 0; queue.userConfig[0x242] = 2; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "polygon mode, depth bias or nonstandard rasterization"); queue.userConfig[0x242] = 4; queue.context[0x205] = 0x00001a4au; queue.context[0x2de] = 0x1f0u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "units other than the depth format"); queue.context[0x2de] = 0x1e9u; auto cleared = queue; cleared.ClearContext(); for (const auto& [offset, value] : queue.context) { if (offset != 0x2dfu) cleared.context[offset] = value; } state = AgcDriver::Graphics::DecodeState(cleared); Require(state.depthBias && state.depthBiasConstant == 4.0f && state.depthBiasClamp == 0.0f, "a depth bias clamp the title never writes must decode as its reset value 0"); } alignas(256) std::array dccKeys{}; void metadataPassTests() { using AgcDriver::Graphics::ColorMetadataPass; using AgcDriver::Graphics::DecodeColorMetadataPass; auto queue = makeState(); queue.context[0x0] = 0; Require(!DecodeColorMetadataPass(queue).has_value(), "normal color rendering decoded as a metadata pass"); queue.context[0x202] = 0xcc0020; queue.context[0x323] = 0x11223344; queue.context[0x324] = 0x55667788; auto pass = DecodeColorMetadataPass(queue); Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::EliminateFastClear && pass->targets.size() == 1, "fast-clear eliminate did not decode"); Require(pass->targets[0].address == reinterpret_cast(colorMemory.data()) && pass->targets[0].extent.width == 64 && pass->targets[0].extent.height == 4, "fast-clear eliminate target changed"); Require(pass->targets[0].clearWords[0] == 0x11223344 && pass->targets[0].clearWords[1] == 0x55667788, "fast-clear eliminate lost CB_COLOR_CLEAR_WORD"); queue.context[0x1c5] = 2; queue.context[0x8f] = 1; Require(DecodeColorMetadataPass(queue).has_value(), "the blit's export format refused a metadata pass"); queue.context[0x202] = 0xcc0060; pass = DecodeColorMetadataPass(queue); Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::DccDecompress, "DCC decompress did not decode"); queue.context[0x8e] = 0; Require(DecodeColorMetadataPass(queue)->targets.empty(), "a disabled target joined the metadata pass"); queue.context[0x8e] = 0xf; queue.context[0x202] = 0xcc0061; expectFailure([&] { DecodeColorMetadataPass(queue); }, "nonstandard ROP"); queue.context[0x202] = 0x330060; expectFailure([&] { DecodeColorMetadataPass(queue); }, "nonstandard ROP"); queue.context[0x202] = 0xcc0060; queue.context[0x200] = 2; expectFailure([&] { DecodeColorMetadataPass(queue); }, "depth or stencil work"); queue.context[0x10] = 0x80000180; queue.context[0x11] = 0x20000180; queue.context[0x200] = 0x700763; queue.context[0x0] = 0x20; Require(DecodeColorMetadataPass(queue).has_value(), "depth and stencil state without depth or stencil planes refused the pass"); queue.context[0x10] = 0x80000183; expectFailure([&] { DecodeColorMetadataPass(queue); }, "depth or stencil work"); queue.context[0x10] = 0x80000180; queue.context[0x11] = 0x20000181; expectFailure([&] { DecodeColorMetadataPass(queue); }, "depth or stencil work"); queue.context[0x11] = 0x20000180; queue.context.erase(0x200); queue.context.erase(0x0); Require(DecodeColorMetadataPass(queue).has_value(), "unwritten depth and render control refused a pass without depth or stencil planes"); queue.context.erase(0x10); queue.context.erase(0x11); queue.context[0x0] = 0; queue.context[0x200] = 0x70; Require(DecodeColorMetadataPass(queue).has_value(), "an always-pass depth function without a test refused the pass"); queue.context[0x91] = 0x40020; expectFailure([&] { DecodeColorMetadataPass(queue); }, "over part of a color target"); queue.context[0x91] = 0x40040; queue.context[0x10f] = std::bit_cast(16.0f); expectFailure([&] { DecodeColorMetadataPass(queue); }, "over part of a color target"); queue.context[0x10f] = std::bit_cast(32.0f); queue.context[0x202] = 0xcc0030; queue.context[0x1c5] = 9; queue.context[0x8f] = 0xf; Require(!DecodeColorMetadataPass(queue).has_value(), "resolve decoded as a metadata pass"); Require(AgcDriver::Graphics::DrawRejection(queue, false).find("mode resolve") != std::string::npos, "the resolve rejection does not name the mode"); queue = makeState(); queue.context[0x0] = 0; queue.context[0x202] = 0xcc0020; queue.context[0x31c] |= 0x10000000; const auto keysAddress = reinterpret_cast(dccKeys.data()); queue.context[0x325] = static_cast(keysAddress >> 8u); queue.context[0x3a8] = static_cast(keysAddress >> 40u); queue.context[0x323] = 0x11223344; queue.context[0x324] = 0; pass = DecodeColorMetadataPass(queue); Require(pass.has_value() && pass->targets.size() == 1 && pass->targets[0].dccAddress == keysAddress, "the metadata pass lost the target's DCC keys"); auto mipmapped = queue; mipmapped.context[0x3b0] |= 1u << 28u; expectFailure([&] { DecodeColorMetadataPass(mipmapped); }, "mipmapped DCC"); const AgcDriver::Graphics::Context context{}; const auto texels = [&](std::uint32_t value) { for (std::size_t offset = 0; offset < colorMemory.size(); offset += 4) { std::uint32_t texel = 0; std::memcpy(&texel, colorMemory.data() + offset, 4); if (texel != value) return false; } return true; }; const auto keysAre = [&](std::uint8_t key) { return std::all_of(dccKeys.begin(), dccKeys.end(), [&](std::uint8_t value) { return value == key; }); }; std::memset(colorMemory.data(), 0x5a, colorMemory.size()); dccKeys.fill(0x20); AgcDriver::Graphics::RunColorMetadataPass(context, *pass); Require(texels(0x11223344) && keysAre(0xff), "a register fast clear was not eliminated into the texels"); std::memset(colorMemory.data(), 0x5a, colorMemory.size()); dccKeys.fill(0xc0); AgcDriver::Graphics::RunColorMetadataPass(context, *pass); Require(texels(0xffffffffu) && keysAre(0xff), "a 1111 fast clear was not eliminated into the texels"); std::memset(colorMemory.data(), 0x5a, colorMemory.size()); AgcDriver::Graphics::RunColorMetadataPass(context, *pass); Require(texels(0x5a5a5a5au) && keysAre(0xff), "a pass over uncompressed keys changed the texels"); dccKeys = {0x20, 0xff, 0x20, 0x20}; expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "per-block metadata"); Require(texels(0x5a5a5a5au), "a refused pass changed the texels"); pass->targets[0].dccAddress = 0; AgcDriver::Graphics::RunColorMetadataPass(context, *pass); Require(texels(0x5a5a5a5au), "a pass over a target without DCC changed its texels"); using AgcDriver::Graphics::DccKeys; struct TenBitClear { std::uint8_t key; DccKeys keys; std::uint32_t texel; const char* code; }; for (const std::uint32_t swap : {0u, 1u}) { auto tenBit = queue; tenBit.context[0x31c] = (tenBit.context[0x31c] & ~0x187cu) | (9u << 2u) | (swap << 11u); const auto tenBitPass = DecodeColorMetadataPass(tenBit); const auto format = swap == 0 ? VK_FORMAT_A2B10G10R10_UNORM_PACK32 : VK_FORMAT_A2R10G10B10_UNORM_PACK32; const std::string name = swap == 0 ? "A2B10G10R10" : "A2R10G10B10"; Require(tenBitPass.has_value() && tenBitPass->targets.size() == 1 && tenBitPass->targets[0].format == format && tenBitPass->targets[0].dccAlphaOnMsb, "the " + name + " metadata pass target changed"); for (const auto& clear : {TenBitClear{0x00, DccKeys::Clear0000, 0u, "0000"}, TenBitClear{0x40, DccKeys::Clear0001, 0xc0000000u, "0001"}, TenBitClear{0x80, DccKeys::Clear1110, 0x3fffffffu, "1110"}, TenBitClear{0xc0, DccKeys::Clear1111, 0xffffffffu, "1111"}}) { std::memset(colorMemory.data(), 0x5a, colorMemory.size()); dccKeys.fill(clear.key); AgcDriver::Graphics::RunColorMetadataPass(context, *tenBitPass); Require(texels(clear.texel) && keysAre(0xff), "a " + std::string(clear.code) + " fast clear of an " + name + " target was not eliminated into its 10/10/10/2 texel"); std::array copied{}; copied.fill(0x5a5a5a5au); Require(AgcDriver::Graphics::FillDccClear(tenBitPass->targets[0].format, clear.keys, tenBitPass->targets[0].dccAlphaOnMsb, std::as_writable_bytes(std::span(copied))) && std::ranges::all_of(copied, [&](std::uint32_t word) { return word == clear.texel; }), "a copied " + name + " target under " + clear.code + " keys was not filled with its 10/10/10/2 texel"); } } constexpr std::uint32_t tiledSide = 128; constexpr std::size_t tiledBytes = tiledSide * tiledSide * 4; constexpr std::size_t blockAlignment = 65536; const auto keyCount = AgcDriver::Graphics::DccKeyCount(AgcDriver::Graphics::TextureTileMode::kR64KBX, 4, tiledSide, tiledSide, tiledBytes); Require(keyCount == 4096 && AgcDriver::Graphics::DccKeyBytes(tiledBytes) == 256, "the 128x128 SW_64KB_R_X DCC extent changed"); std::vector tiledBlock(blockAlignment + tiledBytes + keyCount); const auto tiledAddress = (reinterpret_cast(tiledBlock.data()) + blockAlignment - 1) / blockAlignment * blockAlignment; auto* tiledTexels = reinterpret_cast(tiledAddress); auto* tiledKeys = tiledTexels + tiledBytes; for (const bool pipeAligned : {true, false}) { auto tiled = queue; tiled.context[0x3b8] |= (static_cast(AgcDriver::Graphics::ColorTileMode::RenderTarget) << 14u) | (pipeAligned ? 1u << 30u : 0u); tiled.context[0x3b0] = ((tiledSide - 1u) << 14u) | (tiledSide - 1u); tiled.context[0x318] = static_cast(tiledAddress >> 8u); tiled.context[0x390] = static_cast(tiledAddress >> 40u); tiled.context[0x325] = static_cast((tiledAddress + tiledBytes) >> 8u); tiled.context[0x3a8] = static_cast((tiledAddress + tiledBytes) >> 40u); for (const auto offset : {0xdu, 0x82u, 0x91u, 0x95u}) tiled.context[offset] = (tiledSide << 16u) | tiledSide; tiled.context[0x10f] = std::bit_cast(64.0f); tiled.context[0x110] = std::bit_cast(64.0f); tiled.context[0x111] = std::bit_cast(-64.0f); tiled.context[0x112] = std::bit_cast(64.0f); const auto tiledPass = DecodeColorMetadataPass(tiled); const std::string alignment = pipeAligned ? "pipe-aligned" : "unaligned"; Require(tiledPass.has_value() && tiledPass->targets.size() == 1 && tiledPass->targets[0].tileMode == AgcDriver::Graphics::ColorTileMode::RenderTarget && tiledPass->targets[0].bytes == tiledBytes && tiledPass->targets[0].dccAddress == tiledAddress + tiledBytes && tiledPass->targets[0].dccPipeAligned == pipeAligned, "the 128x128 SW_64KB_R_X metadata pass target with " + alignment + " DCC changed"); const auto expected = pipeAligned ? keyCount : AgcDriver::Graphics::DccKeyBytes(tiledBytes); for (const auto& [key, texel, code] : {std::tuple{std::uint8_t{0x20}, 0x11223344u, "register"}, std::tuple{std::uint8_t{0xc0}, 0xffffffffu, "1111"}}) { std::memset(tiledTexels, 0x5a, tiledBytes); std::memset(tiledKeys, key, keyCount); AgcDriver::Graphics::RunColorMetadataPass(context, *tiledPass); const auto stored = static_cast(std::count(tiledKeys, tiledKeys + keyCount, std::uint8_t{0xff})); bool filled = true; for (std::size_t offset = 0; offset < tiledBytes; offset += 4) filled = filled && std::memcmp(tiledTexels + offset, &texel, 4) == 0; Require(filled && stored == expected && std::all_of(tiledKeys, tiledKeys + expected, [](std::uint8_t value) { return value == 0xff; }), std::string("a ") + code + " fast clear eliminate of a 128x128 SW_64KB_R_X target with " + alignment + " DCC stored uncompressed keys over " + std::to_string(stored) + " of its " + std::to_string(keyCount) + " DCC key bytes, expected " + std::to_string(expected)); } std::vector original(tiledBytes / 4); for (std::size_t index = 0; index < original.size(); ++index) original[index] = 0x01000000u + static_cast(index); const auto resetSingle = [&] { std::memcpy(tiledTexels, original.data(), tiledBytes); std::memset(tiledKeys, 0x10, keyCount); }; resetSingle(); expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *tiledPass); }, "fast-clear eliminate over comp-to-single"); Require(std::memcmp(tiledTexels, original.data(), tiledBytes) == 0 && std::all_of(tiledKeys, tiledKeys + keyCount, [](std::uint8_t value) { return value == 0x10; }), "a refused fast-clear eliminate over comp-to-single keys changed the " + alignment + " target"); resetSingle(); AgcDriver::Graphics::RunColorMetadataPass(context, {AgcDriver::Graphics::ColorMetadataPass::Mode::DccDecompress, tiledPass->targets}); const AgcDriver::Graphics::ColorTargetLayout layout(tiledSide, tiledSide, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); bool expanded = true; for (std::uint32_t y = 0; y < tiledSide && expanded; ++y) { for (std::uint32_t x = 0; x < tiledSide && expanded; ++x) { std::uint32_t texel = 0; std::memcpy(&texel, tiledTexels + layout.Offset(x, y), 4); expanded = texel == original[layout.Offset(x & ~7u, y & ~7u) / 4]; } } const auto stored = static_cast(std::count(tiledKeys, tiledKeys + keyCount, std::uint8_t{0xff})); Require(expanded && stored == expected, "a DCC decompress of comp-to-single keys over a 128x128 SW_64KB_R_X target with " + alignment + " DCC did not give every 8x8 block its first texel (" + std::to_string(stored) + " of " + std::to_string(keyCount) + " keys uncompressed, expected " + std::to_string(expected) + ")"); } } alignas(4096) std::array cmaskMemory{}; void cmaskTests() { using AgcDriver::Graphics::DecodeColorBuffer; auto queue = makeState(); queue.context[0x31c] |= 0x2000; const auto cmaskAddress = reinterpret_cast(cmaskMemory.data()); queue.context[0x31f] = static_cast(cmaskAddress >> 8u); queue.context[0x398] = static_cast(cmaskAddress >> 40u); queue.context[0x323] = 0x11223344; queue.context[0x324] = 0; queue.context[0x3b8] = 0x0dc00000u; const auto target = DecodeColorBuffer(queue.context, 0); Require(target.cmaskAddress == cmaskAddress && target.cmaskBytes == cmaskMemory.size(), "the fast-clear target lost its CMASK"); std::vector log; AgcDriver::Graphics::RegisterReadLog() = &log; static_cast(DecodeColorBuffer(queue.context, 0)); AgcDriver::Graphics::RegisterReadLog() = nullptr; Require(!log.empty(), "the register facade recorded nothing for a fast-clear target"); for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a register the fast-clear target decode reads: " + std::to_string(read.offset)); Require(DecodeColorBuffer(makeState().context, 0).cmaskAddress == 0, "a target without FAST_CLEAR got a CMASK"); using AgcDriver::Graphics::CmaskBytes; Require(CmaskBytes(1920, 1080) == 0x6000 && CmaskBytes(960, 544) == 0x2000 && CmaskBytes(800, 450) == 0x1000 && CmaskBytes(1024, 512) == 0x1000 && CmaskBytes(3840, 2160) == 0x14000, "a CMASK does not span whole 1024x512 metablocks"); auto mipmapped = queue; mipmapped.context[0x3b0] |= 1u << 28u; expectFailure([&] { DecodeColorBuffer(mipmapped.context, 0); }, "mipmapped, 3D or array"); auto unaddressed = queue; unaddressed.context[0x31f] = 0; unaddressed.context[0x398] = 0; expectFailure([&] { DecodeColorBuffer(unaddressed.context, 0); }, "without a CMASK address"); auto unaligned = queue; unaligned.context[0x3b8] = 0x09c00000u; expectFailure([&] { DecodeColorBuffer(unaligned.context, 0); }, "pipe-aligned SW_64KB_Z_X"); unaligned.context[0x3b8] = 0x0d000000u; expectFailure([&] { DecodeColorBuffer(unaligned.context, 0); }, "pipe-aligned SW_64KB_Z_X"); unaligned = queue; unaligned.context[0x31f] += 1u; expectFailure([&] { DecodeColorBuffer(unaligned.context, 0); }, "misaligned"); using AgcDriver::Graphics::CmaskLayout; Require(CmaskBytes(61, 13) == CmaskLayout(61, 13).Bytes() && CmaskBytes(3840, 2160) == CmaskLayout(3840, 2160).Bytes(), "the CMASK size and layout disagree"); queue.context[0x0] = 0; queue.context[0x202] = 0xcc0020; auto pass = AgcDriver::Graphics::DecodeColorMetadataPass(queue); Require(pass.has_value() && pass->targets.size() == 1 && pass->targets[0].cmaskAddress == cmaskAddress, "the metadata pass lost the target's CMASK"); const AgcDriver::Graphics::Context context{}; const auto texels = [&](std::uint32_t value) { for (std::size_t offset = 0; offset < colorMemory.size(); offset += 4) { std::uint32_t texel = 0; std::memcpy(&texel, colorMemory.data() + offset, 4); if (texel != value) return false; } return true; }; const auto cmaskIs = [&](std::uint8_t value) { return std::all_of(cmaskMemory.begin(), cmaskMemory.end(), [&](std::uint8_t byte) { return byte == value; }); }; std::memset(colorMemory.data(), 0x5a, colorMemory.size()); cmaskMemory.fill(0); AgcDriver::Graphics::RunColorMetadataPass(context, *pass); Require(texels(0x11223344) && cmaskIs(0xff), "a CMASK fast clear was not eliminated into the texels"); std::memset(colorMemory.data(), 0x5a, colorMemory.size()); AgcDriver::Graphics::RunColorMetadataPass(context, *pass); Require(texels(0x5a5a5a5au) && cmaskIs(0xff), "a pass over an expanded CMASK changed the texels"); const CmaskLayout layout(64, 4); const auto setTile = [&](std::uint32_t tileX, std::uint8_t code) { const auto nibble = layout.Nibble(tileX, 0); auto& entry = cmaskMemory[nibble / 2u]; const auto shift = (nibble % 2u) * 4u; entry = static_cast((entry & ~(0xfu << shift)) | (code << shift)); }; const auto tiles = [&](const auto& cleared) { for (std::size_t offset = 0; offset < colorMemory.size(); offset += 4) { std::uint32_t texel = 0; std::memcpy(&texel, colorMemory.data() + offset, 4); if (texel != (cleared((offset / 4u) % 64u / 8u) ? 0x11223344u : 0x5a5a5a5au)) return false; } return true; }; std::memset(colorMemory.data(), 0x5a, colorMemory.size()); cmaskMemory.fill(0); cmaskMemory[cmaskMemory.size() - 1] = 0xff; AgcDriver::Graphics::RunColorMetadataPass(context, *pass); Require(texels(0x11223344) && cmaskIs(0xff), "CMASK nibbles past the target's tiles changed what its tiles hold"); std::memset(colorMemory.data(), 0x5a, colorMemory.size()); cmaskMemory.fill(0xff); setTile(1, 0); setTile(7, 0); AgcDriver::Graphics::RunColorMetadataPass(context, *pass); Require(tiles([](std::uint32_t tile) { return tile == 1 || tile == 7; }) && cmaskIs(0xff), "a partly fast-cleared CMASK did not clear exactly its cleared tiles"); std::memset(colorMemory.data(), 0x5a, colorMemory.size()); cmaskMemory.fill(0xff); setTile(4, 0); AgcDriver::Graphics::RunColorMetadataPass(context, {AgcDriver::Graphics::ColorMetadataPass::Mode::DccDecompress, pass->targets}); Require(tiles([](std::uint32_t tile) { return tile == 4; }) && cmaskIs(0xff), "a DCC decompress pass did not eliminate the CMASK fast clear"); std::memset(colorMemory.data(), 0x5a, colorMemory.size()); cmaskMemory.fill(0xff); setTile(2, 0); setTile(3, 3); expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "not a single-sample fast-clear code"); Require(texels(0x5a5a5a5au), "a refused pass changed the texels"); queue.context[0x31c] |= 0x10000000; const auto keysAddress = reinterpret_cast(dccKeys.data()); queue.context[0x325] = static_cast(keysAddress >> 8u); queue.context[0x3a8] = static_cast(keysAddress >> 40u); pass = AgcDriver::Graphics::DecodeColorMetadataPass(queue); Require(pass.has_value() && pass->targets.size() == 1 && pass->targets[0].cmaskAddress == cmaskAddress && pass->targets[0].dccAddress == keysAddress, "a DCC fast-clear target lost its CMASK or DCC keys"); std::memset(colorMemory.data(), 0x5a, colorMemory.size()); cmaskMemory.fill(0xff); dccKeys.fill(0x20); AgcDriver::Graphics::RunColorMetadataPass(context, *pass); Require(texels(0x11223344) && cmaskIs(0xff) && std::ranges::all_of(dccKeys, [](std::uint8_t key) { return key == 0xff; }), "a DCC fast clear under an expanded CMASK was not eliminated through its keys"); std::memset(colorMemory.data(), 0x5a, colorMemory.size()); cmaskMemory.fill(0); expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "DCC color target"); Require(texels(0x5a5a5a5au), "a refused pass changed the texels of a DCC target"); } void uint8x4TargetTests() { auto queue = makeState(); queue.context[0x1c5] = 7; queue.context[0x31c] = (queue.context[0x31c] & ~0x1ffcu) | (10u << 2u) | (4u << 8u); const auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.color.format == VK_FORMAT_R8G8B8A8_UINT && state.color.elementBytes == 4 && state.color.uintExport, "an 8_8_8_8 UINT color target was rejected"); for (const std::uint32_t swap : {2u, 3u}) { queue.context[0x31c] = (queue.context[0x31c] & ~0x1800u) | (swap << 11u); Require(AgcDriver::Graphics::DecodeState(queue).color.format == VK_FORMAT_R8G8B8A8_UINT, "a reversed 8_8_8_8 UINT color target was rejected"); } queue.context[0x31c] = (queue.context[0x31c] & ~0x1800u) | (1u << 11u); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color format 10 number type 4 component swap 1"); } void uint16ExportTests() { auto queue = makeState(); queue.context[0x1c5] = 7; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 7"); queue.context[0x31c] = (queue.context[0x31c] & ~0x77cu) | 0x404u; const auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.hasColorTarget && state.color.format == VK_FORMAT_R8_UINT && state.color.uintExport, "a UINT16_ABGR export into an unsigned integer target did not decode"); Require(!AgcDriver::Graphics::DecodeState(makeState()).color.uintExport, "a float export was marked unsigned integer"); auto blended = queue; blended.context[0x1e0] = 1u << 30u; expectFailure([&] { AgcDriver::Graphics::DecodeState(blended); }, "blending into an unsigned integer target"); queue.context[0x1c5] = 8; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 8"); } VkSampleCountFlags depthSampleCounts = VK_SAMPLE_COUNT_4_BIT; VkResult depthSampleResult = VK_SUCCESS; std::uint32_t depthSampleQueries = 0; VKAPI_ATTR VkResult VKAPI_CALL sampleLocationDepthProperties(VkPhysicalDevice, VkFormat format, VkImageType type, VkImageTiling tiling, VkImageUsageFlags usage, VkImageCreateFlags flags, VkImageFormatProperties* properties) { Require(format == VK_FORMAT_D32_SFLOAT && type == VK_IMAGE_TYPE_2D && tiling == VK_IMAGE_TILING_OPTIMAL, "sample location query changed the depth image format or type"); Require(usage == (VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT) && flags == VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT, "sample location query differs from depth image creation"); ++depthSampleQueries; properties->sampleCounts = depthSampleCounts; return depthSampleResult; } void multisampleTests() { using AgcDriver::Graphics::ColorMetadataPass; using AgcDriver::Graphics::DecodeColorMetadataPass; using AgcDriver::Graphics::DecodeState; const auto multisampled = [] { auto queue = makeState(); queue.context[0x2f8] = 0x00100001; queue.context[0x292] = 3; queue.context[0x3b8] = 0x0dc00000u; for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; queue.context[0x31d] = 0x9000; queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; return queue; }; const auto cmaskAddress = reinterpret_cast(cmaskMemory.data()); const auto setCmaskAddress = [&](auto& target, std::uint32_t slot = 0u) { const auto stride = slot * 0xfu; target.context[0x31f + stride] = static_cast(cmaskAddress >> 8u); target.context[0x398 + slot] = static_cast(cmaskAddress >> 40u); }; auto queue = multisampled(); auto state = DecodeState(queue); Require(state.samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 2 && state.colors[0].cmaskAddress == 0, "2x multisampling did not decode"); const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); Require(rejection.empty(), "the precheck refused 2x multisampling: " + rejection); queue.context[0x302] = 0x44cc; expectFailure([&] { DecodeState(queue); }, "nonstandard sample locations"); queue = multisampled(); queue.context[0x2f8] = 0; queue.context[0x292] = 2; queue.context[0x31d] = 0x12000; state = DecodeState(queue); Require(state.samples == 4 && state.colors.size() == 1 && state.colors[0].samples == 4 && state.customSampleLocations, "single-sample scan conversion into a 4x target did not rasterize the target's samples"); const auto centered = AgcDriver::Graphics::SampleLocations(state); for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(centered[sample].x == 0.5f && centered[sample].y == 0.5f, "single-sample scan conversion did not place every sample at the pixel center"); queue.context[0x292] = 3; expectFailure([&] { DecodeState(queue); }, "MSAA_ENABLE on a single-sample target"); queue = multisampled(); queue.context[0x2f8] = 0x00300003; queue.context[0x31d] = 0x1b000; for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { queue.context[0x2fe + pixel * 4u] = 0xbd153fd1u; queue.context[0x2ff + pixel * 4u] = 0x9773f95bu; } state = DecodeState(queue); Require(state.samples == 8 && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard"); queue.context[0x2f8] = 0x00200002; queue.context[0x31d] = 0x12000; for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x22eea66a; state = DecodeState(queue); Require(state.samples == 4 && state.customSampleLocations, "uniform custom sample locations were not decoded"); const auto locations = AgcDriver::Graphics::SampleLocations(state); const std::array expected{{{0.125f, 0.875f}, {0.875f, 0.125f}, {0.375f, 0.375f}, {0.625f, 0.625f}}}; for (std::size_t sample = 0; sample < expected.size(); ++sample) Require(locations[sample].x == expected[sample].x && locations[sample].y == expected[sample].y, "custom sample coordinates or indices changed"); AgcDriver::Graphics::Context sampleContext{}; expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "VK_EXT_sample_locations"); sampleContext.sampleLocationSampleCounts = VK_SAMPLE_COUNT_2_BIT; expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "sample count"); sampleContext.sampleLocationSampleCounts |= VK_SAMPLE_COUNT_4_BIT; AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); sampleContext.imageFormatProperties = sampleLocationDepthProperties; using AgcDriver::Graphics::SampleLocationsCompatibleDepth; Require(SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "supported custom sample depth format was refused"); state.depth = AgcDriver::Graphics::DepthTarget{}; state.depth->format = VK_FORMAT_D32_SFLOAT; state.depth->samples = 4; Require(AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state), "supported custom depth sample locations were disabled"); depthSampleCounts = VK_SAMPLE_COUNT_2_BIT; Require(!SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "unsupported custom sample depth count was accepted"); expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "compatible depth format and sample count"); depthSampleCounts = VK_SAMPLE_COUNT_4_BIT; depthSampleResult = VK_ERROR_FORMAT_NOT_SUPPORTED; Require(!SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "unsupported custom sample depth format was accepted"); expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "compatible depth format and sample count"); state.customSampleLocations = false; Require(!AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state), "standard locations did not fall back for an incompatible depth format"); depthSampleResult = VK_ERROR_OUT_OF_HOST_MEMORY; expectFailure([&] { SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4); }, "Vulkan result"); depthSampleResult = VK_SUCCESS; const auto queries = depthSampleQueries; Require(!SampleLocationsCompatibleDepth({}, VK_FORMAT_D32_SFLOAT, 4) && !SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 1), "sample location depth compatibility requires the extension and multisampling"); Require(depthSampleQueries == queries, "ineligible sample location depth formats were queried"); for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae; state = DecodeState(queue); Require(!state.customSampleLocations, "standard sample locations require the extension"); const auto standard = AgcDriver::Graphics::SampleLocations(state); const std::array expectedStandard{{{0.375f, 0.125f}, {0.875f, 0.375f}, {0.125f, 0.625f}, {0.625f, 0.875f}}}; for (std::size_t sample = 0; sample < expectedStandard.size(); ++sample) Require(standard[sample].x == expectedStandard[sample].x && standard[sample].y == expectedStandard[sample].y, "standard sample coordinate scale or Y direction changed"); AgcDriver::Graphics::ValidateSampleLocations({}, state); for (const auto samples : {2u, 8u}) { queue = multisampled(); const auto log2 = samples == 2 ? 1u : 3u; queue.context[0x2f8] = (log2 << 20u) | log2; queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { queue.context[0x2fe + pixel * 4u] = 0x77887788; queue.context[0x2ff + pixel * 4u] = 0x77887788; } state = DecodeState(queue); Require(state.customSampleLocations && state.samples == samples, "a custom 2x or 8x pattern did not decode"); const auto endpoints = AgcDriver::Graphics::SampleLocations(state); for (std::uint32_t sample = 0; sample < samples; ++sample) { const auto expected = sample % 2u == 0 ? 0.0f : 15.0f / 16.0f; Require(endpoints[sample].x == expected && endpoints[sample].y == expected, "signed sample coordinate endpoints changed"); } } queue.context[0x2f8] = 0x00300003; queue.context[0x31d] = 0x1b000; for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { queue.context[0x2fe + pixel * 4u] = 0xbd153fd1u; queue.context[0x2ff + pixel * 4u] = 0x9773f95bu; } state = DecodeState(queue); Require(state.samples == 8 && !state.customSampleLocations && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard"); AgcDriver::Graphics::ValidateSampleLocations({}, state); queue = multisampled(); queue.context[0x31d] = 0x1000; expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); queue.context[0x31d] = 0; expectFailure([&] { DecodeState(queue); }, "sample count differs"); queue = multisampled(); queue.context[0x292] = 2; expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE"); queue = multisampled(); queue.context[0x2f8] |= 0x10; expectFailure([&] { DecodeState(queue); }, "coverage conversion"); queue = multisampled(); queue.context[0x31c] |= 0x6000; setCmaskAddress(queue); Require(DecodeState(queue).colors[0].cmaskAddress == cmaskAddress, "the CMASK address of a multisampled target did not decode"); queue = makeState(); queue.context[0x31c] |= 0x4000; expectFailure([&] { DecodeState(queue); }, "color compression"); queue = multisampled(); queue.context[0x31c] |= 0x2000; setCmaskAddress(queue); const auto fastCleared = DecodeState(queue).colors[0]; Require(fastCleared.cmaskAddress == cmaskAddress && fastCleared.cmaskFastClear && fastCleared.cmaskBytes == 0x1000, "a multisampled FAST_CLEAR target did not decode its CMASK at the metablock size"); queue = multisampled(); queue.context[0x31c] |= 0x4000; setCmaskAddress(queue); const auto compressed = DecodeState(queue).colors[0]; Require(compressed.cmaskAddress == cmaskAddress && !compressed.cmaskFastClear && compressed.cmaskBytes == 0x1000, "a multisampled COMPRESSION target did not decode its CMASK at the metablock size"); queue = multisampled(); queue.context[0x31c] |= 0x2000; queue.context[0x31f] = 0; expectFailure([&] { DecodeState(queue); }, "without a CMASK address"); queue = multisampled(); queue.context[0x31c] |= 0x2000; setCmaskAddress(queue); std::vector reads; AgcDriver::Graphics::RegisterReadLog() = &reads; static_cast(DecodeState(queue)); AgcDriver::Graphics::RegisterReadLog() = nullptr; for (const auto read : reads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a CMASK register the decoder reads: " + std::to_string(read.offset)); queue = multisampled(); queue.context[0x0] = 0; queue.context[0x202] = 0xcc0030; for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x390u}) queue.context[offset + (offset >= 0x390u ? 1u : 0xfu)] = queue.context[offset]; queue.context[0x32c] = 0; queue.context[0x3b1] = queue.context[0x3b0]; queue.context[0x3b9] = queue.context[0x3b8]; setCmaskAddress(queue); setCmaskAddress(queue, 1); auto pass = DecodeColorMetadataPass(queue); Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::Resolve && pass->source.has_value() && pass->source->samples == 2 && pass->targets.size() == 1 && pass->targets[0].samples == 1, "a CB resolve of a multisampled target did not decode"); queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u); expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); queue = multisampled(); queue.context[0x0] = 0; queue.context[0x202] = 0xcc0030; for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x390u}) queue.context[offset + (offset >= 0x390u ? 1u : 0xfu)] = queue.context[offset]; queue.context[0x32c] = 0; queue.context[0x3b1] = queue.context[0x3b0]; queue.context[0x3b9] = queue.context[0x3b8]; queue.context[0x32b] = queue.context[0x31c] | 0x2000u; setCmaskAddress(queue, 1); expectFailure([&] { DecodeColorMetadataPass(queue); }, "fast-clear color target"); queue = multisampled(); queue.context[0x2f8] = 0; queue.context[0x0] = 0; queue.context[0x202] = 0xcc0020; expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled color target"); using AgcDriver::Graphics::DccKeys; using AgcDriver::Graphics::MultisampledCmask; MultisampledCmask cmask(0x10000, 0x1000); cmask.NoteFill(0x10000, 0x1000, 0x11111111u); expectFailure([&] { cmask.TakeClear(); }, "leaves its tiles mixed"); cmask.NoteFill(0x10800, 0x1000, 0); expectFailure([&] { cmask.TakeClear(); }, "partly covers"); cmask.NoteFill(0x10000, 0x1000, 0); Require(cmask.TakeClear() && !cmask.TakeClear(), "a zero fill of the whole CMASK did not clear once"); cmask.NoteFill(0x10000, 0x1000, 0xffffffffu); Require(!cmask.TakeClear(), "an expanded CMASK cleared"); cmask.NoteFill(0x20000, 0x1000, 0x11111111u); Require(!cmask.TakeClear() && cmask.Keys() == DccKeys::Uncompressed, "a fill of another range changed the CMASK"); cmask.CheckAddress(0x10000); expectFailure([&] { cmask.CheckAddress(0x20000); }, "used CMASK 0x20000"); MultisampledCmask seeded(0x30000, 0x1000); seeded.SeedKeys(DccKeys::Clear0000); Require(seeded.TakeClear(), "a fast-cleared seed did not clear its first use"); MultisampledCmask mixedSeed(0x30000, 0x1000); mixedSeed.SeedKeys(DccKeys::Mixed); expectFailure([&] { mixedSeed.TakeClear(); }, "not modeled"); MultisampledCmask unaddressed; unaddressed.NoteFill(0, 0x1000, 0); Require(!unaddressed.TakeClear(), "a target without a CMASK cleared"); } 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(0.5f); queue.context[0x114] = std::bit_cast(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(-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(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(-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(0.5f); queue.context[0x113] = std::bit_cast(0.5f); queue.context[0xb4] = std::bit_cast(2.0f); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "inverted viewport depth clamp"); queue.context[0xb4] = 0; auto collapsedQueue = makeState(); collapsedQueue.context[0x10f] = 0; collapsedQueue.context[0x110] = 0; collapsedQueue.context[0x111] = std::bit_cast(-0.0f); collapsedQueue.context[0x112] = 0; const auto collapsed = AgcDriver::Graphics::DecodeState(collapsedQueue); Require(collapsed.scissor.extent.width == 0 && collapsed.scissor.extent.height == 0, "a triangle draw through a zero-scale viewport rasterizes"); Require(collapsed.viewport.width > 0 && collapsed.viewport.height > 0 && collapsed.viewport.minDepth == 0 && collapsed.viewport.maxDepth == 1, "a zero-scale viewport did not become a valid Vulkan viewport"); collapsedQueue.userConfig[0x242] = 2; expectFailure([&] { AgcDriver::Graphics::DecodeState(collapsedQueue); }, "unsupported viewport transform"); collapsedQueue.userConfig[0x242] = 4; collapsedQueue.context[0x10f] = std::bit_cast(-0.0f); Require(AgcDriver::Graphics::DecodeState(collapsedQueue).scissor.extent.width == 0, "a negative-zero viewport scale was not collapsed"); collapsedQueue.context[0x10f] = std::bit_cast(-1.0f); expectFailure([&] { AgcDriver::Graphics::DecodeState(collapsedQueue); }, "unsupported viewport transform"); for (std::uint32_t bit = 0; bit < 32; ++bit) { if (bit == 19 || bit == 24 || bit == 26 || bit == 27) continue; for (const auto linearBit : {0u, 0x01000000u}) { queue.context[0x204] = (1u << bit) | linearBit; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_CL_CLIP_CNTL"); Require(AgcDriver::Graphics::DrawRejection(queue, false).find("PA_CL_CLIP_CNTL") != std::string::npos, "the precheck accepted an unsupported PA_CL_CLIP_CNTL bit"); } } queue = makeState(); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; for (const auto clip : {0x00080000u, 0x00000000u, 0x0c080000u, 0x04000000u}) { queue.context[0x204] = clip; const auto plain = AgcDriver::Graphics::DecodeState(queue); queue.context[0x204] = clip | 0x01000000u; const auto linearClip = AgcDriver::Graphics::DecodeState(queue); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DX_LINEAR_ATTR_CLIP_ENA"); Require(linearClip.negativeOneToOne == plain.negativeOneToOne && linearClip.depthClamp == plain.depthClamp && linearClip.viewport.minDepth == plain.viewport.minDepth && linearClip.viewport.maxDepth == plain.viewport.maxDepth, "DX_LINEAR_ATTR_CLIP_ENA changed the clip space, depth clamping or depth range"); } } void AlphaToCoverageTests() { auto queue = makeState(); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; Require(!AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "alpha-to-coverage decoded while disabled"); queue.context[0x2dc] = 0xaa01; Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "undithered alpha-to-coverage was not decoded"); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected undithered alpha-to-coverage"); for (const auto control : {0x8701u, 0x1aa01u, 0xaa03u}) { queue.context[0x2dc] = control; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "alpha-to-coverage"); Require(AgcDriver::Graphics::DrawRejection(queue, false).find("alpha-to-coverage") != std::string::npos, "the precheck accepted unsupported alpha-to-coverage"); } queue.context[0x2dc] = 0xaa01; const auto exportFormat = queue.context.at(0x1c5); for (const auto format : {4u, 5u, 6u, 9u}) { queue.context[0x1c5] = (exportFormat & ~0xfu) | format; Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "alpha-to-coverage rejected an MRT0 export format with a floating-point alpha"); } for (const auto format : {1u, 2u, 3u}) { queue.context[0x1c5] = (exportFormat & ~0xfu) | format; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "floating-point alpha"); } queue.context[0x1c5] = exportFormat; } 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(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; Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "undithered alpha-to-coverage was not decoded"); 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 arrayElement; std::uint32_t count; VkDescriptorType type; std::vector buffers; }; struct MockVulkan { std::uint64_t next = 1; std::int64_t live = 0; std::map bufferSizes; std::map bufferUsage; std::map allocationFlags; std::map bufferMemory; std::map bufferOffset; std::map> memories; std::map allocationSizes; VkDeviceSize allocatedBytes = 0; std::optional memoryLimit; std::uint64_t allocationAttempts = 0; std::vector layoutBindings; VkDescriptorSetLayoutCreateFlags layoutFlags = 0; std::vector layoutBindingFlags; std::vector poolSizes; std::uint32_t poolMaxSets = 0; VkDescriptorPoolCreateFlags poolFlags = 0; struct DescriptorPool { VkDescriptorPoolCreateFlags flags; std::uint32_t maxSets; std::uint32_t allocated = 0; std::uint32_t attempts = 0; std::uint32_t resets = 0; }; std::map descriptorPools; std::uint32_t descriptorPoolLimit = 1000000; VkResult descriptorResetResult = VK_SUCCESS; std::vector writes; std::vector copies; bool allowCopies = false; std::uint32_t boundSets = 0; std::uint32_t boundFirst = 0; VkPipelineBindPoint boundPoint = VK_PIPELINE_BIND_POINT_MAX_ENUM; std::map pipelineLayoutPushConstantSize; std::uint32_t pipelineCreateCount = 0; std::vector> pipelineSpecializations; VkPipeline boundPipeline = VK_NULL_HANDLE; std::vector lastPushConstants; struct { std::uint32_t x = 0, y = 0, z = 0; } lastDispatchGroups; }; MockVulkan mock; template THandle makeHandle() { const auto value = mock.next++; if constexpr (std::is_pointer_v) return reinterpret_cast(static_cast(value)); else return static_cast(value); } VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateInfo* info, const VkAllocationCallbacks*, VkBuffer* buffer) { *buffer = makeHandle(); 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) { ++mock.allocationAttempts; if (mock.memoryLimit.has_value() && mock.allocatedBytes + info->allocationSize > *mock.memoryLimit) return VK_ERROR_OUT_OF_DEVICE_MEMORY; *memory = makeHandle(); mock.memories[*memory] = std::vector(info->allocationSize); mock.allocationSizes[*memory] = info->allocationSize; mock.allocatedBytes += info->allocationSize; if (info->pNext != nullptr) { const auto* flags = static_cast(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 + mock.bufferSizes.at(buffer) <= mock.memories.at(memory).size(), "mock buffer memory is bound past its allocation"); mock.bufferMemory[buffer] = memory; mock.bufferOffset[buffer] = offset; return VK_SUCCESS; } std::span mockBufferMemory(VkBuffer buffer) { auto& memory = mock.memories.at(mock.bufferMemory.at(buffer)); return std::span(memory).subspan(static_cast(mock.bufferOffset.at(buffer)), static_cast(mock.bufferSizes.at(buffer))); } 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 memory, const VkAllocationCallbacks*) { if (const auto found = mock.allocationSizes.find(memory); found != mock.allocationSizes.end()) { mock.allocatedBytes -= found->second; mock.allocationSizes.erase(found); } --mock.live; } VKAPI_ATTR VkResult VKAPI_CALL mockCreateDescriptorSetLayout(VkDevice, const VkDescriptorSetLayoutCreateInfo* info, const VkAllocationCallbacks*, VkDescriptorSetLayout* layout) { *layout = makeHandle(); mock.layoutBindings.assign(info->pBindings, info->pBindings + info->bindingCount); mock.layoutFlags = info->flags; const auto* bindingFlags = static_cast(info->pNext); if (bindingFlags != nullptr) mock.layoutBindingFlags.assign(bindingFlags->pBindingFlags, bindingFlags->pBindingFlags + bindingFlags->bindingCount); else mock.layoutBindingFlags.clear(); ++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(); mock.poolSizes.assign(info->pPoolSizes, info->pPoolSizes + info->poolSizeCount); mock.poolMaxSets = info->maxSets; mock.poolFlags = info->flags; mock.descriptorPools.emplace(*pool, MockVulkan::DescriptorPool{info->flags, std::min(info->maxSets, mock.descriptorPoolLimit)}); ++mock.live; return VK_SUCCESS; } VKAPI_ATTR void VKAPI_CALL mockDestroyDescriptorPool(VkDevice, VkDescriptorPool pool, const VkAllocationCallbacks*) { mock.descriptorPools.erase(pool); --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"); auto& pool = mock.descriptorPools.at(info->descriptorPool); ++pool.attempts; if (pool.allocated == pool.maxSets) return VK_ERROR_OUT_OF_POOL_MEMORY; ++pool.allocated; sets[0] = makeHandle(); return VK_SUCCESS; } VKAPI_ATTR VkResult VKAPI_CALL mockResetDescriptorPool(VkDevice, VkDescriptorPool handle, VkDescriptorPoolResetFlags) { auto& pool = mock.descriptorPools.at(handle); const auto result = mock.descriptorResetResult; mock.descriptorResetResult = VK_SUCCESS; if (result != VK_SUCCESS) return result; pool.allocated = 0; ++pool.resets; return VK_SUCCESS; } VKAPI_ATTR void VKAPI_CALL mockUpdateDescriptorSets(VkDevice, std::uint32_t count, const VkWriteDescriptorSet* writes, std::uint32_t copyCount, const VkCopyDescriptorSet* copies) { Require(copyCount == 0 || mock.allowCopies, "descriptor copies are not expected"); if (copyCount != 0) mock.copies.insert(mock.copies.end(), copies, copies + copyCount); for (std::uint32_t i = 0; i < count; ++i) { MockDescriptorWrite write{writes[i].dstBinding, writes[i].dstArrayElement, 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 VkResult VKAPI_CALL mockGetMemoryHostPointerProperties(VkDevice, VkExternalMemoryHandleTypeFlagBits type, const void* pointer, VkMemoryHostPointerPropertiesEXT* properties) { Require(type == VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT && pointer != nullptr, "invalid host pointer import query"); properties->memoryTypeBits = 1; return VK_SUCCESS; } 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(info->buffer) * 0x10000; } VKAPI_ATTR VkResult VKAPI_CALL mockCreatePipelineLayout(VkDevice, const VkPipelineLayoutCreateInfo* info, const VkAllocationCallbacks*, VkPipelineLayout* layout) { *layout = makeHandle(); mock.pipelineLayoutPushConstantSize[*layout] = info->pushConstantRangeCount > 0 ? info->pPushConstantRanges[0].size : 0; ++mock.live; return VK_SUCCESS; } VKAPI_ATTR void VKAPI_CALL mockDestroyPipelineLayout(VkDevice, VkPipelineLayout, const VkAllocationCallbacks*) { --mock.live; } VKAPI_ATTR VkResult VKAPI_CALL mockCreateShaderModule(VkDevice, const VkShaderModuleCreateInfo*, const VkAllocationCallbacks*, VkShaderModule* module) { *module = makeHandle(); ++mock.live; return VK_SUCCESS; } VKAPI_ATTR void VKAPI_CALL mockDestroyShaderModule(VkDevice, VkShaderModule, const VkAllocationCallbacks*) { --mock.live; } VKAPI_ATTR VkResult VKAPI_CALL mockCreateComputePipelines(VkDevice, VkPipelineCache, std::uint32_t count, const VkComputePipelineCreateInfo* infos, const VkAllocationCallbacks*, VkPipeline* pipelines) { Require(count == 1, "mock expects exactly one compute pipeline per call"); Require(infos[0].stage.pSpecializationInfo != nullptr, "compute pipeline must provide specialization data"); // The detiler's constants 0-2 (element size, block size, addressing family) are what the tests // check; the swizzle equation and block extent follow them. std::array values{}; Require(infos[0].stage.pSpecializationInfo->dataSize >= sizeof(values), "compute pipeline specialization data has an unexpected size"); std::memcpy(values.data(), infos[0].stage.pSpecializationInfo->pData, sizeof(values)); *pipelines = makeHandle(); mock.pipelineSpecializations.push_back(values); ++mock.pipelineCreateCount; ++mock.live; return VK_SUCCESS; } VKAPI_ATTR void VKAPI_CALL mockDestroyPipeline(VkDevice, VkPipeline, const VkAllocationCallbacks*) { --mock.live; } VKAPI_ATTR void VKAPI_CALL mockCmdBindPipeline(VkCommandBuffer, VkPipelineBindPoint, VkPipeline pipeline) { mock.boundPipeline = pipeline; } VKAPI_ATTR void VKAPI_CALL mockCmdPushConstants(VkCommandBuffer, VkPipelineLayout, VkShaderStageFlags, std::uint32_t, std::uint32_t size, const void* values) { const auto* bytes = static_cast(values); mock.lastPushConstants.assign(bytes, bytes + size); } VKAPI_ATTR void VKAPI_CALL mockCmdDispatch(VkCommandBuffer, std::uint32_t x, std::uint32_t y, std::uint32_t z) { mock.lastDispatchGroups = {x, y, z}; } VKAPI_ATTR void VKAPI_CALL mockCmdUpdateBuffer(VkCommandBuffer, VkBuffer buffer, VkDeviceSize offset, VkDeviceSize size, const void* data) { auto memory = mockBufferMemory(buffer); Require(offset + size <= memory.size(), "a buffer update exceeds its buffer"); std::memcpy(memory.data() + offset, data, static_cast(size)); } VKAPI_ATTR VkResult VKAPI_CALL mockAllocateCommandBuffers(VkDevice, const VkCommandBufferAllocateInfo* info, VkCommandBuffer* commands) { for (std::uint32_t index = 0; index < info->commandBufferCount; ++index) commands[index] = makeHandle(); mock.live += info->commandBufferCount; return VK_SUCCESS; } VKAPI_ATTR void VKAPI_CALL mockFreeCommandBuffers(VkDevice, VkCommandPool, std::uint32_t count, const VkCommandBuffer*) { mock.live -= count; } VKAPI_ATTR VkResult VKAPI_CALL mockCreateFence(VkDevice, const VkFenceCreateInfo*, const VkAllocationCallbacks*, VkFence* fence) { *fence = makeHandle(); ++mock.live; return VK_SUCCESS; } VKAPI_ATTR void VKAPI_CALL mockDestroyFence(VkDevice, VkFence, const VkAllocationCallbacks*) { --mock.live; } VKAPI_ATTR VkResult VKAPI_CALL mockGetFenceStatus(VkDevice, VkFence) { return VK_SUCCESS; } VKAPI_ATTR VkResult VKAPI_CALL mockResetFences(VkDevice, std::uint32_t, const VkFence*) { return VK_SUCCESS; } VKAPI_ATTR VkResult VKAPI_CALL mockWaitForFences(VkDevice, std::uint32_t, const VkFence*, VkBool32, std::uint64_t) { return VK_SUCCESS; } VKAPI_ATTR VkResult VKAPI_CALL mockBeginCommandBuffer(VkCommandBuffer, const VkCommandBufferBeginInfo*) { return VK_SUCCESS; } VKAPI_ATTR VkResult VKAPI_CALL mockEndCommandBuffer(VkCommandBuffer) { return VK_SUCCESS; } VKAPI_ATTR VkResult VKAPI_CALL mockQueueSubmit(VkQueue, std::uint32_t, const VkSubmitInfo*, VkFence) { return VK_SUCCESS; } VKAPI_ATTR void VKAPI_CALL mockCmdPipelineBarrier(VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags, VkDependencyFlags, std::uint32_t, const VkMemoryBarrier*, std::uint32_t, const VkBufferMemoryBarrier*, std::uint32_t, const VkImageMemoryBarrier*) {} VKAPI_ATTR void VKAPI_CALL mockCmdBeginQuery(VkCommandBuffer, VkQueryPool, std::uint32_t, VkQueryControlFlags) {} VKAPI_ATTR void VKAPI_CALL mockCmdEndQuery(VkCommandBuffer, VkQueryPool, std::uint32_t) {} VKAPI_ATTR void VKAPI_CALL mockCmdResetQueryPool(VkCommandBuffer, VkQueryPool, std::uint32_t, std::uint32_t) {} VKAPI_ATTR void VKAPI_CALL mockCmdCopyQueryPoolResults(VkCommandBuffer, VkQueryPool, std::uint32_t, std::uint32_t, VkBuffer, VkDeviceSize, VkDeviceSize, VkQueryResultFlags) {} PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { static const std::map table{ {"vkAllocateCommandBuffers", reinterpret_cast(mockAllocateCommandBuffers)}, {"vkFreeCommandBuffers", reinterpret_cast(mockFreeCommandBuffers)}, {"vkCreateFence", reinterpret_cast(mockCreateFence)}, {"vkDestroyFence", reinterpret_cast(mockDestroyFence)}, {"vkGetFenceStatus", reinterpret_cast(mockGetFenceStatus)}, {"vkResetFences", reinterpret_cast(mockResetFences)}, {"vkWaitForFences", reinterpret_cast(mockWaitForFences)}, {"vkBeginCommandBuffer", reinterpret_cast(mockBeginCommandBuffer)}, {"vkEndCommandBuffer", reinterpret_cast(mockEndCommandBuffer)}, {"vkQueueSubmit", reinterpret_cast(mockQueueSubmit)}, {"vkCmdPipelineBarrier", reinterpret_cast(mockCmdPipelineBarrier)}, {"vkCmdBeginQuery", reinterpret_cast(mockCmdBeginQuery)}, {"vkCmdEndQuery", reinterpret_cast(mockCmdEndQuery)}, {"vkCmdResetQueryPool", reinterpret_cast(mockCmdResetQueryPool)}, {"vkCmdCopyQueryPoolResults", reinterpret_cast(mockCmdCopyQueryPoolResults)}, {"vkGetBufferDeviceAddressKHR", reinterpret_cast(mockGetBufferDeviceAddress)}, {"vkGetMemoryHostPointerPropertiesEXT", reinterpret_cast(mockGetMemoryHostPointerProperties)}, {"vkCreateBuffer", reinterpret_cast(mockCreateBuffer)}, {"vkGetBufferMemoryRequirements", reinterpret_cast(mockGetBufferMemoryRequirements)}, {"vkAllocateMemory", reinterpret_cast(mockAllocateMemory)}, {"vkBindBufferMemory", reinterpret_cast(mockBindBufferMemory)}, {"vkMapMemory", reinterpret_cast(mockMapMemory)}, {"vkUnmapMemory", reinterpret_cast(mockUnmapMemory)}, {"vkDestroyBuffer", reinterpret_cast(mockDestroyBuffer)}, {"vkFreeMemory", reinterpret_cast(mockFreeMemory)}, {"vkCreateDescriptorSetLayout", reinterpret_cast(mockCreateDescriptorSetLayout)}, {"vkDestroyDescriptorSetLayout", reinterpret_cast(mockDestroyDescriptorSetLayout)}, {"vkCreateDescriptorPool", reinterpret_cast(mockCreateDescriptorPool)}, {"vkDestroyDescriptorPool", reinterpret_cast(mockDestroyDescriptorPool)}, {"vkAllocateDescriptorSets", reinterpret_cast(mockAllocateDescriptorSets)}, {"vkResetDescriptorPool", reinterpret_cast(mockResetDescriptorPool)}, {"vkUpdateDescriptorSets", reinterpret_cast(mockUpdateDescriptorSets)}, {"vkCmdBindDescriptorSets", reinterpret_cast(mockCmdBindDescriptorSets)}, {"vkCreatePipelineLayout", reinterpret_cast(mockCreatePipelineLayout)}, {"vkDestroyPipelineLayout", reinterpret_cast(mockDestroyPipelineLayout)}, {"vkCreateShaderModule", reinterpret_cast(mockCreateShaderModule)}, {"vkDestroyShaderModule", reinterpret_cast(mockDestroyShaderModule)}, {"vkCreateComputePipelines", reinterpret_cast(mockCreateComputePipelines)}, {"vkDestroyPipeline", reinterpret_cast(mockDestroyPipeline)}, {"vkCmdBindPipeline", reinterpret_cast(mockCmdBindPipeline)}, {"vkCmdPushConstants", reinterpret_cast(mockCmdPushConstants)}, {"vkCmdDispatch", reinterpret_cast(mockCmdDispatch)}, {"vkCmdUpdateBuffer", reinterpret_cast(mockCmdUpdateBuffer)} }; 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 = 16384; context.nullDescriptors = true; 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 guestFirst{0x11111111, 0x22222222, 0x33333333, 0x44444444}; alignas(16) std::array guestSecond{1, 2, 3, 4, 5, 6, 7, 8}; alignas(16) std::array guestThird{0xaaaaaaaa, 0xbbbbbbbb}; std::vector vsharp(const void* pointer, std::uint32_t bytes) { const auto address = reinterpret_cast(pointer); return {static_cast(address), static_cast(address >> 32u) & 0xffffu, bytes, 0x31000000u}; } std::vector join(std::vector first, const std::vector& 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 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; } std::vector ShaderDataWords(std::initializer_list userData) { std::vector words(ShaderRecompiler::RuntimeAbi::ShaderDataDwords); words[0] = ShaderRecompiler::RuntimeAbi::Version; std::copy(userData.begin(), userData.end(), words.begin() + ShaderRecompiler::RuntimeAbi::UserDataDword); return words; } bool sameBytes(std::span 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)); } std::span bufferBytes(VkBuffer buffer) { return mockBufferMemory(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 expectSingleAccepted(const ShaderRecompiler::DescriptorBinding& binding, std::string_view what) { ShaderRecompiler::RecompileResult vertex; ShaderRecompiler::RecompileResult fragment; vertex.bindings.push_back(binding); mock = MockVulkan{}; const auto context = mockContext(); const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; { AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); } Require(mock.live == 0, std::string(what) + " leaked Vulkan objects"); } void descriptorLayoutFlagTests() { constexpr VkDescriptorBindingFlags indexed = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; mock = MockVulkan{}; { AgcDriver::Graphics::DescriptorCache cache(mockContext()); const VkDescriptorSetLayoutBinding binding{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; const std::array key{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT}; const std::array flags{indexed}; const auto layout = cache.Layout(key, std::span(&binding, 1), false, flags); Require(mock.layoutFlags == 0 && mock.layoutBindingFlags == std::vector{indexed}, "native layout lost binding flags without update-after-bind"); Require(cache.Layout(key, std::span(&binding, 1), false, flags) == layout, "identical binding flags missed the layout cache"); Require(cache.Layout(key, std::span(&binding, 1)) != layout && mock.layoutBindingFlags.empty(), "layout cache ignored binding flags"); } Require(mock.live == 0, "binding flag layout cache leaked Vulkan objects"); for (const bool updateAfterBind : {false, true}) { mock = MockVulkan{}; auto context = mockContext(); context.emptyBuffer = reinterpret_cast(std::uintptr_t{0x1000}); context.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindStorageBuffers = 17; context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 128; context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 32; { ShaderRecompiler::RecompileResult program; const std::uint32_t count = updateAfterBind ? 17 : 1; program.bindings.push_back(makeBinding(Role::GuestBuffers, 0, count, std::vector(4 * count))); const AgcDriver::Graphics::CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; AgcDriver::Graphics::ShaderResources resources(context, compute); const auto flags = resources.LayoutBindingFlags(); Require(flags.size() == resources.LayoutBindings().size() && flags.size() == 1, "exported binding flag count differs from native layout"); Require(flags[0] == (updateAfterBind ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0), "exported binding flags differ from native layout"); Require(updateAfterBind ? mock.layoutBindingFlags == std::vector(flags.begin(), flags.end()) : mock.layoutBindingFlags.empty(), "native layout did not use the exported binding flag vector"); } Require(mock.live == 0, "exported binding flag fixture leaked Vulkan objects"); } } void descriptorPoolTests() { using namespace AgcDriver::Graphics; mock = MockVulkan{}; mock.descriptorPoolLimit = 2; { DescriptorCache cache(mockContext()); const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; const std::array key{binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags}; const auto layout = cache.Layout(key, std::span(&binding, 1)); const VkDescriptorPoolSize size{binding.descriptorType, binding.descriptorCount}; const auto allocate = [&] { return cache.Allocate(layout, std::span(&size, 1)); }; const auto first = allocate(); const auto second = allocate(); Require(first.pool == second.pool, "descriptor allocations did not share a pool"); Require(mock.descriptorPools.at(first.pool).flags == 0, "descriptor pools use the individually freed allocation path"); const auto third = allocate(); const auto fourth = allocate(); const auto fifth = allocate(); Require(third.pool == fourth.pool && third.pool != first.pool && fifth.pool != third.pool, "exhausted descriptor pools did not advance the chain"); Require(mock.descriptorPools.at(first.pool).attempts == 3, "an exhausted descriptor pool was retried before reset"); cache.Free(first); Require(mock.descriptorPools.at(first.pool).resets == 0, "a descriptor pool reset while another set was live"); cache.Free(second); Require(mock.descriptorPools.at(first.pool).resets == 1, "an empty descriptor pool was not reset"); const auto sixth = allocate(); const auto seventh = allocate(); Require(sixth.pool == fifth.pool && seventh.pool == first.pool && cache.Counters().pools == 3, "an empty descriptor pool was not recycled"); cache.Free(third); cache.Free(fourth); cache.Free(fifth); cache.Free(sixth); cache.Free(seventh); Require(mock.descriptorPools.at(first.pool).resets == 2 && mock.descriptorPools.at(third.pool).resets == 1 && mock.descriptorPools.at(fifth.pool).resets == 1, "descriptor pool ownership was lost after recycling"); } Require(mock.live == 0 && mock.descriptorPools.empty(), "descriptor pool cache leaked Vulkan objects"); } void descriptorPoolResetFailureTest() { using namespace AgcDriver::Graphics; mock = MockVulkan{}; DescriptorCache cache(mockContext()); const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; const std::array key{binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags}; const auto layout = cache.Layout(key, std::span(&binding, 1)); const VkDescriptorPoolSize size{binding.descriptorType, binding.descriptorCount}; const auto allocation = cache.Allocate(layout, std::span(&size, 1)); mock.descriptorResetResult = VK_ERROR_DEVICE_LOST; const auto previous = _ZSt13set_terminatePFvvE_nid_postfix([] { std::_Exit(mock.descriptorResetResult == VK_SUCCESS ? 0 : 1); }); cache.Free(allocation); _ZSt13set_terminatePFvvE_nid_postfix(previous); } void pushConstantTests() { ShaderRecompiler::RecompileResult vertex; ShaderRecompiler::RecompileResult fragment; vertex.pushConstants.assign(8, std::byte{1}); fragment.pushConstants.assign(12, std::byte{2}); std::array 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() == AgcDriver::Graphics::PipelinePushConstantBytes && 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} && bytes.back() == 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); }, "crosses a push constant slot"); shaders[1].pushConstantOffset = AgcDriver::Graphics::PipelinePushConstantBytes - 4; expectFailure([&] { AgcDriver::Graphics::AssemblePushConstants(shaders); }, "outside the pipeline push constant block"); shaders[1].pushConstantOffset = AgcDriver::Graphics::PipelinePushSlotBytes; const auto slotted = AgcDriver::Graphics::AssemblePushConstants(shaders); Require(slotted[128] == std::byte{2} && slotted[139] == std::byte{2} && slotted[8] == std::byte{0}, "a pixel shader in its push slot was misplaced"); 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 descriptorSnapshotTests() { using AgcDriver::Graphics::DescriptorCache; using AgcDriver::Graphics::Recorder; using AgcDriver::Graphics::ShaderResources; for (const unsigned selected : {1u, 2u, 3u}) { mock = MockVulkan{}; mock.allowCopies = true; auto context = mockContext(); { DescriptorCache cache(context); context.descriptorCache = &cache; std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); Recorder recorder(context); ShaderRecompiler::RecompileResult vertex, fragment; auto binding = makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32))); binding.bufferWritten = {false, false}; vertex.bindings.push_back(binding); fragment.bindings.push_back(makeBinding(Role::GuestBuffers, 4, 1, vsharp(guestThird.data(), 8))); const auto state = AgcDriver::Graphics::DecodeState(makeState()); ShaderResources resources(context, vertex, fragment, state.color, 0, 0); const auto original = findWrite(0).buffers; const auto originalThird = findWrite(4).buffers; std::vector moved; for (std::size_t index = 0; index < 2; ++index) { if ((selected & (1u << index)) == 0) continue; const std::size_t bytes = index == 0 ? 24 : 12; moved.push_back({index, 0, bytes, std::vector(bytes / 4, static_cast(0x12340000u + index))}); } mock.writes.clear(); const auto snapshots = resources.PrepareDrawBindings(recorder, moved); Require(snapshots != nullptr && snapshots->snapshots.size() == moved.size(), "draw snapshots are missing"); Require(mock.copies.empty(), "snapshot descriptor set must be written without descriptor copies"); Require(mock.writes.size() == 2, "snapshot update must write every binding once"); const auto& write = mock.writes.front(); const auto& untouched = mock.writes.back(); Require(untouched.binding == 4 && untouched.count == 1 && untouched.buffers[0].buffer == originalThird[0].buffer && untouched.buffers[0].offset == originalThird[0].offset && untouched.buffers[0].range == originalThird[0].range, "snapshot update changed a binding without snapshots"); Require(write.binding == 0 && write.arrayElement == 0 && write.count == 2, "snapshot update did not start from array element zero"); std::size_t snapshot = 0; for (std::size_t index = 0; index < 2; ++index) { const auto& descriptor = write.buffers[index]; if ((selected & (1u << index)) == 0) { Require(descriptor.buffer == original[index].buffer && descriptor.offset == original[index].offset && descriptor.range == original[index].range, "snapshot update changed an untouched buffer descriptor"); } else { const auto& item = snapshots->snapshots[snapshot]; Require(descriptor.buffer == item.buffer->Handle() && descriptor.offset == 0 && descriptor.range == moved[snapshot].size, "snapshot descriptor used another element's buffer or range"); Require(sameBytes(bufferBytes(descriptor.buffer), moved[snapshot].words.data(), moved[snapshot].size), "snapshot descriptor lost its buffer data"); ++snapshot; } } recorder.Sync(); } Require(mock.live == 0, "snapshot descriptor resources leaked Vulkan objects"); } } void resourceTests() { mock = MockVulkan{}; const auto context = mockContext(); const auto state = AgcDriver::Graphics::DecodeState(makeState()); const auto commands = reinterpret_cast(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, ShaderDataWords({7, 8, 9}))); vertex.shaderDataDwords = ShaderRecompiler::RuntimeAbi::ShaderDataDwords; 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 auto data = ShaderDataWords({7, 8, 9}); Require(findWrite(5).buffers.size() == 1 && findWrite(5).buffers[0].range == data.size() * 4u && sameBytes(bufferBytes(findWrite(5).buffers[0].buffer), data.data(), data.size() * 4u), "shader data buffer is incorrect"); const std::array 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 = mockBufferMemory(array.buffers[0].buffer); std::memset(first.data(), 0xab, 16); auto shaderData = mockBufferMemory(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(mockBufferMemory(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"); mock = MockVulkan{}; { auto descriptor = vsharp(guestSecond.data(), 2); descriptor[1] |= 16u << 16u; descriptor[3] = 0x0004dfacu; ShaderRecompiler::RecompileResult compute; compute.bindings.push_back(makeBinding(Role::GuestBuffers, 3, 1, descriptor)); const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &compute, 0}; AgcDriver::Graphics::ShaderResources resources(context, shader); const auto& buffer = findWrite(3).buffers.at(0); Require(buffer.range == sizeof(guestSecond), "strided buffer range does not cover every record"); Require(sameBytes(bufferBytes(buffer.buffer), guestSecond.data(), sizeof(guestSecond)), "strided buffer contents were not uploaded"); const std::uint32_t changed = 0x12345678u; auto bytes = mockBufferMemory(buffer.buffer); std::memcpy(bytes.data() + 16, &changed, sizeof(changed)); resources.WriteBack(); Require(guestSecond[4] == changed && guestSecond[0] == 1 && guestSecond[7] == 8, "strided buffer write back changed the wrong record"); guestSecond[4] = 5; } Require(mock.live == 0, "strided buffer 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; binding.binding = 1u; }), "guest texture descriptor must contain 8 dwords"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::StorageImage; binding.binding = 29u; }), "guest storage image descriptors must contain 8 dwords"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestSamplers; binding.kind = Kind::Sampler; binding.binding = static_cast(ShaderRecompiler::RuntimeAbi::Binding::Samplers); }), "shader sampler descriptors exceed per-stage limits"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::SampledImage; binding.binding = 29u; }), "resource class disagrees"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::Gds; }), "invalid GDS descriptor contract"); 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::ShaderData; }), "invalid compact shader data size"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::ShaderData; binding.guestDescriptor = ShaderDataWords({}); binding.guestDescriptor.pop_back(); }), "invalid compact shader data size"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::FlattenedSrt; binding.guestDescriptor.clear(); }), "empty shader data descriptor"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x40000000u; }), "reserved bits"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[3] |= 0x40000000u; }), "unsupported type"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x3fffu << 16u; binding.guestDescriptor[2] = 0xffffffffu; }), "descriptor range limit"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[2] = 32768; }), "descriptor range limit"); expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(0x1000), 8); }), "an unmapped V#"); expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(state.color.address), 64); }), "aliases the render target"); expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast(0x1000), 8)); }), "an unmapped V# element"); expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast(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; std::vector words; for (int index = 0; index < 17; ++index) words = join(words, vsharp(reinterpret_cast(0x1000), 8)); vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 17, words)); auto updateAfterBind = mockContext(); updateAfterBind.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindStorageBuffers = 17; updateAfterBind.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 128; updateAfterBind.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 32; mock = MockVulkan{}; { AgcDriver::Graphics::ShaderResources resources(updateAfterBind, vertex, fragment, state.color, 0, 0); Require(mock.layoutFlags == VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT, "buffers above the per-stage limit did not get an update-after-bind layout"); Require(mock.layoutBindingFlags.size() == 1 && mock.layoutBindingFlags[0] == VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT, "the guest buffers binding is not update-after-bind"); Require(mock.poolFlags == VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT, "the update-after-bind set did not come from an update-after-bind pool"); } Require(mock.live == 0, "update-after-bind shader resources leaked Vulkan objects"); updateAfterBind.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindStorageBuffers = 16; expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(updateAfterBind, vertex, fragment, state.color, 0, 0); }, "shader descriptors exceed per-stage limits"); Require(mock.live == 0, "failed update-after-bind shader resources leaked Vulkan objects"); mock = MockVulkan{}; vertex.bindings.front() = makeBinding(Role::GuestBuffers, 0, 1, vsharp(reinterpret_cast(0x1000), 8)); { AgcDriver::Graphics::ShaderResources resources(updateAfterBind, vertex, fragment, state.color, 0, 0); } Require(mock.layoutFlags == 0 && mock.layoutBindingFlags.empty() && mock.poolFlags == 0, "buffers within the per-stage limit took the update-after-bind path"); } { 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::GuestImages, 1, 1, std::vector(8, 0))); const auto expectStageResources = [&](Role role, Kind kind, std::uint32_t words, std::uint32_t limit, std::string_view reason) { vertex.bindings.back().role = role; vertex.bindings.back().kind = kind; vertex.bindings.back().binding = kind == Kind::StorageImage ? ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding : kind == Kind::Sampler ? static_cast(ShaderRecompiler::RuntimeAbi::Binding::Samplers) : ShaderRecompiler::RuntimeAbi::FirstImageBinding; vertex.bindings.back().guestDescriptor.assign(words, 0); mock = MockVulkan{}; auto limited = mockContext(); limited.limits.maxPerStageResources = limit; expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(limited, vertex, fragment, state.color, 0, 0); }, reason); Require(mock.live == 0, "failed shader resources leaked Vulkan objects"); }; expectStageResources(Role::GuestImages, Kind::SampledImage, 8, 2, "shader descriptors exceed per-stage limits"); expectStageResources(Role::GuestImages, Kind::StorageImage, 8, 2, "shader descriptors exceed per-stage limits"); expectStageResources(Role::GuestImages, Kind::SampledImage, 8, 3, "missing an image shape"); expectStageResources(Role::GuestImages, Kind::StorageImage, 8, 3, "detiler is unavailable"); expectStageResources(Role::GuestSamplers, Kind::Sampler, 4, 2, "shader sampler descriptors exceed per-stage limits"); } { ShaderRecompiler::RecompileResult vertex; ShaderRecompiler::RecompileResult fragment; fragment.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)))); const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; auto limited = mockContext(); limited.limits.maxPerStageResources = 2; const auto expectAttachments = [&](std::uint32_t attachments, bool accepted, std::string_view what) { mock = MockVulkan{}; if (accepted) { AgcDriver::Graphics::ShaderResources resources(limited, shaders, state.color, attachments, 0, 0); } else { expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(limited, shaders, state.color, attachments, 0, 0); }, "shader descriptors exceed per-stage limits"); } Require(mock.live == 0, std::string(what) + " leaked Vulkan objects"); }; expectAttachments(0, true, "a pixel stage at the per-stage limit"); expectAttachments(1, false, "a pixel stage over the per-stage limit with its colour attachment"); std::swap(vertex.bindings, fragment.bindings); expectAttachments(1, true, "a vertex stage at the per-stage limit"); expectAttachments(3, false, "colour attachments over the per-stage limit"); } { 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); } } void descriptorCacheTests() { mock = MockVulkan{}; auto context = mockContext(); context.limits.maxDescriptorSetStorageBuffers = 8192; context.limits.maxPerStageDescriptorStorageBuffers = 8192; context.limits.maxPerStageResources = 8192; context.limits.maxDescriptorSetSampledImages = 2048; { AgcDriver::Graphics::DescriptorCache cache(context); const auto layout = [&](std::uint32_t count) { const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, count, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}; const std::array key{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, count, VK_SHADER_STAGE_FRAGMENT_BIT}; return cache.Layout(key, std::span(&binding, 1)); }; const auto large = layout(4097); const auto live = mock.live; const std::array oversized{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4097}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 3}}}; const auto dedicated = cache.Allocate(large, oversized); Require(dedicated.set != VK_NULL_HANDLE && dedicated.pool != VK_NULL_HANDLE, "a set above the chain pool's capacity got no set"); Require(mock.live == live + 1 && mock.poolMaxSets == 1 && mock.poolFlags == 0, "an oversized set does not get a pool of its own"); Require(mock.poolSizes.size() == 2 && mock.poolSizes[0].type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER && mock.poolSizes[0].descriptorCount == 4097 && mock.poolSizes[1].type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE && mock.poolSizes[1].descriptorCount == 3, "the dedicated pool is not sized to its set"); Require(cache.Counters().pools == 0 && cache.Counters().sets == 1, "an oversized set was counted in the chain pools"); cache.Free(dedicated); Require(mock.live == live, "freeing an oversized set did not destroy its pool"); const std::array fitting{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4096}}}; const auto chained = cache.Allocate(layout(4096), fitting); Require(chained.set != VK_NULL_HANDLE && mock.poolMaxSets == 1024 && mock.poolFlags == 0 && cache.Counters().pools == 1, "a set the chain pool holds left the chain"); cache.Free(chained); Require(mock.descriptorPools.at(chained.pool).resets == 1 && mock.live == live + 2, "a chain pool was not reset after its last set"); const std::array regular{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4096}}}; const auto first = cache.Allocate(layout(4096), regular); const auto oversizedMixed = cache.Allocate(large, oversized); const auto second = cache.Allocate(layout(4096), regular); Require(first.pool == chained.pool && second.pool == chained.pool && oversizedMixed.pool != chained.pool && mock.descriptorPools.at(chained.pool).resets == 1, "an oversized set disturbed the chain pool"); cache.Free(oversizedMixed); Require(mock.live == live + 2 && mock.descriptorPools.count(oversizedMixed.pool) == 0 && mock.descriptorPools.at(chained.pool).resets == 1, "freeing an oversized set between chain sets touched the chain pool"); cache.Free(first); Require(mock.descriptorPools.at(chained.pool).resets == 1, "a chain pool reset while a set was live"); cache.Free(second); Require(mock.descriptorPools.at(chained.pool).resets == 2, "a chain pool was not reset after the oversized set was freed"); const std::array beyondDevice{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 8193}}}; expectFailure([&] { cache.Allocate(large, beyondDevice); }, "descriptor set exceeds the device's per-set descriptor limit"); const std::array storageImages{{{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1}}}; expectFailure([&] { cache.Allocate(large, storageImages); }, "descriptor set exceeds the device's per-set descriptor limit"); const std::array uniformBuffers{{{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1}}}; expectFailure([&] { cache.Allocate(large, uniformBuffers); }, "descriptor set uses an unsupported descriptor type 6"); } Require(mock.live == 0, "the descriptor cache leaked a pool or layout"); } void textureCacheBudgetTests() { using AgcDriver::Graphics::TextureCacheBudget; constexpr std::uint64_t GiB = 1ull << 30u; VkPhysicalDeviceMemoryProperties memory{}; Require(TextureCacheBudget(memory) == 2 * GiB, "a device without memory heaps does not keep 2 GiB of cached textures"); memory.memoryHeapCount = 3; memory.memoryHeaps[0] = {256ull << 20u, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; memory.memoryHeaps[1] = {32 * GiB, 0}; memory.memoryHeaps[2] = {16 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT | VK_MEMORY_HEAP_MULTI_INSTANCE_BIT}; Require(TextureCacheBudget(memory) == 4 * GiB, "a 16 GiB device-local heap does not give 4 GiB of cached textures"); memory.memoryHeaps[2].size = 6 * GiB; Require(TextureCacheBudget(memory) == 2 * GiB, "a 6 GiB device-local heap does not keep the 2 GiB floor"); memory.memoryHeaps[2].size = 24 * GiB; memory.memoryHeapCount = 2; Require(TextureCacheBudget(memory) == 2 * GiB, "a heap past memoryHeapCount or a host heap counted toward the texture caches"); memory.memoryHeapCount = 3; Require(TextureCacheBudget(memory) == 6 * GiB, "a 24 GiB device-local heap does not give 6 GiB of cached textures"); } void sampledTextureBudgetTests() { using AgcDriver::Graphics::SampledTextureBudget; constexpr std::uint64_t GiB = 1ull << 30u; constexpr std::uint64_t MiB = 1ull << 20u; VkPhysicalDeviceMemoryProperties memory{}; memory.memoryHeapCount = 3; memory.memoryHeaps[0] = {256 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; memory.memoryHeaps[1] = {32 * GiB, 0}; memory.memoryHeaps[2] = {16 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; Require(SampledTextureBudget(memory, nullptr, 0) == 4 * GiB, "without VK_EXT_memory_budget the sampled texture cache does not keep a quarter of the 16 GiB heap"); reported.heapBudget[0] = 100 * GiB; reported.heapBudget[1] = 100 * GiB; Require(SampledTextureBudget(memory, &reported, 0) == 4 * GiB, "a zero budget for the largest device-local heap did not fall back to a quarter of the heap"); reported.heapBudget[2] = 15 * GiB; reported.heapUsage[2] = 9 * GiB; Require(SampledTextureBudget(memory, &reported, 5 * GiB) == 5 * GiB + 128 * MiB, "a 15 GiB budget with 4 GiB used outside the texture caches does not leave 5 GiB 128 MiB after the 4 GiB storage cache and 1 GiB 896 MiB of headroom"); reported.heapUsage[2] = 3 * GiB; Require(SampledTextureBudget(memory, &reported, 5 * GiB) == 9 * GiB + 128 * MiB, "usage below the cached bytes is not read as no other device memory in use"); reported.heapUsage[2] = 12 * GiB; Require(SampledTextureBudget(memory, &reported, 1 * GiB) == 2 * GiB, "a heap whose other users leave no room does not keep the 2 GiB floor"); memory.memoryHeaps[0].flags = 0; memory.memoryHeaps[2].flags = 0; Require(SampledTextureBudget(memory, &reported, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of sampled textures"); } void sampledBudgetReportTests() { using AgcDriver::Graphics::SampledBudgetReportDue; using std::chrono::seconds; constexpr std::uint64_t MiB = 1ull << 20u; Require(SampledBudgetReportDue(0, 2048 * MiB, seconds(10)), "a budget never reported is not due"); Require(!SampledBudgetReportDue(4000 * MiB, 4400 * MiB, seconds(60)), "a budget 10% above the reported one is due, not only one more than 10% away"); Require(!SampledBudgetReportDue(4000 * MiB, 3600 * MiB, seconds(60)), "a budget 10% below the reported one is due, not only one more than 10% away"); Require(SampledBudgetReportDue(4000 * MiB, 4401 * MiB, seconds(10)), "a budget more than 10% above the reported one is not due after 10 s"); Require(SampledBudgetReportDue(4000 * MiB, 3599 * MiB, seconds(10)), "a budget more than 10% below the reported one is not due after 10 s"); Require(!SampledBudgetReportDue(4000 * MiB, 2048 * MiB, seconds(9)), "a budget change is reported again within 10 s of the last report"); } void misalignedShaderDataTests() { mock = MockVulkan{}; auto context = mockContext(); context.limits.minStorageBufferOffsetAlignment = 16; const auto commands = reinterpret_cast(std::uintptr_t{1}); alignas(64) std::array guest{1, 2, 3, 4, 5, 6, 7, 8}; { ShaderRecompiler::RecompileResult compute; compute.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guest.data(), 32), vsharp(guest.data() + 1, 8)))); compute.bindings.push_back(makeBinding(Role::ShaderData, 1, 1, ShaderDataWords({0x11, 0x22}))); compute.shaderDataDwords = ShaderRecompiler::RuntimeAbi::ShaderDataDwords; compute.memoryOffsetDword = ShaderRecompiler::RuntimeAbi::BufferOffsetsDword; auto live = compute; live.bindings[1].guestDescriptor = ShaderDataWords({0x33, 0x44}); const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &compute, 0}; const AgcDriver::Graphics::CompiledShader liveShader{ShaderRecompiler::ShaderStage::Compute, &live, 0}; AgcDriver::Graphics::ShaderResources resources(context, shader); const auto& views = findWrite(0); Require(views.buffers.size() == 2 && views.buffers[0].range == 32 && views.buffers[1].range == 12 && views.buffers[1].offset == views.buffers[0].offset, "the misaligned view is not bound from the aligned offset below it"); const auto data = findWrite(1).buffers.at(0).buffer; auto patched = ShaderDataWords({0x11, 0x22}); patched[ShaderRecompiler::RuntimeAbi::BufferOffsetsDword] = 0x400u; Require(sameBytes(bufferBytes(data), patched.data(), patched.size() * 4u), "the shader data buffer does not hold the misaligned view's offset"); Require(resources.RefreshData(commands, liveShader), "a refresh with different words recorded nothing"); auto refreshed = ShaderDataWords({0x33, 0x44}); refreshed[ShaderRecompiler::RuntimeAbi::BufferOffsetsDword] = 0x400u; Require(sameBytes(bufferBytes(data), refreshed.data(), refreshed.size() * 4u), "a data refresh dropped the misaligned view's offset"); Require(!resources.DataWordsDiffer(liveShader) && resources.DataWordsHash() == AgcDriver::Graphics::ShaderResources::DataWordsHash(liveShader), "the refreshed template's words are not the dispatch's"); } Require(mock.live == 0, "misaligned shader data resources leaked Vulkan objects"); } struct ModuleShape { bool fragment = false; std::optional fragmentMode; 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; std::uint32_t parameterLocation = 0; bool rectParameters = false; bool secondTarget = false; bool sampleId = false; bool layer = false; bool fragDepth = false; std::uint32_t sampleMaskLength = 0; std::uint32_t floatControlsWidth = 0; }; void emit(std::vector& out, spv::Op op, std::initializer_list operands) { out.push_back((static_cast(operands.size() + 1) << 16u) | static_cast(op)); out.insert(out.end(), operands.begin(), operands.end()); } std::vector makeModule(const ModuleShape& shape) { std::vector annotations; std::vector declarations; std::vector 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 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, shape.parameterLocation}); extraInterface.push_back(parameter); } if (shape.secondTarget) { const auto target = id(); emit(declarations, spv::OpVariable, {outputPointer, target, spv::StorageClassOutput}); emit(annotations, spv::OpDecorate, {target, spv::DecorationLocation, 1}); extraInterface.push_back(target); } if (shape.rectParameters) { const auto pointer = id(); emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassInput, vectorType}); for (std::uint32_t location = 0; location < 2; ++location) { const auto parameter = id(); emit(declarations, spv::OpVariable, {pointer, parameter, spv::StorageClassInput}); emit(annotations, spv::OpDecorate, {parameter, spv::DecorationLocation, location}); if (location == 1) emit(annotations, spv::OpDecorate, {parameter, spv::DecorationFlat}); 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.sampleId || shape.layer) { const auto intType = id(); const auto pointer = id(); emit(declarations, spv::OpTypeInt, {intType, 32, 1}); emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassInput, intType}); for (const auto [wanted, builtin] : {std::pair{shape.sampleId, spv::BuiltInSampleId}, std::pair{shape.layer, spv::BuiltInLayer}}) { if (!wanted) continue; const auto variable = id(); emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassInput}); emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, static_cast(builtin)}); emit(annotations, spv::OpDecorate, {variable, spv::DecorationFlat}); extraInterface.push_back(variable); } } if (shape.fragDepth) { const auto pointer = id(); const auto variable = id(); emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, floatType}); emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassOutput}); emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, spv::BuiltInFragDepth}); extraInterface.push_back(variable); } if (shape.sampleMaskLength != 0) { const auto length = id(); const auto array = id(); const auto pointer = id(); const auto variable = id(); emit(declarations, spv::OpConstant, {uintType, length, shape.sampleMaskLength}); emit(declarations, spv::OpTypeArray, {array, uintType, length}); emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, array}); emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassOutput}); emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, spv::BuiltInSampleMask}); 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 words{spv::MagicNumber, 0x10300, 0, next, 0}; emit(words, spv::OpCapability, {spv::CapabilityShader}); if (shape.sampleId) emit(words, spv::OpCapability, {spv::CapabilitySampleRateShading}); if (shape.layer) emit(words, spv::OpCapability, {spv::CapabilityGeometry}); 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(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(static_cast(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(extraInterface.size()) << 16u; words.insert(words.end(), extraInterface.begin(), extraInterface.end()); if (shape.fragmentMode) emit(words, spv::OpExecutionMode, {main, *shape.fragmentMode}); if (shape.fragment) emit(words, spv::OpExecutionMode, {main, spv::ExecutionModeOriginUpperLeft}); if (shape.fragDepth) emit(words, spv::OpExecutionMode, {main, spv::ExecutionModeDepthReplacing}); if (shape.floatControlsWidth != 0) { for (const auto mode : {spv::ExecutionModeRoundingModeRTE, spv::ExecutionModeDenormPreserve, spv::ExecutionModeSignedZeroInfNanPreserve}) emit(words, spv::OpExecutionMode, {main, mode, shape.floatControlsWidth}); } 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 barycentricEmulationTests() { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; const std::array parameters{{{0u, 0u, false, false}, {1u, 1u, false, true}, {2u, 2u, true, false}}}; const auto layout = LayoutBarycentricEmulation(parameters, {true, true, true}); Require(layout.smoothLocation == 1u && layout.linearLocation == 3u && layout.perVertexLocations == std::vector>{{1u, 4u}}, "emulated barycentrics did not take the free locations in order"); Require(LayoutBarycentricEmulation(parameters, {}).perVertexLocations.empty() && LayoutBarycentricEmulation(parameters, {}).smoothLocation == BarycentricEmulationLayout::NoLocation, "an inactive emulation was given locations"); std::vector crowded; for (std::uint32_t location = 0; location < 11u; ++location) crowded.push_back({location, location, false, true}); expectFailure([&] { static_cast(LayoutBarycentricEmulation(crowded, {true, false, false})); }, "exceed 32 locations"); const auto compile = [&](std::span code, bool barycentric, std::uint32_t secondInput = 0xffffffffu) { auto queue = makeState(); queue.context[0x1b3] = 0x2u; queue.context[0x1b4] = 0x2u; queue.context[0x1b6] = secondInput == 0xffffffffu ? 1u : 2u; queue.context[0x191] = 0u; if (secondInput != 0xffffffffu) queue.context[0x192] = secondInput; RecompileRequest request{}; request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; request.context.waveSize = 64; request.context.pixel = DecodePixelStageInfo(queue.context, IdentityExports); request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; request.target.fragmentShaderBarycentricEnabled = barycentric; request.layout.pushConstantSizeBytes = 128; request.useCache = false; return Recompile(request); }; const auto decorations = [](const RecompileResult& result, spv::Decoration decoration) { const auto& words = result.spirv.Words(); std::set inputs; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { if (static_cast(words[at] & 0xffffu) == spv::OpVariable && words[at + 3] == spv::StorageClassInput) inputs.insert(words[at + 2]); } std::vector values; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { if (static_cast(words[at] & 0xffffu) == spv::OpDecorate && words[at + 2] == decoration && inputs.contains(words[at + 1])) values.push_back((words[at] >> 16u) > 3u ? words[at + 3] : words[at + 1]); } return values; }; const auto barycentricCapability = [](const RecompileResult& result) { const auto& words = result.spirv.Words(); for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { if (static_cast(words[at] & 0xffffu) == spv::OpCapability && words[at + 1] == spv::CapabilityFragmentBarycentricKHR) return true; } return false; }; static constexpr std::array readsIj{0xf800180fu, 0x01000100u, 0xbf810000u}; static constexpr std::array interpolates{0xc8080000u, 0xc8090001u, 0xf800180fu, 0x02020202u, 0xbf810000u}; static constexpr std::array readsVertex{0xc80a0000u, 0xf800180fu, 0x02020202u, 0xbf810000u}; static constexpr std::array shiftsIj{0x060000f0u, 0xc8080000u, 0xc8090001u, 0xf800180fu, 0x02020202u, 0xbf810000u}; auto fragment = compile(readsIj, false); Require(fragment.barycentricEmulation.active && fragment.barycentricEmulation.smooth && !fragment.barycentricEmulation.linear, "reading the I/J VGPRs without the extension did not emulate barycentrics"); const auto builtins = decorations(fragment, spv::DecorationBuiltIn); Require(std::find(builtins.begin(), builtins.end(), static_cast(spv::BuiltInBaryCoordKHR)) == builtins.end() && !barycentricCapability(fragment), "emulated barycentrics still need VK_KHR_fragment_shader_barycentric"); Require(decorations(fragment, spv::DecorationLocation) == std::vector{0u}, "the emulated barycentrics were not read from location 0"); Require(!compile(readsIj, true).barycentricEmulation.active, "a device with the extension emulated barycentrics"); Require(!compile(interpolates, false).barycentricEmulation.active, "plain v_interp_p1/p2 lost host interpolation"); Require(compile(shiftsIj, false).barycentricEmulation.active, "v_interp_p1/p2 through a changed I lost exact barycentrics"); static constexpr std::array overwritesI{0xc8080000u, 0xc8090001u, 0x7e0002f2u, 0xf800180fu, 0x02020002u, 0xbf810000u}; static constexpr std::array accumulatesInI{0xc8000000u, 0xc8010001u, 0xf800180fu, 0x00000000u, 0xbf810000u}; for (const auto& code : {std::span(overwritesI), std::span(accumulatesInI)}) { const auto reused = compile(code, false); const auto reusedBuiltins = decorations(reused, spv::DecorationBuiltIn); Require(!reused.barycentricEmulation.active && !barycentricCapability(reused) && std::find(reusedBuiltins.begin(), reusedBuiltins.end(), static_cast(spv::BuiltInBaryCoordKHR)) == reusedBuiltins.end(), "reusing the I/J VGPRs after v_interp_p1/p2 lost host interpolation"); } static constexpr std::array widensExec{0xbefe0381u, 0xc8080000u, 0xbefe03c1u, 0xc8090001u, 0xf800180fu, 0x02020202u, 0xbf810000u}; Require(compile(widensExec, false).barycentricEmulation.active, "v_interp_p2_f32 under a wider EXEC than its v_interp_p1_f32 lost exact barycentrics"); static constexpr std::array sharedSlot{0xc8080000u, 0xc8090001u, 0xc80e0402u, 0xf800180fu, 0x03030202u, 0xbf810000u}; const auto shared = compile(sharedSlot, false, 0x400u); Require(shared.barycentricEmulation.active && !barycentricCapability(shared), "inputs sharing a slot with different interpolation did not emulate per-vertex parameters"); fragment = compile(readsVertex, false); Require(fragment.barycentricEmulation.active && !fragment.barycentricEmulation.smooth && fragment.fragmentParameters.size() == 1u && fragment.fragmentParameters[0].perVertex, "v_interp_mov of P10 did not emulate per-vertex parameters"); Require(decorations(fragment, spv::DecorationLocation) == std::vector{0u} && decorations(fragment, spv::DecorationPerVertexKHR).empty() && !decorations(fragment, spv::DecorationFlat).empty() && !barycentricCapability(fragment), "the emulated per-vertex parameter was not a flat array at the first free location"); RecompileResult vertex; vertex.spirv = makeModule({.parameterOutput = true}); vertex.parameterExports = {0u}; const std::array capabilities{spv::CapabilityShader, spv::CapabilityGeometry}; SpirvTarget target{}; target.vulkanVersion = VK_API_VERSION_1_1; target.spirvVersion = 0x00010300u; target.supportedCapabilities = capabilities; const GeometryStageLimits limits{64u, 128u, 256u, 1024u, 128u}; auto geometry = BuildBarycentricGeometryShader(vertex, fragment, target, limits); const std::array shaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Geometry, &geometry, 0}, {ShaderStage::Fragment, &fragment, 0}}}; const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; auto state = DecodeState(makeState()); Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "the reference state is not a triangle list"); ValidateShaders(shaders, state, subgroup, false, false, false, true); state.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; expectFailure([&] { ValidateShaders(shaders, state, subgroup, false, false, false, true); }, "triangle primitives"); state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; ValidateShaders(shaders, state, subgroup, false, false, false, true); const auto plain = compile(interpolates, false); const std::array plainShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Geometry, &geometry, 0}, {ShaderStage::Fragment, &plain, 0}}}; expectFailure([&] { ValidateShaders(plainShaders, state, subgroup, false, false, false, true); }, "only for emulated barycentrics"); expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, plain, target, limits)); }, "does not emulate barycentrics"); expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, std::nullopt)); }, "geometry shaders are unavailable"); expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, GeometryStageLimits{64u, 12u, 256u, 1024u, 128u})); }, "exceeds device limits"); vertex.parameterExports.clear(); expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, limits)); }, "not exported by the vertex shader"); } void rectListTests() { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; RecompileResult vertex; vertex.spirv = makeModule({}); RecompileResult fragment; fragment.spirv = makeModule({.fragment = true}); const std::array capabilities{spv::CapabilityShader, spv::CapabilityTessellation}; SpirvTarget target{}; target.vulkanVersion = VK_API_VERSION_1_1; target.spirvVersion = 0x00010300u; target.supportedCapabilities = capabilities; target.tessellation = TessellationTargetLimits{32, 128, 128, 120, 4096, 128, 128}; vertex.bindings = {{DescriptorKind::StorageBuffer, DescriptorRole::GuestBuffers, 0, 0, 1, {}}}; const auto faultAuxiliary = BuildRectListShaders(vertex, fragment, target); Require(faultAuxiliary.control.bindings.size() == 1 && faultAuxiliary.control.bindings[0].role == DescriptorRole::FaultBuffer && faultAuxiliary.control.bindings[0].binding == RuntimeAbi::BindingNumber(RuntimeAbi::Stage::TessellationControl, RuntimeAbi::Binding::FaultBuffer), "the rect-list control shader's fault buffer is not in the tessellation control binding group"); vertex.bindings.clear(); for (const auto version : {0x00010300u, 0x00010400u}) { target.spirvVersion = version; auto auxiliary = BuildRectListShaders(vertex, fragment, target); const std::array shaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &auxiliary.control, 0}, {ShaderStage::TessellationEvaluation, &auxiliary.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; for (const auto primitive : {7u, 17u}) { auto queue = makeState(); queue.userConfig[0x242] = primitive; queue.context[0x205] = 3; auto state = DecodeState(queue); Require(state.rectList && state.topology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST && state.cullMode == VK_CULL_MODE_NONE, "rect-list state was not decoded"); Require(!state.stages.tessellation && state.stages.path == ShaderPath::Vertex, "rect-list changed guest shader routing"); ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); state.rectList = false; expectFailure([&] { ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); }, "stage count"); } } vertex.spirv = makeModule({.parameterOutput = true}); fragment.spirv = makeModule({.fragment = true, .rectParameters = true}); vertex.parameterExports = {0}; fragment.fragmentParameters = {{0, 0, false, false}, {1, 0, true, false}}; auto auxiliary = BuildRectListShaders(vertex, fragment, target); Require(!auxiliary.control.spirv.empty() && !auxiliary.evaluation.spirv.empty(), "rect-list parameter shaders are empty"); auto parameterQueue = makeState(); parameterQueue.userConfig[0x242] = 17; const auto parameterState = DecodeState(parameterQueue); const std::array parameterShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &auxiliary.control, 0}, {ShaderStage::TessellationEvaluation, &auxiliary.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; ValidateShaders(parameterShaders, parameterState, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); fragment.fragmentParameters[0].perVertex = true; auto explicitInterpolation = BuildRectListShaders(vertex, fragment, target); Require(!explicitInterpolation.control.spirv.empty() && !explicitInterpolation.evaluation.spirv.empty(), "rect-list shaders for an explicitly interpolated parameter are empty"); fragment.fragmentParameters[0].custom = true; expectFailure([&] { static_cast(BuildRectListShaders(vertex, fragment, target)); }, "per-vertex interpolation"); fragment.fragmentParameters[0].perVertex = false; fragment.fragmentParameters[0].custom = false; vertex.parameterExports.clear(); auto unexported = BuildRectListShaders(vertex, fragment, target); Require(!unexported.control.spirv.empty() && !unexported.evaluation.spirv.empty(), "rect-list shaders with an unexported parameter are empty"); fragment.fragmentParameters.clear(); target.tessellation->maxPatchSize = 3; expectFailure([&] { static_cast(BuildRectListShaders(vertex, fragment, target)); }, "device limits"); target.tessellation.reset(); expectFailure([&] { static_cast(BuildRectListShaders(vertex, fragment, target)); }, "unavailable"); auto queue = makeState(); queue.userConfig[0x242] = 17; const auto state = DecodeState(queue); const Context context{}; std::array indices{}; const auto drawFailure = [&](std::uint32_t count, bool indexed) { const AgcDriver::Pm4::DrawParameters draw = indexed ? AgcDriver::Pm4::DrawParameters{reinterpret_cast(indices.data()), count, 2, 1, 0, true} : AgcDriver::Pm4::DrawParameters{0, count, 0, 1, 0, false}; try { Draw(context, state, draw, {}); } catch (const std::exception& error) { return std::string(error.what()); } return std::string{}; }; for (const auto indexed : {false, true}) { const auto complete = drawFailure(3, indexed); for (const auto count : {4u, 5u}) Require(drawFailure(count, indexed) == complete, "a rect-list draw with a partial last rectangle was not drawn like its complete rectangles"); } } ShaderRecompiler::RecompileResult recompilePixel(std::initializer_list controls, std::span code) { auto queue = makeState(); queue.context[0x1b3] = 0x2u; queue.context[0x1b4] = 0x2u; queue.context[0x1b6] = static_cast(controls.size()); std::uint32_t index = 0; for (const auto control : controls) queue.context[0x191 + index++] = control; const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); ShaderRecompiler::RecompileRequest request{}; request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; request.context.waveSize = 64; request.context.pixel = pixel; request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; request.target.fragmentShaderBarycentricEnabled = true; request.layout.pushConstantSizeBytes = 128; request.useCache = false; return ShaderRecompiler::Recompile(request); } struct LocatedInput { std::uint32_t location; bool flat; bool perVertex; }; std::vector locatedInputs(std::span words) { std::map decorated; std::set inputs; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { const auto op = static_cast(words[at] & 0xffffu); if (op == spv::OpVariable && words[at + 3] == spv::StorageClassInput) inputs.insert(words[at + 2]); if (op != spv::OpDecorate) continue; auto& input = decorated[words[at + 1]]; if (words[at + 2] == spv::DecorationLocation) input.location = words[at + 3] + 1u; if (words[at + 2] == spv::DecorationFlat) input.flat = true; if (words[at + 2] == spv::DecorationPerVertexKHR) input.perVertex = true; } std::vector result; for (const auto& [id, input] : decorated) { if (input.location != 0 && inputs.contains(id)) result.push_back({input.location - 1u, input.flat, input.perVertex}); } std::sort(result.begin(), result.end(), [](const auto& a, const auto& b) { return a.location < b.location; }); return result; } constexpr std::array DepthExportCode{0xf8001881u, 2u, 0xbf810000u}; AgcDriver::QueueState depthExportQueue(std::uint32_t shaderControl) { auto queue = makeState(); queue.context[0x1b3] = 0x402u; queue.context[0x1b4] = 0x402u; queue.context[0x203] = shaderControl; return queue; } ShaderRecompiler::RecompileRequest depthExportRequest(std::uint32_t shaderControl) { const auto queue = depthExportQueue(shaderControl); ShaderRecompiler::RecompileRequest request{}; request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, DepthExportCode, 0, {}}; request.context.waveSize = 64; request.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; request.layout.pushConstantSizeBytes = 128; request.useCache = false; return request; } std::set depthModes(const ShaderRecompiler::RecompileRequest& request) { const auto result = ShaderRecompiler::Recompile(request); const auto& words = result.spirv.Words(); std::set modes; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { if (static_cast(words[at] & 0xffffu) != spv::OpExecutionMode) continue; const auto mode = words[at + 2]; if (mode == spv::ExecutionModeDepthReplacing || mode == spv::ExecutionModeDepthGreater || mode == spv::ExecutionModeDepthLess || mode == spv::ExecutionModeDepthUnchanged) modes.insert(mode); } return modes; } void conservativeZExportTests() { const std::array>, 4> cases{{ {0x0021u, {spv::ExecutionModeDepthReplacing}}, {0x2021u, {spv::ExecutionModeDepthReplacing, spv::ExecutionModeDepthLess}}, {0x4021u, {spv::ExecutionModeDepthReplacing, spv::ExecutionModeDepthGreater}}, {0x2020u, {}} }}; const ShaderRecompiler::RequestSerializer serializer; std::set> keys; for (const auto& [shaderControl, expected] : cases) { const auto request = depthExportRequest(shaderControl); const auto replayed = serializer.Deserialize(serializer.Serialize(request)); Require(depthModes(request) == expected && depthModes(replayed.request) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " declared the wrong depth execution modes"); std::vector key; ShaderRecompiler::RecompileCacheKey::Build(request, key); keys.insert(key); } Require(keys.size() == cases.size(), "the recompile cache key ignores CONSERVATIVE_Z_EXPORT"); for (const auto shaderControl : {0x2020u, 0x4020u}) { const auto queue = depthExportQueue(shaderControl); static_cast(AgcDriver::Graphics::DecodeState(queue)); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DB_SHADER_CONTROL " + std::to_string(shaderControl)); } expectFailure([] { static_cast(depthExportRequest(0x6021u)); }, "CONSERVATIVE_Z_EXPORT"); AgcDriver::Graphics::State state{}; state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({}); ShaderRecompiler::RecompileResult pixel; const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}}; const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; for (const auto mode : {spv::ExecutionModeDepthLess, spv::ExecutionModeDepthGreater}) { pixel.spirv = makeModule({.fragment = true, .fragmentMode = mode}); AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); } pixel.spirv = makeModule({.fragment = true, .fragmentMode = spv::ExecutionModeDepthUnchanged}); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment execution mode"); } constexpr std::array OrderedPixelCode{0x7e0e02f2u, 0xbf900007u, 0xf800180fu, 0x07070707u, 0xbf810000u}; constexpr std::array OrderedBdaPixelCode{0x7e040f00u, 0x7e060280u, 0xdc208001u, 0x047d0002u, 0xdc308008u, 0x057d0002u, 0xbf8c3f70u, 0x4a080b04u, 0x7e0e0d04u, 0xf800180fu, 0x07070707u, 0xbf810000u}; AgcDriver::QueueState orderedPixelQueue(std::uint32_t shaderControl) { auto queue = makeState(); queue.context[0x1b3] = 0x2u; queue.context[0x1b4] = 0x2u; queue.context[0x203] = shaderControl; return queue; } ShaderRecompiler::RecompileRequest orderedPixelRequest(std::uint32_t shaderControl, std::span capabilities, std::span code = OrderedPixelCode) { const auto queue = orderedPixelQueue(shaderControl); ShaderRecompiler::RecompileRequest request{}; request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; request.context.waveSize = 64; request.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; request.target.supportedCapabilities = capabilities; request.layout.pushConstantSizeBytes = 128; request.useCache = false; return request; } std::string interlockShape(std::span words) { std::string shape; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { const auto op = static_cast(words[at] & 0xffffu); if (op == spv::OpCapability && words[at + 1] == spv::CapabilityFragmentShaderPixelInterlockEXT) shape += "capability "; if (op == spv::OpExtension && std::string_view(reinterpret_cast(&words[at + 1])) == "SPV_EXT_fragment_shader_interlock") shape += "extension "; if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModePixelInterlockOrderedEXT) shape += "ordered "; if (op == spv::OpBeginInvocationInterlockEXT) shape += "begin "; if (op == spv::OpEndInvocationInterlockEXT) shape += "end "; } return shape; } std::string interlockExits(std::span words, std::size_t& kills) { struct Block { std::vector ops; std::vector next; }; std::map blocks; std::uint32_t entry = 0, function = 0, label = 0, first = 0; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { const auto op = static_cast(words[at] & 0xffffu); const auto end = at + (words[at] >> 16u); if (op == spv::OpEntryPoint) entry = words[at + 2]; if (op == spv::OpFunction) { function = words[at + 2]; label = 0; } if (op == spv::OpLabel) label = words[at + 1]; if (op == spv::OpLabel && function == entry && first == 0) first = label; if (function != entry) { if (op == spv::OpBeginInvocationInterlockEXT || op == spv::OpEndInvocationInterlockEXT || op == spv::OpKill) return "an interlock instruction or OpKill outside the entry point"; continue; } if (label == 0) continue; auto& block = blocks[label]; block.ops.push_back(op); if (op == spv::OpBranch) block.next = {words[at + 1]}; if (op == spv::OpBranchConditional) block.next = {words[at + 2], words[at + 3]}; if (op == spv::OpSwitch) { block.next = {words[at + 2]}; for (auto target = at + 4; target < end; target += 2) block.next.push_back(words[target]); } } std::map state{{first, 0}}; std::vector work{first}; while (!work.empty()) { const auto current = work.back(); work.pop_back(); auto stage = state[current]; for (const auto op : blocks[current].ops) { if (op == spv::OpBeginInvocationInterlockEXT && stage++ != 0) return "block " + std::to_string(current) + " begins the interlock twice"; if (op == spv::OpEndInvocationInterlockEXT && stage++ != 1) return "block " + std::to_string(current) + " ends the interlock outside it"; if ((op == spv::OpKill || op == spv::OpReturn || op == spv::OpReturnValue || op == spv::OpUnreachable || op == spv::OpTerminateInvocation) && stage != 2) return "an exit in block " + std::to_string(current) + " skips OpEndInvocationInterlockEXT"; if (op == spv::OpKill) ++kills; } for (const auto next : blocks[current].next) { const auto [known, added] = state.emplace(next, stage); if (!added && known->second != stage) return "block " + std::to_string(next) + " is reached both inside and outside the interlock"; if (added) work.push_back(next); } } return {}; } void orderedPixelShaderTests() { const std::array interlock{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT}; const std::array plain{spv::CapabilityShader}; for (const auto shaderControl : {0x30640u, 0x30600u}) { const auto queue = orderedPixelQueue(shaderControl); static_cast(AgcDriver::Graphics::DecodeState(queue)); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DB_SHADER_CONTROL " + std::to_string(shaderControl)); Require(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports).orderedPixelShader, "PRIMITIVE_ORDERED_PIXEL_SHADER did not reach the pixel stage"); } for (const auto shaderControl : {0x130600u, 0x430600u}) { const auto queue = orderedPixelQueue(shaderControl); expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "ordered fragment execution"); Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted POPS_OVERLAP_NUM_SAMPLES in DB_SHADER_CONTROL " + std::to_string(shaderControl)); } const ShaderRecompiler::RequestSerializer serializer; std::set> keys; for (const auto& [shaderControl, expected] : std::array, 2>{{{0x30600u, "capability extension ordered begin end "}, {0x20600u, ""}}}) { const auto request = orderedPixelRequest(shaderControl, interlock); const auto replayed = serializer.Deserialize(serializer.Serialize(request)); Require(replayed.request.context.pixel->orderedPixelShader == (shaderControl == 0x30600u), "the serialized request lost PRIMITIVE_ORDERED_PIXEL_SHADER"); const auto result = ShaderRecompiler::Recompile(request); Require(interlockShape(result.spirv.Words()) == expected && interlockShape(ShaderRecompiler::Recompile(replayed.request).spirv.Words()) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " declared the wrong pixel interlock: " + interlockShape(result.spirv.Words())); std::vector key; ShaderRecompiler::RecompileCacheKey::Build(request, key); keys.insert(key); AgcDriver::Graphics::State state{}; state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({}); const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &result, 0}}}; AgcDriver::Graphics::ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); } Require(keys.size() == 2, "the recompile cache key ignores PRIMITIVE_ORDERED_PIXEL_SHADER"); const std::array bdaInterlock{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; const std::array bdaExtensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; auto faulting = orderedPixelRequest(0x30600u, bdaInterlock, OrderedBdaPixelCode); faulting.target.supportedExtensions = bdaExtensions; faulting.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; std::size_t kills = 0; const auto exits = interlockExits(ShaderRecompiler::Recompile(faulting).spirv.Words(), kills); Require(exits.empty() && kills == 6u, "a primitive-ordered pixel shader with BDA reads: " + (exits.empty() ? std::to_string(kills) + " OpKill, not the valid-mask kill and 5 BDA fault kills" : exits)); expectFailure([&] { static_cast(ShaderRecompiler::Recompile(orderedPixelRequest(0x30600u, plain))); }, "fragmentShaderPixelInterlock"); } constexpr std::array StencilRefExportCode{0x7e0e02f2u, 0xf8001882u, 0x700u, 0xbf810000u}; constexpr std::array DepthStencilRefExportCode{0x7e0e02f2u, 0xf8001883u, 0x707u, 0xbf810000u}; AgcDriver::QueueState stencilRefExportQueue(std::uint32_t shaderControl, std::uint32_t zFormat) { auto queue = makeState(); queue.context[0x1b3] = 0x300u; queue.context[0x1b4] = 0x300u; queue.context[0x1c4] = zFormat; queue.context[0x203] = shaderControl; return queue; } ShaderRecompiler::RecompileRequest stencilRefExportRequest(std::uint32_t shaderControl, std::span capabilities, std::span code = StencilRefExportCode) { const auto queue = stencilRefExportQueue(shaderControl, 2u); ShaderRecompiler::RecompileRequest request{}; request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; request.context.waveSize = 64; request.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; request.target.supportedCapabilities = capabilities; request.layout.pushConstantSizeBytes = 128; request.useCache = false; return request; } std::string stencilRefShape(std::span words) { std::string shape; std::uint32_t variable = 0; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { const auto op = static_cast(words[at] & 0xffffu); if (op == spv::OpCapability && words[at + 1] == spv::CapabilityStencilExportEXT) shape += "capability "; if (op == spv::OpExtension && std::string_view(reinterpret_cast(&words[at + 1])) == "SPV_EXT_shader_stencil_export") shape += "extension "; if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeEarlyFragmentTests) shape += "early "; if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeDepthReplacing) shape += "depth "; if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeStencilRefReplacingEXT) shape += "replacing "; if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == spv::BuiltInFragStencilRefEXT) { variable = words[at + 1]; shape += "builtin "; } if (op == spv::OpStore && variable != 0 && words[at + 1] == variable) shape += "store "; } return shape; } void stencilRefExportTests() { for (const auto shaderControl : {0x52u, 0x12u, 0x02u, 0x03u}) { for (const auto zFormat : {2u, 9u}) { const auto queue = stencilRefExportQueue(shaderControl, zFormat); static_cast(AgcDriver::Graphics::DecodeState(queue)); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with Z format " + std::to_string(zFormat)); const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); Require(pixel.stencilRefExportEnable, "STENCIL_TEST_VAL_EXPORT_ENABLE did not reach the pixel stage"); Require(!pixel.earlyZ, "a pixel shader that exports the stencil reference was given early tests"); } for (const auto zFormat : {0u, 1u, 3u, 7u}) { const auto queue = stencilRefExportQueue(shaderControl, zFormat); expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "depth export, shader coverage or ordered fragment execution"); Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted a stencil reference export with Z format " + std::to_string(zFormat)); } } for (const auto shaderControl : {0x54u, 0x56u}) { const auto queue = stencilRefExportQueue(shaderControl, 2u); expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "depth export, shader coverage or ordered fragment execution"); Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted STENCIL_OP_VAL_EXPORT_ENABLE in DB_SHADER_CONTROL " + std::to_string(shaderControl)); } Require(!AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x50u, 2u).context, IdentityExports).stencilRefExportEnable, "a pixel stage without STENCIL_TEST_VAL_EXPORT_ENABLE exports the stencil reference"); const std::array stencilExport{spv::CapabilityShader, spv::CapabilityStencilExportEXT}; const std::array plain{spv::CapabilityShader}; const ShaderRecompiler::RequestSerializer serializer; std::set> keys; const std::array, 4> cases{{{0x52u, "capability extension replacing builtin store "}, {0x12u, "capability extension replacing builtin store "}, {0x50u, ""}, {0x10u, "early "}}}; for (const auto& [shaderControl, expected] : cases) { const auto request = stencilRefExportRequest(shaderControl, stencilExport); const auto replayed = serializer.Deserialize(serializer.Serialize(request)); Require(replayed.request.context.pixel->stencilRefExportEnable == ((shaderControl & 2u) != 0), "the serialized request lost STENCIL_TEST_VAL_EXPORT_ENABLE"); const auto result = ShaderRecompiler::Recompile(request); Require(stencilRefShape(result.spirv.Words()) == expected && stencilRefShape(ShaderRecompiler::Recompile(replayed.request).spirv.Words()) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " declared the wrong stencil reference export: " + stencilRefShape(result.spirv.Words())); std::vector key; ShaderRecompiler::RecompileCacheKey::Build(request, key); keys.insert(key); AgcDriver::Graphics::State state{}; state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({}); const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &result, 0}}}; AgcDriver::Graphics::ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); } Require(keys.size() == cases.size(), "the recompile cache key ignores STENCIL_TEST_VAL_EXPORT_ENABLE"); const auto both = stencilRefShape(ShaderRecompiler::Recompile(stencilRefExportRequest(0x53u, stencilExport, DepthStencilRefExportCode)).spirv.Words()); Require(both == "capability extension depth replacing builtin store ", "a depth and stencil reference export declared the wrong outputs: " + both); const auto depthOnly = stencilRefShape(ShaderRecompiler::Recompile(stencilRefExportRequest(0x51u, stencilExport, DepthStencilRefExportCode)).spirv.Words()); Require(depthOnly == "depth ", "an MRTZ stencil channel without STENCIL_TEST_VAL_EXPORT_ENABLE declared the wrong outputs: " + depthOnly); expectFailure([&] { static_cast(ShaderRecompiler::Recompile(stencilRefExportRequest(0x52u, plain))); }, "VK_EXT_shader_stencil_export"); auto queue = stencilRefExportQueue(0x52u, 2u); queue.context[0x000] = 0; queue.context[0x002] = 0; queue.context[0x007] = (1u << 16u) | 3u; queue.context[0x00a] = 7; queue.context[0x00b] = 0; queue.context[0x010] = 0x22900983; queue.context[0x011] = 0x20000181; for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; queue.context[0x10b] = 0x00030030; queue.context[0x10c] = 0x05ffff02; queue.context[0x10d] = 0x05ffff02; queue.context[0x200] = 0x00200211; auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.stencilTest && state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL && state.stencilFront.passOp == VK_STENCIL_OP_REPLACE, "a stencil reference export changed the REPLACE_TEST decode"); for (const auto ops : {0x00020020u, 0x00040040u, 0x00000200u, 0x00000004u}) { queue.context[0x10b] = ops; expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "exports the stencil test value"); } queue.context[0x10b] = 0x00050050; queue.context[0x10c] = 0x01ffff02; state = AgcDriver::Graphics::DecodeState(queue); Require(state.stencilFront.passOp == VK_STENCIL_OP_INCREMENT_AND_CLAMP, "a stencil reference export changed the increment decode"); queue.context[0x000] = 2; expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "stencil clear with a pixel shader that exports the stencil test value"); queue.context[0x203] = 0x50u; static_cast(AgcDriver::Graphics::DecodeState(queue)); } void ProvokingVertexTests() { using namespace AgcDriver::Graphics; constexpr std::uint32_t lastVertex = 1u << 19u; auto queue = makeState(); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; const auto first = DecodeState(queue); Require(first.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, "the default provoking vertex is not first"); for (const auto primitive : {2u, 4u, 5u, 6u}) { queue.userConfig[0x242] = primitive; for (const auto last : {false, true, false}) { queue.context[0x205] = 0x240u | (last ? lastVertex : 0u); const auto state = DecodeState(queue); Require(state.provokingVertexMode == (last ? VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT : VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT), "PROVOKING_VTX_LAST did not select the requested vertex for primitive " + std::to_string(primitive)); const auto rejection = DrawRejection(queue, false); Require(rejection.empty(), "the draw precheck rejected provoking vertex mode for primitive " + std::to_string(primitive) + ": " + rejection); Require(state.cullMode == first.cullMode && state.frontFace == first.frontFace && state.depthBias == first.depthBias, "PROVOKING_VTX_LAST changed another rasterization field"); } } queue.context[0x205] |= lastVertex; queue.ClearContext(); Require((queue.context.at(0x205) & lastVertex) == 0, "context reset kept last provoking vertex mode"); Require(DrawKeyCovers({RegisterBank::Context, 0x205}), "the draw cache key does not cover provoking vertex mode"); const Context unsupported{}; ValidateProvokingVertex(unsupported, first); auto last = first; last.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT; expectFailure([&] { ValidateProvokingVertex(unsupported, last); }, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); auto supported = unsupported; supported.provokingVertexLast = true; ValidateProvokingVertex(supported, last); for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_POINT_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) { auto state = last; state.topology = topology; ValidateProvokingVertex(supported, state); } for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY, VK_PRIMITIVE_TOPOLOGY_MAX_ENUM}) { auto state = last; state.topology = topology; expectFailure([&] { ValidateProvokingVertex(supported, state); }, "last provoking vertex is unsupported for this primitive topology"); } const std::array flatColor{0xc8120002u, 0xc8160102u, 0xc81a0202u, 0xc81e0302u, 0xf800180fu, 0x07060504u, 0xbf810000u}; const auto flatPixel = recompilePixel({0x400u}, flatColor); const auto inputs = locatedInputs(flatPixel.spirv.Words()); Require(inputs.size() == 1 && inputs[0].location == 0 && inputs[0].flat && !inputs[0].perVertex, "flat v_interp_mov P0 did not compile to a flat input"); Require(flatPixel.fragmentParameters.size() == 1 && flatPixel.fragmentParameters[0].flat && !flatPixel.fragmentParameters[0].perVertex, "flat v_interp_mov P0 has explicit per-vertex metadata"); const std::array flatShaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatPixel, 0}}}; ValidateProvokingVertex(supported, last, flatShaders); const std::array flatP10Color{0xc8120000u, 0xc8160100u, 0xc81a0200u, 0xc81e0300u, 0xf800180fu, 0x07060504u, 0xbf810000u}; const auto flatP10Pixel = recompilePixel({0x400u}, flatP10Color); const auto p10Inputs = locatedInputs(flatP10Pixel.spirv.Words()); Require(p10Inputs.size() == 1 && p10Inputs[0].location == 0 && p10Inputs[0].flat && !p10Inputs[0].perVertex, "flat v_interp_mov P10 did not compile to a flat input"); Require(flatP10Pixel.fragmentParameters.size() == 1 && flatP10Pixel.fragmentParameters[0].flatP10 && !flatP10Pixel.fragmentParameters[0].perVertex && !flatPixel.fragmentParameters[0].flatP10, "flat v_interp_mov P10 was not recorded apart from P0"); const std::array flatP10Shaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatP10Pixel, 0}}}; ValidateProvokingVertex(supported, last, flatP10Shaders); expectFailure([&] { ValidateProvokingVertex(supported, first, flatP10Shaders); }, "v_interp_mov p10 of a flat input with the first provoking vertex is not measured"); ShaderRecompiler::RecompileResult fragment{}; const std::array shaders{{{ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) { auto state = last; state.topology = topology; for (const auto flat : {false, true}) { for (const auto perVertex : {false, true}) { fragment.fragmentParameters = {{0, 0, flat, perVertex}}; auto firstState = state; firstState.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; ValidateProvokingVertex(unsupported, firstState, shaders); if (flat && perVertex) { expectFailure([&] { ValidateProvokingVertex(supported, state, shaders); }, "last provoking vertex with explicit per-vertex flat interpolation is unsupported"); } else if (perVertex && topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) { expectFailure([&] { ValidateProvokingVertex(supported, state, shaders); }, "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported"); } else { ValidateProvokingVertex(supported, state, shaders); } } } } for (const auto path : {ShaderPath::Geometry, ShaderPath::Tessellation, ShaderPath::TessellationGeometry}) { auto generated = last; generated.stages.path = path; expectFailure([&] { ValidateProvokingVertex(supported, generated); }, "last provoking vertex is unsupported for generated primitive pipelines"); generated.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; ValidateProvokingVertex(unsupported, generated); } for (std::uint32_t generated = 0; generated < 3; ++generated) { auto state = last; if (generated == 0) state.rectList = true; if (generated == 1) state.stages.mesh.emplace(); if (generated == 2) state.stages.tessellation.emplace(); expectFailure([&] { ValidateProvokingVertex(supported, state); }, "last provoking vertex is unsupported for generated primitive pipelines"); } last.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_MAX_ENUM_EXT; expectFailure([&] { ValidateProvokingVertex(supported, last); }, "invalid provoking vertex mode"); } void ProvokingVertexRenderPassTests() { using namespace AgcDriver::Graphics; const auto first = DecodeState(makeState()); const std::array views{reinterpret_cast(std::uintptr_t{1})}; const std::array modes{ VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT, VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT, VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT }; for (const bool perPipeline : {false, true}) { Context context{}; context.provokingVertexLast = true; context.provokingVertexModePerPipeline = perPipeline; auto state = first; const auto initialKey = DrawRenderPassKey(context, state, views); auto previousKey = initialKey; auto previousMode = modes.front(); for (const auto mode : modes) { state.provokingVertexMode = mode; const auto key = DrawRenderPassKey(context, state, views); const bool reusable = key == previousKey; Require(reusable == (perPipeline || mode == previousMode), "render-pass reuse does not respect provokingVertexModePerPipeline"); previousKey = key; previousMode = mode; } Require(previousKey == initialKey, "returning to first provoking vertex changed the render-pass identity"); state.viewport.x += 1.0f; Require(DrawRenderPassKey(context, state, views) == initialKey, "dynamic viewport changes prevent render-pass reuse"); const std::array differentViews{reinterpret_cast(std::uintptr_t{2})}; Require(DrawRenderPassKey(context, state, differentViews) != initialKey, "different attachments share a render-pass identity"); ++state.renderExtent.width; Require(DrawRenderPassKey(context, state, views) != initialKey, "different render extents share a render-pass identity"); } } void ConservativeRasterizationTests() { auto queue = makeState(); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; Require(AgcDriver::Graphics::DecodeState(queue).conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT, "PA_SC_CONSERVATIVE_RASTERIZATION_CNTL 0x6000 enabled conservative rasterization"); queue.context[0x313] = 0x6001; for (const auto primitive : {4u, 5u, 6u}) { queue.userConfig[0x242] = primitive; Require(AgcDriver::Graphics::DecodeState(queue).conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_OVERESTIMATE_EXT, "OVER_RAST_ENABLE did not decode to overestimation for primitive type " + std::to_string(primitive)); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected OVER_RAST_ENABLE"); } for (const auto& [primitive, rejected] : std::array, 3>{{{1u, "conservative rasterization of points is unsupported (VGT_PRIMITIVE_TYPE=0x1)"}, {2u, "conservative rasterization of lines is unsupported (VGT_PRIMITIVE_TYPE=0x2)"}, {17u, "conservative rasterization of rectangles is unsupported (VGT_PRIMITIVE_TYPE=0x11)"}}}) { queue.userConfig[0x242] = primitive; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, rejected); } queue.userConfig[0x242] = 4; auto geometry = queue; geometry.context[0x2d5] = 0x2020; geometry.userConfig[0x25b] = (64u << 9u) | 21u; geometry.context[0x1ff] = 64; geometry.context[0x2ce] = 3; geometry.context[0x29b] = 2; geometry.context[0x2ab] = 4; geometry.shader[0x8a] = 3u << 29u; geometry.shader[0x8b] = 3u << 16u; for (const auto input : {1u, 2u, 4u}) { geometry.userConfig[0x242] = input; const auto state = AgcDriver::Graphics::DecodeState(geometry); Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_OVERESTIMATE_EXT, "OVER_RAST_ENABLE did not overestimate the triangle strips of a geometry shader with input primitive type " + std::to_string(input)); } for (const auto& [output, rejected] : std::array, 3>{{{0u, "conservative rasterization of points is unsupported (VGT_GS_OUT_PRIM_TYPE=0x0)"}, {1u, "conservative rasterization of lines is unsupported (VGT_GS_OUT_PRIM_TYPE=0x1)"}, {0x80000002u, "conservative rasterization of per-stream primitive types is unsupported (VGT_GS_OUT_PRIM_TYPE=0x80000002)"}}}) { geometry.context[0x29b] = output; expectFailure([&] { AgcDriver::Graphics::DecodeState(geometry); }, rejected); } auto tessellation = queue; tessellation.context[0x2d5] = 0x0200210d; tessellation.userConfig[0x242] = 9; tessellation.context[0x2d6] = 0xc355; tessellation.context[0x2db] = 1u | (2u << 2u) | (2u << 5u); tessellation.context[0x286] = 0x42800000; tessellation.context[0x287] = 0x3f800000; const auto tessellated = AgcDriver::Graphics::DecodeState(tessellation); Require(tessellated.stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && tessellated.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_OVERESTIMATE_EXT, "OVER_RAST_ENABLE did not overestimate clockwise tessellated triangles"); for (const auto& [parameters, rejected] : std::array, 3>{{{1u | (2u << 2u), "conservative rasterization of points is unsupported (VGT_TF_PARAM=0x9)"}, {1u | (2u << 2u) | (1u << 5u), "conservative rasterization of lines is unsupported (VGT_TF_PARAM=0x29)"}, {(2u << 2u) | (1u << 5u), "conservative rasterization of lines is unsupported (VGT_TF_PARAM=0x28)"}}}) { tessellation.context[0x2db] = parameters; expectFailure([&] { AgcDriver::Graphics::DecodeState(tessellation); }, rejected); } tessellation.context[0x2d5] = 0x202d; tessellation.context[0x2db] = 1u | (2u << 2u) | (2u << 5u); tessellation.context[0x29b] = 1; expectFailure([&] { AgcDriver::Graphics::DecodeState(tessellation); }, "conservative rasterization of lines is unsupported (VGT_GS_OUT_PRIM_TYPE=0x1)"); for (const auto inputs : {0x6u, 0x42u}) { queue.context[0x1b3] = inputs; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "conservative rasterization with centroid interpolation"); } queue.context[0x1b3] = 2; for (const auto control : {0x6003u, 0x6020u, 0x6401u, 0xe06001u, 0x1u}) { queue.context[0x313] = control; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_SC_CONSERVATIVE_RASTERIZATION_CNTL=0x"); Require(AgcDriver::Graphics::DrawRejection(queue, false).find("PA_SC_CONSERVATIVE_RASTERIZATION_CNTL=0x") != std::string::npos, "the precheck accepted PA_SC_CONSERVATIVE_RASTERIZATION_CNTL " + std::to_string(control)); } } void floatControlsModeTests() { AgcDriver::Graphics::State state{}; state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; ShaderRecompiler::RecompileResult vertex; ShaderRecompiler::RecompileResult pixel; const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}}; const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; for (const auto width : {16u, 32u, 64u}) { vertex.spirv = makeModule({.floatControlsWidth = width}); pixel.spirv = makeModule({.fragment = true, .floatControlsWidth = width}); AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); } vertex.spirv = makeModule({.floatControlsWidth = 8u}); pixel.spirv = makeModule({.fragment = true}); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "float controls execution modes"); } void pixelParameterSlotTests() { using AgcDriver::Graphics::CompiledShader; const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; auto state = AgcDriver::Graphics::DecodeState(makeState()); ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({.parameterOutput = true}); const std::array mixed{0xc8100000u, 0xc8110001u, 0xc8160402u, 0xc81a0802u, 0xc81e0f02u, 0xf800180fu, 0x07060504u, 0xbf810000u}; auto pixel = recompilePixel({0x0u, 0x400u, 0x22u, 0x320u}, mixed); auto inputs = locatedInputs(pixel.spirv.Words()); Require(inputs.size() == 1 && inputs[0].location == 0 && inputs[0].perVertex, "a slot read flat and interpolated did not become one per-vertex input"); std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}}; AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, true); const std::array shared{0xc8100000u, 0xc8110001u, 0xc8140500u, 0xc8150501u, 0xf800180fu, 0x05040504u, 0xbf810000u}; pixel = recompilePixel({0x3u, 0x3u}, shared); inputs = locatedInputs(pixel.spirv.Words()); Require(inputs.size() == 1 && inputs[0].location == 3 && inputs[0].perVertex && !inputs[0].flat, "inputs reading one slot were not declared once at the slot"); pixel = recompilePixel({0x404u, 0x0u}, shared); inputs = locatedInputs(pixel.spirv.Words()); Require(inputs.size() == 2 && inputs[0].location == 0 && inputs[0].perVertex && inputs[1].location == 4 && inputs[1].perVertex, "flat and interpolated inputs of different slots moved"); pixel = recompilePixel({0x20u, 0x2320u}, shared); Require(locatedInputs(pixel.spirv.Words()).empty(), "a defaulted input was declared as a parameter"); AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, true); const std::array vertices{0xc8120002u, 0xc8160000u, 0xc81a0001u, 0xc81e0302u, 0xf800180fu, 0x07060504u, 0xbf810000u}; const auto subtracts = [](std::span words) { std::size_t count = 0; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) count += (words[at] & 0xffffu) == spv::OpFSub; return count; }; pixel = recompilePixel({0x423u}, vertices); inputs = locatedInputs(pixel.spirv.Words()); Require(inputs.size() == 1 && inputs[0].location == 3 && inputs[0].perVertex, "a pass-through input (OFFSET bit 5 with FLAT_SHADE) was not read per vertex at its slot"); Require(subtracts(pixel.spirv.Words()) == 0, "v_interp_mov p10/p20 of a pass-through input subtracted vertex 0"); ShaderRecompiler::RecompileResult slotVertex; slotVertex.spirv = makeModule({.parameterOutput = true, .parameterLocation = 3}); const std::array slotShaders{{{ShaderRecompiler::ShaderStage::Vertex, &slotVertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}}; AgcDriver::Graphics::ValidateShaders(slotShaders, state, subgroup, true); pixel = recompilePixel({0x403u}, vertices); inputs = locatedInputs(pixel.spirv.Words()); Require(inputs.size() == 1 && inputs[0].location == 3 && inputs[0].perVertex && subtracts(pixel.spirv.Words()) == 2, "v_interp_mov p10/p20 of a flat input did not read differences to vertex 0"); expectFailure([&] { recompilePixel({0x423u, 0x3u}, shared); }, "passes its vertices through unchanged"); } ShaderRecompiler::RecompileRequest packedExportRequest(const AgcDriver::QueueState& queue, const AgcDriver::Graphics::State& state, std::span code) { ShaderRecompiler::RecompileRequest request{}; request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; request.context.waveSize = 64; request.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state)); request.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; request.layout.pushConstantSizeBytes = 128; request.useCache = true; return request; } void unorm10_11_11TargetTests() { using ShaderRecompiler::ColorExportPacking; auto queue = makeState(); queue.context[0x31c] = (queue.context.at(0x31c) & ~0x7cu) | (6u << 2u); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; const auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.color.format == VK_FORMAT_R32_UINT && state.color.elementBytes == 4 && state.color.bytes == colorMemory.size() && state.color.packing == ColorExportPacking::Unorm10_11_11, "a 10_11_11 unorm target did not decode as a packed R32_UINT attachment"); const auto packings = AgcDriver::Graphics::ExportPackings(state); Require(packings[0] == ColorExportPacking::Unorm10_11_11 && std::all_of(packings.begin() + 1, packings.end(), [](ColorExportPacking packing) { return packing == ColorExportPacking::None; }), "export 0 did not carry the 10_11_11 unorm packing alone"); auto changed = queue; changed.context[0x31c] |= 7u << 8u; const auto floating = AgcDriver::Graphics::DecodeState(changed); Require(floating.color.format == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && floating.color.packing == ColorExportPacking::None, "a 10_11_11 float target changed"); changed = queue; changed.context[0x31c] |= 1u << 8u; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 6 number type 1"); changed = queue; changed.context[0x1e0] = 0x40010001u; for (std::uint32_t i = 0; i < 4; ++i) changed.context[0x105 + i] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "blending into a 10_11_11 unorm color target"); changed = queue; changed.context[0x8e] = 3; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of a 10_11_11 unorm color target"); changed.context[0x8e] = 7; Require(AgcDriver::Graphics::DecodeState(changed).color.packing == ColorExportPacking::Unorm10_11_11, "an RGB write of a 10_11_11 unorm target was refused"); for (const auto format : {5u, 6u}) { changed = queue; changed.context[0x1c5] = format; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "color export format " + std::to_string(format) + " into a 10_11_11 unorm target"); } changed = queue; changed.context[0x1c5] = 4; Require(AgcDriver::Graphics::DecodeState(changed).color.packing == ColorExportPacking::Unorm10_11_11, "an FP16_ABGR export into a 10_11_11 unorm target was refused"); changed = queue; changed.context[0x31c] |= 0x10000000u; changed.context[0x325] = 0x1234; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "DCC-compressed 10_11_11 unorm color targets"); changed = queue; changed.context[0x31c] |= 0x40000u; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "truncating (ROUND_MODE) 10_11_11 unorm color targets"); constexpr std::array code{0xf800180fu, 0x07060504u, 0xbf810000u}; const auto request = packedExportRequest(queue, state, code); auto plain = request; plain.context.pixel->targetExportPacking.fill(ColorExportPacking::None); const ShaderRecompiler::RequestSerializer serializer; Require(serializer.Deserialize(serializer.Serialize(request)).request.context.pixel->targetExportPacking == request.context.pixel->targetExportPacking, "the 10_11_11 unorm packing was lost in serialization"); std::vector key; std::vector plainKey; ShaderRecompiler::BuildPreparedShaderKey(request, key); ShaderRecompiler::BuildPreparedShaderKey(plain, plainKey); Require(key == plainKey, "the export packing entered the static shader ABI"); const auto prepared = ShaderRecompiler::PrepareShader(plain); Require(ShaderRecompiler::MatchesPreparedShader(request, *prepared), "a packed draw did not match the registered pixel shader"); const auto packed = ShaderRecompiler::Recompile(request); const auto unpacked = ShaderRecompiler::Recompile(plain); Require(packed.variantId == unpacked.variantId && packed.variantId == ShaderRecompiler::GetPreparedArtifact(*prepared).variantId, "the packed draw did not reuse the registered artifact"); Require(packed.PipelineVariantId() != unpacked.PipelineVariantId() && packed.spirv.Words() != unpacked.spirv.Words(), "the export packing did not specialize the module"); Require(ShaderRecompiler::Recompile(request).PipelineVariantId() == packed.PipelineVariantId(), "the packed specialization was not reused"); ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({}); const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; const auto validate = [&](const ShaderRecompiler::RecompileResult& pixel, const AgcDriver::Graphics::State& target) { const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}}; return AgcDriver::Graphics::ValidateShaders(shaders, target, subgroup, false); }; Require(validate(packed, state) == std::set{0u}, "the packed export did not reach attachment 0"); expectFailure([&] { validate(unpacked, state); }, "uint4 words to its packed 10_11_11 unorm attachments"); auto plainState = state; plainState.colors[0].packing = ColorExportPacking::None; expectFailure([&] { validate(packed, plainState); }, "float4 colors"); auto uintExport = request; uintExport.context.pixel->targetOutputMode[0] = 7; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(uintExport)); }, "fragment output 0 has no 10_11_11 unorm packing path"); } AgcDriver::QueueState dualSourceQueue() { auto queue = makeState(); queue.context[0x8f] = 0xffu; queue.context[0x1c5] = 0x44u; queue.context[0x1e0] = 0x40010f01u; for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = 0; queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; return queue; } struct OutputDecorations { std::map locations; std::map indices; }; OutputDecorations outputDecorations(std::span words) { OutputDecorations result; std::set outputs; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { const auto op = static_cast(words[at] & 0xffffu); if (op == spv::OpVariable && words[at + 3] == spv::StorageClassOutput) outputs.insert(words[at + 2]); if (op != spv::OpDecorate) continue; if (words[at + 2] == spv::DecorationLocation) result.locations[words[at + 1]] = words[at + 3]; if (words[at + 2] == spv::DecorationIndex) result.indices[words[at + 1]] = words[at + 3]; } std::erase_if(result.locations, [&](const auto& entry) { return !outputs.contains(entry.first); }); return result; } void DualSourceBlendTests() { auto queue = dualSourceQueue(); const auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.dualSourceBlend && state.colors.size() == 1 && state.blends.size() == 1, "an ONE + SRC1_COLOR blend did not decode as dual-source blending into one target"); Require(state.blends[0].srcColorBlendFactor == VK_BLEND_FACTOR_ONE && state.blends[0].dstColorBlendFactor == VK_BLEND_FACTOR_SRC1_COLOR && state.blends[0].srcAlphaBlendFactor == VK_BLEND_FACTOR_ONE && state.blends[0].dstAlphaBlendFactor == VK_BLEND_FACTOR_SRC1_COLOR, "BLEND_SRC1_COLOR did not become VK_BLEND_FACTOR_SRC1_COLOR"); const std::array, 4> factors{{{15u, VK_BLEND_FACTOR_SRC1_COLOR}, {16u, VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR}, {17u, VK_BLEND_FACTOR_SRC1_ALPHA}, {18u, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA}}}; for (const auto& [value, factor] : factors) { auto changed = queue; changed.context[0x1e0] = 0x60000000u | (value << 24u) | (1u << 16u) | (value << 8u) | value; const auto decoded = AgcDriver::Graphics::DecodeState(changed); Require(decoded.dualSourceBlend && decoded.blends[0].srcColorBlendFactor == factor && decoded.blends[0].dstColorBlendFactor == factor && decoded.blends[0].srcAlphaBlendFactor == VK_BLEND_FACTOR_ONE && decoded.blends[0].dstAlphaBlendFactor == factor, "blend factor " + std::to_string(value) + " decoded wrongly"); } auto changed = queue; changed.context[0x1e0] = 0x00010f01u; Require(!AgcDriver::Graphics::DecodeState(changed).dualSourceBlend, "SRC1 factors of a disabled blend enabled dual-source blending"); changed = queue; changed.context[0x1e0] = 0x40010001u; Require(!AgcDriver::Graphics::DecodeState(changed).dualSourceBlend, "a blend without SRC1 factors enabled dual-source blending"); for (const auto operation : {2u, 3u}) { changed.context[0x1e0] = 0x40010f01u | (operation << 5u); expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "MIN or MAX operation"); } changed.context[0x1e0] = 0x40010f01u | (4u << 5u); Require(AgcDriver::Graphics::DecodeState(changed).blends[0].colorBlendOp == VK_BLEND_OP_REVERSE_SUBTRACT, "a reverse-subtract dual-source blend was refused"); changed = queue; changed.context[0x8f] = 0xfu; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "MRT slots 0 and 1"); changed.context[0x8f] = 0xfffu; changed.context[0x1c5] = 0x444u; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "MRT slots 0 and 1"); changed.context[0x8f] = 0xf000fu; changed.context[0x1c5] = 0x44u; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "MRT slots 0 and 1"); alignas(256) static std::array slotOneMemory{}; changed = queue; for (const auto offset : {0x31bu, 0x31cu, 0x31du}) changed.context[offset + 0xfu] = changed.context.at(offset); for (const auto offset : {0x3b0u, 0x3b8u}) changed.context[offset + 1u] = changed.context.at(offset); changed.context[0x318 + 0xfu] = static_cast(reinterpret_cast(slotOneMemory.data()) >> 8u); changed.context[0x390 + 1u] = static_cast(reinterpret_cast(slotOneMemory.data()) >> 40u); changed.context[0x1e1] = 0; changed.context[0x8e] = 0xffu; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "color writes to slot 0 alone"); for (const auto format : {0x04u, 0x14u, 0x74u, 0x94u}) { changed = queue; changed.context[0x1c5] = format; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "MRT1 export in the MRT0 export format"); } changed = queue; changed.context[0x1c5] = 0x99u; Require(AgcDriver::Graphics::DecodeState(changed).dualSourceBlend, "a 32_ABGR MRT0 and MRT1 export pair was refused"); for (const auto mask : {0x7fu, 0xf7u}) { changed = queue; changed.context[0x8f] = mask; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "MRT1 export with the MRT0 shader mask"); } changed = queue; changed.context[0x31c] = 0x8804u; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "component-swapped color target"); constexpr std::array code{0xf800000fu, 0x07060504u, 0xf800181fu, 0x0b0a0908u, 0xbf810000u}; ShaderRecompiler::RecompileRequest request{}; request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; request.context.waveSize = 64; request.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state)); request.context.pixel->dualSourceBlend = state.dualSourceBlend; request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; request.layout.pushConstantSizeBytes = 128; request.useCache = true; auto plain = request; plain.context.pixel->dualSourceBlend = false; const ShaderRecompiler::RequestSerializer serializer; Require(serializer.Deserialize(serializer.Serialize(request)).request.context.pixel->dualSourceBlend, "dual-source blending was lost in serialization"); std::vector key; std::vector plainKey; ShaderRecompiler::BuildPreparedShaderKey(request, key); ShaderRecompiler::BuildPreparedShaderKey(plain, plainKey); Require(key == plainKey, "dual-source blending entered the static shader ABI"); const auto prepared = ShaderRecompiler::PrepareShader(plain); Require(ShaderRecompiler::MatchesPreparedShader(request, *prepared), "a dual-source draw did not match the registered pixel shader"); const auto dual = ShaderRecompiler::Recompile(request); const auto single = ShaderRecompiler::Recompile(plain); Require(dual.variantId == single.variantId && dual.variantId == ShaderRecompiler::GetPreparedArtifact(*prepared).variantId, "the dual-source draw did not reuse the registered artifact"); Require(dual.PipelineVariantId() != single.PipelineVariantId() && ShaderRecompiler::Recompile(request).PipelineVariantId() == dual.PipelineVariantId(), "dual-source blending did not key its own specialization"); const auto dualOutputs = outputDecorations(dual.spirv.Words()); const auto singleOutputs = outputDecorations(single.spirv.Words()); Require(dualOutputs.locations.size() == 2 && std::all_of(dualOutputs.locations.begin(), dualOutputs.locations.end(), [](const auto& entry) { return entry.second == 0u; }) && dualOutputs.indices.size() == 1 && dualOutputs.indices.begin()->second == 1u, "the MRT1 export of a dual-source draw was not the location 0 index 1 output"); std::set singleLocations; for (const auto& entry : singleOutputs.locations) singleLocations.insert(entry.second); Require(singleOutputs.indices.empty() && singleLocations == std::set{0u, 1u}, "MRT0 and MRT1 without dual-source blending did not keep locations 0 and 1"); ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({}); const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; const auto validate = [&](const ShaderRecompiler::RecompileResult& pixel, const AgcDriver::Graphics::State& target) { const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}}; return AgcDriver::Graphics::ValidateShaders(shaders, target, subgroup, false); }; Require(validate(dual, state) == std::set{0u}, "the dual-source shader did not validate against its draw"); expectFailure([&] { validate(single, state); }, "needs a float4 second color at location 0 index 1"); auto singleState = state; singleState.dualSourceBlend = false; expectFailure([&] { validate(dual, singleState); }, "Index decoration"); } 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 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, .sampleId = true, .layer = true, .fragDepth = true, .sampleMaskLength = 1}); AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, true, true); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, true); }, "unsupported device capability 2"); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, true, false); }, "unsupported device capability 35"); pixel.spirv = makeModule({.fragment = true, .sampleMaskLength = 2}); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment built-in"); vertex.spirv = makeModule({.parameterOutput = true, .sampleId = true}); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability 35"); vertex.spirv = makeModule({.parameterOutput = true}); 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({}); ShaderRecompiler::RecompileResult pixel; pixel.spirv = makeModule({.fragment = true, .secondTarget = true}); const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}}; const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; Require(state.colors.empty() && AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false) == std::set{0u}, "an export past the attachments was not dropped"); } { ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({.vertexInput = true}); ShaderRecompiler::VertexAttribute attribute{0, 4, {{0x1000, 32u << 16u, 3, 77u << 12u}}, 0}; const std::array 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"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1) == 112, "an index past the records still spans its fetch"); { auto wider = attribute; wider.resource.fields[2] = 9; auto instanced = attribute; instanced.fetchIndex = 1; instanced.resource.fields[2] = 1000; auto constant = attribute; constant.resource.fields[1] = 0; const std::array attributes{attribute, wider, instanced, constant}; Require(AgcDriver::Graphics::ZeroFetchVertex(attributes) == 9u, "the first vertex reading zeros is not past the largest per-vertex record count"); auto clamped = attribute; clamped.resource.fields[3] |= 2u << 28u; const std::array selective{attribute, clamped}; Require(!AgcDriver::Graphics::ZeroFetchVertex(selective).has_value(), "an attribute with out-of-bounds select 2 was taken to read zeros past its records"); std::array indices{0xffffffffu, 2u, 0x12345u, 0xffffffffu}; Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(indices)), 4, true, 9u) == 9u && indices == std::array{0xffffffffu, 2u, 9u, 0xffffffffu}, "clamping kept an index past the limit or moved a restart index"); Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(indices)), 4, false, 9u) == 9u && indices == std::array{9u, 2u, 9u, 9u}, "clamping without restart kept a 0xffffffff index"); std::array narrow{0xffffu, 1u}; Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(narrow)), 2, false, 4u) == 4u && narrow[0] == 4u && narrow[1] == 1u, "clamping 16-bit indices failed"); } attribute.fetchIndex = 1; Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 48, "instance attributes used the vertex index"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2, 1) == 80, "first instance was ignored"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 2, 2) == 112, "a first instance past the records still spans its fetch"); expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 2, 0xffffffffu); }, "instance range overflow"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 4) == 112, "an instance range past the records still spans its fetch"); attribute.fetchIndex = 0; attribute.resource.fields[3] = 0x1004dfacu; Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1) == 112, "oob_select 1 past the records still spans its fetch"); attribute.resource.fields[3] = 0x2004dfacu; expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1); }, "record count"); attribute.resource.fields[3] = 0x3004dfacu; expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1); }, "record count"); attribute.resource.fields[3] = 77u << 12u; attribute.fetchIndex = 1; 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"); Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "a structured zero-stride fetch was treated as out of range"); attribute.resource.fields[3] = (77u << 12u) | (2u << 28u); attribute.resource.fields[2] = 1; Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "OOB_SELECT 2 must read a zero-stride fetch whole while NUM_RECORDS is nonzero"); Require(AgcDriver::Graphics::VertexBufferExtent(attribute) == 16, "OOB_SELECT 2 extent must cover the whole fetch"); Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 2 with records was treated as out of range"); attribute.resource.fields[2] = 0; Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 2 without records must read zero"); attribute.resource.fields[3] = (77u << 12u) | (3u << 28u); Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 without records must read zero"); attribute.resource.fields[2] = 8; Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 with records was treated as out of range"); expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range"); attribute.resource.fields[2] = 16; Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1) == 16, "OOB_SELECT 3 must read a fetch inside the range"); attribute.resource.fields[3] = 113u << 12u; expectFailure([&] { AgcDriver::Graphics::DecodeVertexFormat(attribute); }, "unsupported vertex format"); attribute.resource.fields[3] = 50u << 12u; Require(AgcDriver::Graphics::DecodeVertexFormat(attribute).format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 && AgcDriver::Graphics::DecodeVertexFormat(attribute).bytes == 4, "2_10_10_10 vertex format was not decoded"); attribute.resource.fields[3] = 55u << 12u; Require(AgcDriver::Graphics::DecodeVertexFormat(attribute).format == VK_FORMAT_A2B10G10R10_SINT_PACK32 && std::string_view(AgcDriver::Graphics::DecodeVertexFormat(attribute).scalar) == "i32", "2_10_10_10 sint vertex format was not decoded"); attribute.resource.fields[3] = 36u << 12u; Require(AgcDriver::Graphics::DecodeVertexFormat(attribute).format == VK_FORMAT_B10G11R11_UFLOAT_PACK32, "10_11_11 float vertex format was not decoded"); for (std::uint32_t format = 1; format < 128u; ++format) { attribute.resource.fields[3] = format << 12u; std::string_view scalar; try { scalar = AgcDriver::Graphics::DecodeVertexFormat(attribute).scalar; } catch (const std::exception&) { continue; } const auto numeric = ShaderRecompiler::VertexInputNumericClass(static_cast(format)); Require((scalar == "u32") == (numeric == ShaderRecompiler::IrTextureNumericClass::Uint) && (scalar == "i32") == (numeric == ShaderRecompiler::IrTextureNumericClass::Sint), "vertex format " + std::to_string(format) + " declares a shader input of another numeric type than its attribute"); } attribute.resource.fields[3] = 43u << 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::CapabilityInt64Atomics, spv::CapabilityInt64ImageEXT}) { ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({}); vertex.spirv.insert(vertex.spirv.begin() + 5, {(2u << 16u) | spv::OpCapability, static_cast(capability)}); const std::array 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); }, "unsupported device capability"); AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, true); } { ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({}); vertex.spirv.insert(vertex.spirv.begin() + 5, {(2u << 16u) | spv::OpCapability, static_cast(spv::CapabilityInt64Atomics)}); const std::array 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); }, "unsupported device capability 12"); AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true); } 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(capability)}); const std::array 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"); } { ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({}); vertex.spirv.insert(vertex.spirv.begin() + 5, {(2u << 16u) | spv::OpCapability, static_cast(spv::CapabilityGroupNonUniformArithmetic)}); const std::array 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_FRAGMENT_BIT; subgroup.supportedOperations = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_ARITHMETIC_BIT; expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported for shader stage"); subgroup.supportedStages = VK_SHADER_STAGE_VERTEX_BIT; subgroup.supportedOperations = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_BALLOT_BIT | VK_SUBGROUP_FEATURE_SHUFFLE_BIT; expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "device lacks operations"); subgroup.supportedOperations |= VK_SUBGROUP_FEATURE_ARITHMETIC_BIT; AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); } const std::vector words(8, 0); const auto validate = [&](const ShaderRecompiler::RecompileResult& vertex, std::uint32_t fragmentOffset) { const std::array 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 its stage's"); } { 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"); } } } bool recompilesDebugBranch(std::uint32_t opcode) { auto queue = makeState(); queue.context[0x1b3] = 0x2u; queue.context[0x1b4] = 0x2u; const auto state = AgcDriver::Graphics::DecodeState(queue); const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state)); const std::array code{0xbf800000u | (opcode << 16u) | 1u, 0xf800180fu, 0x00000000u, 0xbf810000u}; ShaderRecompiler::RecompileRequest request{}; request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; request.context.waveSize = 64; request.context.pixel = pixel; request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; request.target.fragmentShaderBarycentricEnabled = true; request.layout.pushConstantSizeBytes = 128; request.useCache = false; return !ShaderRecompiler::Recompile(request).spirv.Words().empty(); } void meshIndexBufferTests() { const AgcDriver::Pm4::DrawParameters automatic{0, 96, 0, 1, 0, false}; const auto unindexed = AgcDriver::Graphics::MeshIndexBufferDescriptor(automatic); Require(unindexed[0] == 0 && unindexed[1] == 0 && unindexed[2] == 0 && unindexed[3] == 0x31016facu, "a non-indexed mesh draw did not get a null index buffer V#"); const AgcDriver::Pm4::DrawParameters indexed{0x123456789a00ull, 5, 2, 1, 0, true}; const auto words = AgcDriver::Graphics::MeshIndexBufferDescriptor(indexed); Require(words[0] == 0x56789a00u && words[1] == 0x1234u && words[2] == 12u && words[3] == 0x31016facu, "an indexed mesh draw's index buffer V# changed"); expectFailure([] { static_cast(AgcDriver::Graphics::MeshIndexBufferDescriptor(AgcDriver::Pm4::DrawParameters{0, 3, 2, 1, 0, true})); }, "invalid mesh index buffer range"); } void stagingPoolBudgetTests() { if (std::getenv("APS5_STAGING_POOL_MIB") != nullptr) return; VkPhysicalDeviceMemoryProperties memory{}; memory.memoryHeapCount = 2; memory.memoryHeaps[0] = {VkDeviceSize{32} << 30u, 0}; memory.memoryHeaps[1] = {VkDeviceSize{16} << 30u, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{2} << 30u, "the staging pool budget is not an eighth of the device-local heap"); memory.memoryHeaps[1].size = VkDeviceSize{2} << 30u; Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{512} << 20u, "the staging pool budget fell below 512 MiB"); memory.memoryHeapCount = 1; Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{512} << 20u, "a host heap sized the staging pool budget"); } void meshArgumentTests() { using AgcDriver::Graphics::MeshArguments; using AgcDriver::Graphics::ResolveMeshArguments; AgcDriver::Graphics::Context context{}; context.meshLimits.maxMeshWorkGroupCount[0] = 1000; context.meshLimits.maxMeshWorkGroupCount[1] = 600; context.meshLimits.maxMeshWorkGroupTotalCount = 4000; const ShaderRecompiler::MeshConfiguration points{1u, 1u, 1u, 1u, 1u, 64u, 1024u, 0u, 4u}; const ShaderRecompiler::MeshConfiguration triangles{4u, 32u, 96u, 96u, 32u, 128u, 2048u, 0u, 4u}; const ShaderRecompiler::MeshConfiguration strip{6u, 8u, 10u, 10u, 8u, 64u, 1024u, 0u, 4u}; const ShaderRecompiler::MeshConfiguration fan{5u, 8u, 10u, 10u, 8u, 64u, 1024u, 0u, 4u}; const auto same = [](const MeshArguments& a, const MeshArguments& b) { return a.groups == b.groups && a.instances == b.instances && a.layers == b.layers && a.indexCount == b.indexCount && a.firstIndex == b.firstIndex; }; const auto rules = [&](const ShaderRecompiler::MeshConfiguration& mesh, std::uint32_t indexCount) { return AgcDriver::Graphics::MeshArgumentRulesFor(context, mesh, indexCount); }; const auto record = [](std::uint32_t count, std::uint32_t instances, std::uint32_t first) { return AgcDriver::Pm4::DrawArguments{count, instances, first, 0, 0}; }; Require(same(ResolveMeshArguments(record(1, 512, 0), rules(points, 1)), {1, 512, 1, 1, 0}), "one point, 512 instances"); Require(same(ResolveMeshArguments(record(1, 0, 0), rules(points, 1)), {0, 0, 0, 1, 0}), "no instances draws nothing"); Require(same(ResolveMeshArguments(record(0, 4, 0), rules(points, 1)), {0, 0, 0, 0, 0}), "no indices draws nothing"); Require(same(ResolveMeshArguments(record(96, 2, 0), rules(triangles, 300)), {1, 2, 1, 96, 0}), "one full group"); Require(same(ResolveMeshArguments(record(99, 2, 0), rules(triangles, 300)), {2, 2, 1, 99, 0}), "a partial second group"); Require(same(ResolveMeshArguments(record(200, 3, 150), rules(triangles, 300)), {2, 3, 1, 150, 150}), "count clamped to the index buffer"); Require(same(ResolveMeshArguments(record(9, 1, 300), rules(triangles, 300)), {0, 0, 0, 0, 300}), "first index past the index buffer draws nothing"); Require(same(ResolveMeshArguments(record(2, 1, 0), rules(triangles, 300)), {0, 0, 0, 2, 0}), "no complete triangle draws nothing"); Require(same(ResolveMeshArguments(record(10, 1, 0), rules(strip, 64)), {1, 1, 1, 10, 0}), "eight strip triangles are one group"); Require(same(ResolveMeshArguments(record(11, 1, 0), rules(strip, 64)), {2, 1, 1, 11, 0}), "a ninth strip triangle starts a group"); Require(same(ResolveMeshArguments(record(10, 1, 0), rules(fan, 64)), {1, 1, 1, 10, 0}), "eight fan triangles are one group"); Require(same(ResolveMeshArguments(record(11, 1, 0), rules(fan, 64)), {2, 1, 1, 11, 0}), "a ninth fan triangle starts a group"); Require(same(ResolveMeshArguments(record(1, 601, 0), rules(points, 1)), {0, 0, 0, 1, 0}), "instances over the device limit draw nothing"); Require(same(ResolveMeshArguments(record(96 * 7, 600, 0), rules(triangles, 96 * 7)), {0, 0, 0, 96 * 7, 0}), "groups times instances over the device limit draw nothing"); Require(same(ResolveMeshArguments(record(96 * 6, 600, 0), rules(triangles, 96 * 6)), {6, 600, 1, 96 * 6, 0}), "groups times instances at the device limit"); Require(same(ResolveMeshArguments(record(0xffffffffu, 1, 0xfffffff0u), rules(points, 0xffffffffu)), {15, 1, 1, 15, 0xfffffff0u}), "first index near the end of a huge index buffer"); Require(same(ResolveMeshArguments(record(0xffffffffu, 1, 0), rules(points, 0xffffffffu)), {0, 0, 0, 0xffffffffu, 0}), "groups over the device limit draw nothing"); } void debugBranchTests() { for (const auto opcode : {0x17u, 0x18u, 0x19u, 0x1au}) Require(recompilesDebugBranch(opcode), "a conditional debug branch did not recompile"); } void highestDrawIndexTests() { using AgcDriver::Graphics::HighestDrawIndex; const auto bytesOf = [](const auto& values) { return std::as_bytes(std::span(values)); }; const std::array narrow{0, 7, 0xffff, 3, 0xffff}; Require(HighestDrawIndex(bytesOf(narrow), 2, true) == 7, "a 16-bit restart index counted as a vertex"); Require(HighestDrawIndex(bytesOf(narrow), 2, false) == 0xffff, "a 16-bit all-ones index without restart was skipped"); const std::array wide{9, 0xffffffffu, 0xffff, 2}; Require(HighestDrawIndex(bytesOf(wide), 4, true) == 0xffff, "a 32-bit restart index counted as a vertex, or 0xffff was taken for it"); Require(HighestDrawIndex(bytesOf(wide), 4, false) == 0xffffffffu, "a 32-bit all-ones index without restart was skipped"); const std::array restartOnly{0xffff, 0xffff, 0xffff, 0xffff}; Require(!HighestDrawIndex(bytesOf(restartOnly), 2, true).has_value(), "a draw of only restart indices reached a vertex"); Require(HighestDrawIndex(bytesOf(restartOnly), 2, false) == 0xffff, "a 16-bit all-ones index without restart was skipped"); Require(HighestDrawIndex(bytesOf(narrow).first(4), 2, true) == 7 && HighestDrawIndex(bytesOf(narrow).first(2), 2, true) == 0, "the scan read past its index range"); expectFailure([&] { HighestDrawIndex(bytesOf(narrow), 1, false); }, "unsupported index size"); } void storeAtFlipTests() { using AgcDriver::Graphics::StorageTexture; Require(!StorageTexture::StoreAtFlipRequested(nullptr), "an unset APS5_STORE_AT_FLIP stored at each flip"); Require(StorageTexture::StoreAtFlipRequested("1"), "APS5_STORE_AT_FLIP=1 did not store at each flip"); for (const char* value : {"0", "", "true", "11"}) expectFailure([&] { StorageTexture::StoreAtFlipRequested(value); }, "expected 1"); } void nullVertexDescriptorTests() { using AgcDriver::Graphics::DecodeVertexFormat; constexpr std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32B32_SFLOAT, VK_FORMAT_R32G32B32A32_SFLOAT}; ShaderRecompiler::VertexAttribute attribute{}; attribute.formatComponents = 1; for (std::uint32_t components = 1; components <= 4; ++components) { attribute.components = components; const auto format = DecodeVertexFormat(attribute); Require(format.format == formats[components - 1] && format.bytes == components * 4u && std::string_view(format.scalar) == "f32", "a null V# did not read as zero floats of the attribute's width"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100000, 3, 2) == format.bytes && AgcDriver::Graphics::VertexBufferExtent(attribute) == format.bytes, "a null V# was sized as a guest range"); } attribute.resource.fields = {0x1000, 0, 0, 0}; expectFailure([&] { DecodeVertexFormat(attribute); }, "unsupported vertex format"); } void vertexCopyTests() { using AgcDriver::Graphics::PlanVertexCopies; using AgcDriver::Graphics::VertexFetch; { const std::array fetches{{{0x1018, 0x1018 + 32 * 9 + 8, 32, 0, 4}, {0x1000, 0x1000 + 32 * 9 + 12, 32, 0, 4}, {0x100c, 0x100c + 32 * 9 + 12, 32, 0, 4}}}; const auto plan = PlanVertexCopies(fetches); Require(plan.copies.size() == 1 && plan.copies[0].first == 0x1000 && plan.copies[0].second == 0x1018 + 32 * 9 + 8, "interleaved attributes were not copied as one union"); Require(plan.copyOf == std::vector{0, 0, 0} && plan.offsets == std::vector{0x18, 0, 0xc}, "interleaved attribute offsets are wrong"); Require(plan.alignments == std::vector{4}, "the union's alignment is not its attributes'"); } { const std::array fetches{{{0x4000, 0x4100, 16, 0, 2}, {0x4004, 0x4104, 16, 0, 4}}}; const auto plan = PlanVertexCopies(fetches); Require(plan.copies.size() == 1 && plan.alignments == std::vector{4}, "a union did not take its strictest attribute alignment"); } { const std::array fetches{{ {0x2000, 0x2100, 32, 0, 4}, {0x2004, 0x2100, 16, 0, 4}, {0x2020, 0x2120, 32, 0, 4}, {0x2002, 0x2102, 32, 0, 4}, {0x2008, 0x2108, 32, 1, 4}, {0x2000, 0x2010, 0, 0, 4}, }}; const auto plan = PlanVertexCopies(fetches); Require(plan.copies.size() == 6, "fetches of other records, strides, rates or alignments shared a copy"); for (std::size_t i = 0; i < fetches.size(); ++i) { const auto& copy = plan.copies[plan.copyOf[i]]; Require(plan.offsets[i] == 0 && copy.first == fetches[i].begin && copy.second == fetches[i].end, "a lone fetch was not copied exactly"); } } { const std::array fetches{{{0x3000, 0x3100, 24, 0, 2}, {0x3002, 0x3102, 24, 0, 2}}}; const auto plan = PlanVertexCopies(fetches); Require(plan.copies.size() == 1 && plan.copies[0].second == 0x3102 && plan.offsets[1] == 2, "aligned 16-bit attributes of one record were not merged"); } { const std::array empty{{{0x4000, 0x4000, 16, 0, 4}}}; expectFailure([&] { PlanVertexCopies(empty); }, "empty vertex fetch"); } } void vertexZeroFillTests() { using AgcDriver::Graphics::CopyZeroPaddedDrawInput; using AgcDriver::Graphics::VertexBufferReadSize; constexpr std::uint32_t stride = 24; constexpr std::uint32_t records = 3; const ShaderRecompiler::VertexAttribute attribute{0, 4, {{0x1000u, stride << 16u, records, 77u << 12u}}, 0}; const auto extent = VertexBufferReadSize(attribute, records, 1); const auto valid = static_cast(stride) * records; std::vector guest(extent); std::fill(guest.begin(), guest.begin() + static_cast(valid), std::byte{0x7a}); std::fill(guest.begin() + static_cast(valid), guest.end(), std::byte{0x55}); const auto copy = CopyZeroPaddedDrawInput(mockContext(), reinterpret_cast(guest.data()), extent, valid); const auto bytes = copy.buffer->Bytes(); Require(bytes.size() == extent, "a zero-padded vertex fetch changed its size"); Require(std::all_of(bytes.begin(), bytes.begin() + static_cast(valid), [](std::byte value) { return value == std::byte{0x7a}; }), "a vertex fetch did not copy the bytes within the record count"); Require(std::all_of(bytes.begin() + static_cast(valid), bytes.end(), [](std::byte value) { return value == std::byte{0}; }), "a vertex fetch past the record count did not read as zeros"); } void vertexZeroFillMergeTests() { using AgcDriver::Graphics::PlanVertexCopies; using AgcDriver::Graphics::SoloZeroPaddedFetchIndices; using AgcDriver::Graphics::VertexFetch; const std::vector fetches{{0x1000u, 0x1000u + 88u, 24u, 0u, 4u}, {0x1010u, 0x1010u + 80u, 24u, 0u, 4u}}; Require(PlanVertexCopies(fetches).copies.size() == 1, "interleaved attributes share one vertex copy"); Require(SoloZeroPaddedFetchIndices(fetches, {88u, 80u}).empty(), "fully valid fetches share the straight copy"); const auto solo = SoloZeroPaddedFetchIndices(fetches, {88u, 72u}); Require(solo.size() == 1 && solo[0] == 1, "a fetch past the records is copied alone"); } void parallelCompareTests() { using AgcDriver::Graphics::CompareSpan; using AgcDriver::Graphics::CompareSpansFrom; constexpr std::size_t block = 65536; constexpr std::size_t count = 48; std::vector first(block * count + 100, std::byte{7}); auto second = first; const std::array changedAt{3 * block + 17, 20 * block, 33 * block + block - 1, 47 * block + 120}; for (const auto at : changedAt) second[at] = std::byte{9}; std::vector spans; for (std::size_t k = 0; k < count; ++k) { const auto length = k + 1 == count ? block + 100 : block; spans.push_back({first.data() + k * block, second.data() + k * block, length}); } const auto expected = [&](std::size_t k) { return std::none_of(changedAt.begin(), changedAt.end(), [&](std::size_t at) { return at >= k * block && at < k * block + spans[k].bytes; }); }; for (const std::size_t threshold : {std::size_t{0}, std::size_t{1} << 40u}) { for (int round = 0; round < 20; ++round) { std::vector equal(count, 2); CompareSpansFrom(spans, equal, threshold); for (std::size_t k = 0; k < count; ++k) Require((equal[k] == 1) == expected(k) && equal[k] <= 1, "parallel compare misreported a span"); } } std::vector left(count, 2), right(count, 2); std::thread other([&] { for (int round = 0; round < 50; ++round) CompareSpansFrom(spans, right, 0); }); for (int round = 0; round < 50; ++round) CompareSpansFrom(spans, left, 0); other.join(); for (std::size_t k = 0; k < count; ++k) Require((left[k] == 1) == expected(k) && (right[k] == 1) == expected(k), "concurrent parallel compares misreported a span"); std::vector none; CompareSpansFrom({}, none, 0); } int main(int argc, char** argv) { #ifdef _WIN32 _putenv_s("APS5_PIN_WAIT_MS", "200"); _putenv_s("APS5_HEAP_MIRROR_MIB", "4"); #else setenv("APS5_PIN_WAIT_MS", "200", 1); setenv("APS5_HEAP_MIRROR_MIB", "4", 1); #endif try { if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") { descriptorLayoutFlagTests(); std::cout << "Descriptor layout native and captured binding flags passed\n"; return 0; } if (argc == 2 && std::string_view(argv[1]) == "--descriptor-pool-reset-failure") { descriptorPoolResetFailureTest(); std::cerr << "descriptor pool reset failure did not terminate\n"; return 1; } { const AgcDriver::Graphics::Context context{}; const AgcDriver::Graphics::State state{}; AgcDriver::Pm4::DrawParameters draw{0, 0, 0, 1, 0, false}; AgcDriver::Graphics::Draw(context, state, draw, {}); draw.indexCount = 3; draw.instanceCount = 0; AgcDriver::Graphics::Draw(context, state, draw, {}); draw.instanceCount = 2; draw.firstInstance = 0xffffffffu; expectFailure([&] { AgcDriver::Graphics::Draw(context, state, draw, {}); }, "instance range overflow"); draw.firstInstance = 0; draw.firstVertex = 0xffffffffu; expectFailure([&] { AgcDriver::Graphics::Draw(context, state, draw, {}); }, "vertex range overflow"); draw.firstVertex = 0; draw.indexAddress = 1; expectFailure([&] { AgcDriver::Graphics::Draw(context, state, draw, {}); }, "must not reference an index buffer"); draw.indexAddress = 0; draw.flags = 1; expectFailure([&] { AgcDriver::Graphics::Draw(context, state, draw, {}); }, "draw modifiers"); auto bounded = state; bounded.depthBoundsTest = true; bounded.minDepthBounds = 0.25f; AgcDriver::Graphics::ValidateDepthBounds(context, bounded); bounded.minDepthBounds = 1.5f; expectFailure([&] { AgcDriver::Graphics::ValidateDepthBounds(context, bounded); }, "depth bounds outside [0, 1]"); auto unrestricted = context; unrestricted.depthRangeUnrestricted = true; AgcDriver::Graphics::ValidateDepthBounds(unrestricted, bounded); } RunGuestLeaseWaitTests(); stateTests(); hdrComponentSwapTests(); depthMaintenanceTests(); hardwareScreenOffsetTests(); srgb8TargetTests(); DepthClipTests(); DepthStencilTests(); OneDimensionalColorTests(); htileDepthClearTests(); ZExportTests(); unorm10_11_11TargetTests(); DepthBoundsBiasTests(); conservativeZExportTests(); orderedPixelShaderTests(); stencilRefExportTests(); ProvokingVertexTests(); ProvokingVertexRenderPassTests(); ConservativeRasterizationTests(); DisabledColorTests(); OpaqueDestinationAlphaTests(); Color1555TargetTests(); CompactedExportTests(); ReversedComponentOrderTests(); ShadersWriteMemoryTests(); DualSourceBlendTests(); metadataPassTests(); cmaskTests(); uint16ExportTests(); uint8x4TargetTests(); multisampleTests(); MsaaEnableSingleSampleTests(); ShaderStageTests(); NggVertexWorkgroupTests(); TuningFieldTests(); PixelInputLayoutTests(); UnexportedPixelInputTests(); ColorPipeBankXorTests(); HeaderlessVertexStageTests(); HiddenUserWordVertexTablesTests(); ComputeScratchTests(); shaderUserDataTailPaddingTests(); InitialContextTests(); AlphaToCoverageTests(); descriptorPoolTests(); pushConstantTests(); resourceTests(); descriptorSnapshotTests(); descriptorCacheTests(); misalignedShaderDataTests(); debugBranchTests(); textureCacheBudgetTests(); sampledTextureBudgetTests(); sampledBudgetReportTests(); meshArgumentTests(); meshIndexBufferTests(); stagingPoolBudgetTests(); validationTests(); vertexCopyTests(); highestDrawIndexTests(); storeAtFlipTests(); nullVertexDescriptorTests(); vertexZeroFillTests(); vertexZeroFillMergeTests(); parallelCompareTests(); pixelParameterSlotTests(); rectListTests(); floatControlsModeTests(); barycentricEmulationTests(); mock = MockVulkan{}; auto bdaContext = mockContext(); bdaContext.bufferDeviceAddress = true; bdaContext.limits.maxStorageBufferRange = 1u << 27; RunBdaResourceTests(bdaContext, { [](VkBuffer buffer) -> std::span { return mockBufferMemory(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"); }, [](VkDeviceAddress address) { const auto offset = address - 0x100000000000ULL; const auto buffer = reinterpret_cast(offset / 0x10000); return mockBufferMemory(buffer).subspan(offset % 0x10000); }, [](std::optional headroom) { mock.memoryLimit = headroom.has_value() ? std::optional(mock.allocatedBytes + *headroom) : std::nullopt; }, [] { return mock.allocationAttempts; } }); Require(mock.live == 0, "BDA resources leaked Vulkan objects"); RunGuestAllocationTests(); RunUnmappedGapTests(); RunColorTargetLayoutTests(); RunLiveStackAccessTests(); RunTextureFormatTests(); RunTextureTilingTests(); RunGuestTextureResourceTests(); RunDepthSurfaceReuseTests(); RunGuestSamplerResourceTests(); mock = MockVulkan{}; auto textureDetilerContext = mockContext(); textureDetilerContext.limits.minStorageBufferOffsetAlignment = 16; textureDetilerContext.limits.maxStorageBufferRange = 256; RunTextureDetilerTests(textureDetilerContext, { [](std::uint64_t size) { VkBuffer buffer{}; VkBufferCreateInfo bufferInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; bufferInfo.size = size; bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; mockCreateBuffer(VK_NULL_HANDLE, &bufferInfo, nullptr, &buffer); VkMemoryRequirements requirements{}; mockGetBufferMemoryRequirements(VK_NULL_HANDLE, buffer, &requirements); VkMemoryAllocateInfo allocationInfo{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocationInfo.allocationSize = requirements.size; VkDeviceMemory memory{}; mockAllocateMemory(VK_NULL_HANDLE, &allocationInfo, nullptr, &memory); mockBindBufferMemory(VK_NULL_HANDLE, buffer, memory, 0); return buffer; }, [](VkBuffer buffer) -> std::vector& { return mock.memories.at(mock.bufferMemory.at(buffer)); }, [] { Require(mock.writes.size() >= 2, "expected texture detiling descriptor writes"); const auto& destinationWrite = mock.writes.back(); const auto& sourceWrite = mock.writes[mock.writes.size() - 2]; DetilerCapture capture{}; capture.groupsX = mock.lastDispatchGroups.x; capture.groupsY = mock.lastDispatchGroups.y; capture.groupsZ = mock.lastDispatchGroups.z; capture.pushConstants = mock.lastPushConstants; capture.sourceBuffer = sourceWrite.buffers.at(0).buffer; capture.sourceOffset = sourceWrite.buffers.at(0).offset; capture.sourceRange = sourceWrite.buffers.at(0).range; capture.destinationBuffer = destinationWrite.buffers.at(0).buffer; capture.destinationOffset = destinationWrite.buffers.at(0).offset; capture.destinationRange = destinationWrite.buffers.at(0).range; return capture; }, [] { return mock.pipelineCreateCount; }, [] { return mock.pipelineSpecializations.back(); } }); std::cout << "Graphics validation tests passed\n"; return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } }