mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-22 08:33:12 -05:00
A read-only V# the build clamps (size above the descriptor range limit cut to the mapped end, stride overhang cut to the allocation end) was compared with the template by its raw size, so an unchanged buffer counted as moved, was refused over the limit and the dispatch rebuilt every time; a moved one with an overhang was snapshotted past the allocation end. The build and the move proof now take the range from one function (ShaderResources::guestBufferBytes), compare clamped with clamped and snapshot the clamped range. An overlay without a descriptor set is refused like one that could not be made, and recording one throws instead of binding the template's set.
8253 lines
563 KiB
C++
8253 lines
563 KiB
C++
#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/PipelineJournal.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/GuestBufferMemory.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/GuestAllocations.hpp"
|
|
#include "prx/libc/include/general/VabiMacros.hpp"
|
|
#include "prx/libc/include/GuestHeap.hpp"
|
|
#include "ImageTableAbi.hpp"
|
|
#include "SpirvBackend/SpirvSpecialization.hpp"
|
|
#include <spirv/unified1/spirv.hpp>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <bit>
|
|
#include <chrono>
|
|
#include <cstddef>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <exception>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <random>
|
|
#include <initializer_list>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <span>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <tuple>
|
|
#include <type_traits>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
extern "C" std::terminate_handler APS5_VABI _ZSt13set_terminatePFvvE_nid_postfix(std::terminate_handler handler);
|
|
|
|
namespace {
|
|
|
|
using AgcDriver::Graphics::Require;
|
|
|
|
alignas(256) std::array<std::byte, 1024> colorMemory{};
|
|
alignas(256) std::array<std::byte, 2048> sliceMemory{};
|
|
alignas(256) std::array<std::uint8_t, 4> volumeKeyMemory{};
|
|
|
|
AgcDriver::QueueState makeState() {
|
|
AgcDriver::QueueState queue;
|
|
queue.userConfig[0x242] = 4;
|
|
queue.context = {
|
|
{0x2d5, 0x2000},
|
|
{0x1b6, 0}, {0x1b8, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800},
|
|
{0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0},
|
|
{0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa},
|
|
{0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff},
|
|
{0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240},
|
|
{0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010},
|
|
{0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028},
|
|
{0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u},
|
|
{0x3b8, 0x9000000}, {0x1e0, 0},
|
|
{0xc, 0}, {0xd, 0x40040},
|
|
{0x81, 0x80000000}, {0x82, 0x40040},
|
|
{0x90, 0x80000000}, {0x91, 0x40040},
|
|
{0x94, 0x80000000}, {0x95, 0x40040}
|
|
};
|
|
const auto address = reinterpret_cast<std::uintptr_t>(colorMemory.data());
|
|
queue.context[0x318] = static_cast<std::uint32_t>(address >> 8u);
|
|
queue.context[0x390] = static_cast<std::uint32_t>(address >> 40u);
|
|
queue.context[0x10f] = std::bit_cast<std::uint32_t>(32.0f);
|
|
queue.context[0x110] = std::bit_cast<std::uint32_t>(32.0f);
|
|
queue.context[0x111] = std::bit_cast<std::uint32_t>(-2.0f);
|
|
queue.context[0x112] = std::bit_cast<std::uint32_t>(2.0f);
|
|
queue.context[0x113] = std::bit_cast<std::uint32_t>(1.0f);
|
|
queue.context[0x114] = 0;
|
|
queue.context[0xb4] = 0;
|
|
queue.context[0xb5] = std::bit_cast<std::uint32_t>(1.0f);
|
|
queue.shader[0x008] = 0x100;
|
|
queue.shader[0x009] = 0;
|
|
return queue;
|
|
}
|
|
|
|
template<typename TAction>
|
|
void expectFailure(TAction action, std::string_view reason) {
|
|
try {
|
|
action();
|
|
} catch (const std::runtime_error& error) {
|
|
Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string("unexpected failure: ") + error.what());
|
|
return;
|
|
}
|
|
throw std::runtime_error("expected graphics rejection: " + std::string(reason));
|
|
}
|
|
|
|
void inactiveVertexPointSizeTests() {
|
|
const auto pointSizeState = [] {
|
|
auto queue = makeState();
|
|
queue.context[0x1b3] = queue.context[0x1b4] = 2;
|
|
queue.context[0x207] = 0x01210000;
|
|
return queue;
|
|
};
|
|
for (const auto primitive : {2u, 4u, 5u, 6u, 7u, 17u}) {
|
|
auto queue = pointSizeState();
|
|
queue.userConfig[0x242] = primitive;
|
|
for (const auto raster : {0u, 0x240u, 0x248u}) {
|
|
queue.context[0x205] = raster;
|
|
const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false);
|
|
Require(rejection.empty(), "inactive vertex point size failed the draw precheck: " + rejection);
|
|
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
|
|
}
|
|
}
|
|
for (const auto raster : {0u, 0x240u, 0x248u}) {
|
|
auto queue = pointSizeState();
|
|
queue.userConfig[0x242] = 3;
|
|
queue.context[0x205] = raster;
|
|
const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false);
|
|
Require(rejection.empty(), "inactive line-strip point size failed the draw precheck: " + rejection);
|
|
const auto state = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && !state.primitiveRestart, "inactive point size changed line-strip decoding");
|
|
Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Line, "inactive point size changed the line primitive class");
|
|
queue.userConfig[0x24b] = 1;
|
|
queue.context[0x103] = 0xffffffffu;
|
|
const auto indexedRejection = AgcDriver::Graphics::DrawRejection(queue, true);
|
|
Require(indexedRejection.empty(), "inactive line-strip point size with primitive restart failed the draw precheck: " + indexedRejection);
|
|
const auto indexedState = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(indexedState.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && indexedState.primitiveRestart, "inactive point size changed indexed line-strip decoding");
|
|
}
|
|
const auto rejected = [](AgcDriver::QueueState queue) {
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("auxiliary vertex exports") != std::string::npos, "unsupported vertex exports passed the draw precheck");
|
|
try {
|
|
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
|
|
} catch (const std::runtime_error&) {
|
|
return;
|
|
}
|
|
throw std::runtime_error("potentially active point size passed the state decoder");
|
|
};
|
|
for (const auto primitive : {1u, 9u}) {
|
|
auto queue = pointSizeState();
|
|
queue.userConfig[0x242] = primitive;
|
|
rejected(queue);
|
|
}
|
|
for (const auto stages : {0x2004u, 0x2020u}) {
|
|
auto queue = pointSizeState();
|
|
queue.context[0x2d5] = stages;
|
|
rejected(queue);
|
|
}
|
|
for (const auto raster : {8u, 0x208u, 0x48u}) {
|
|
auto queue = pointSizeState();
|
|
queue.context[0x205] = raster;
|
|
rejected(queue);
|
|
}
|
|
for (const auto flag : {1u << 17u, 1u << 20u}) {
|
|
auto queue = pointSizeState();
|
|
queue.context[0x207] = 0x01210000 | flag;
|
|
rejected(queue);
|
|
}
|
|
for (const auto distances : {1u, 0x100u, 0x00c000ffu, 0x00c0ff00u}) {
|
|
auto queue = pointSizeState();
|
|
queue.context[0x207] |= distances;
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "inactive point size rejected supported clip/cull distances");
|
|
Require(AgcDriver::Graphics::DecodeState(queue).paClVsOutCntl == queue.context[0x207], "inactive point size discarded clip/cull distance state");
|
|
}
|
|
{
|
|
auto queue = pointSizeState();
|
|
queue.context[0x207] |= 0x004001ffu;
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "inactive point size bypassed the clip/cull count limit");
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "more than eight clip and cull distances");
|
|
}
|
|
for (const auto offset : {0x2d5u, 0x205u}) {
|
|
auto queue = pointSizeState();
|
|
queue.context.erase(offset);
|
|
rejected(queue);
|
|
}
|
|
auto queue = pointSizeState();
|
|
queue.userConfig.erase(0x242);
|
|
rejected(queue);
|
|
}
|
|
|
|
void retainedPixelStageTests() {
|
|
using namespace AgcDriver::Graphics;
|
|
auto queue = makeState();
|
|
queue.context[0x8e] = queue.context[0x8f] = queue.context[0x203] = 0;
|
|
for (const auto format : {0u, 1u, 2u, 3u, 9u}) {
|
|
queue.context[0x1c4] = format;
|
|
Require(PixelProgramSkipped(queue), "a retained export format forced an unused pixel program to run");
|
|
Require(DrawRejection(queue, true).empty(), "an unused pixel program with a retained export format was rejected");
|
|
for (const auto control : {1u, 2u, 4u, 0x40u, 0x80u, 0x100u, 0x200u, 0x400u, 0x20000u}) {
|
|
queue.context[0x203] = control;
|
|
Require(!PixelProgramSkipped(queue), "an enabled export or execution flag was skipped");
|
|
}
|
|
queue.context[0x203] = 0;
|
|
queue.context[0x8e] = 0xf0000000u;
|
|
queue.context[0x8f] = 0x0f000000u;
|
|
Require(PixelProgramSkipped(queue), "disjoint color masks ran an unused pixel program");
|
|
queue.context[0x8f] = 0xf0000000u;
|
|
Require(!PixelProgramSkipped(queue), "an enabled high render target lost its pixel program");
|
|
queue.context[0x8e] = queue.context[0x8f] = 0;
|
|
}
|
|
queue.context[0x1b3] = queue.context[0x1b4] = 2;
|
|
for (const auto [format, control] : {std::pair{1u, 1u}, std::pair{2u, 2u}, std::pair{9u, 0x100u}, std::pair{9u, 0x103u}}) {
|
|
queue.context[0x1c4] = format;
|
|
queue.context[0x203] = control;
|
|
const auto state = DecodeState(queue);
|
|
Require(DrawRejection(queue, true).empty(), "a valid pixel export combination was rejected");
|
|
const auto pixel = DecodePixelStageInfo(queue.context, ExportMappings(state), PixelProgramSkipped(queue));
|
|
Require(pixel.depthExportEnable == ((control & 1u) != 0) && pixel.stencilRefExportEnable == ((control & 2u) != 0) && pixel.sampleMaskExportEnable == ((control & 0x100u) != 0), "decoding lost an enabled pixel export");
|
|
}
|
|
queue.context[0x203] = 0;
|
|
for (const auto alpha : {0xaa00u, 0xaa01u}) {
|
|
queue.context[0x2dc] = alpha;
|
|
Require(PixelProgramSkipped(queue) == ((alpha & 1u) == 0), "alpha-to-coverage enable was not respected");
|
|
}
|
|
queue.context[0x2dc] = 0xaa00u;
|
|
for (const auto offset : {0x8eu, 0x8fu, 0x203u}) {
|
|
auto missing = queue;
|
|
missing.context.erase(offset);
|
|
Require(!PixelProgramSkipped(missing), "incomplete execution state skipped a bound pixel program");
|
|
}
|
|
}
|
|
|
|
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<std::uintptr_t>(sliceMemory.data());
|
|
queue.context[0x318] = static_cast<std::uint32_t>(address >> 8u);
|
|
queue.context[0x390] = static_cast<std::uint32_t>(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<std::uint32_t>(float(i + 1) / 8.0f);
|
|
state = DecodeState(queue);
|
|
Require(state.blendConstants == std::array<float, 4>{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<float, 4>{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<std::uint32_t>(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<std::uintptr_t>(colorMemory.data()) && state.color.bytes == colorMemory.size(), "render-target address or size changed");
|
|
Require(state.viewport.y == 4 && state.viewport.height == -4, "negative viewport height was lost");
|
|
Require(state.color.format == VK_FORMAT_R8G8B8A8_UNORM, "RGBA format changed");
|
|
queue.userConfig[0x24b] = 1;
|
|
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("out-of-order") == 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("out-of-order") == 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("out-of-order") != std::string::npos, "out-of-order rasterization was accepted");
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "out-of-order");
|
|
queue.context[0x293] = 0x06030000u;
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("out-of-order") == std::string::npos, "per-sample iteration of a single-sample target was rejected");
|
|
Require(!AgcDriver::Graphics::DecodeState(queue).perSampleShading, "per-sample iteration of a single-sample target requested sample shading");
|
|
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<AgcDriver::Graphics::RegisterRead> log;
|
|
{
|
|
const AgcDriver::Graphics::RegisterReadLogScope logScope(&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<void>(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state)));
|
|
}
|
|
Require(!log.empty(), "the register facade recorded nothing");
|
|
const auto recorded = log.size();
|
|
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
|
|
Require(log.size() == recorded, "the register facade recorded without an open log");
|
|
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<void>(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<std::uint8_t, 8>{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<std::uint8_t, 8>{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 retained Z export format ran a pixel program with depth exports disabled");
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a depth-only draw with a retained Z export format was rejected");
|
|
for (const auto control : {1u, 2u, 4u, 0x40u, 0x80u, 0x100u, 0x200u, 0x400u, 0x20000u}) {
|
|
queue.context[0x203] = control;
|
|
Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "an enabled pixel export or execution flag was ignored with a retained Z format");
|
|
}
|
|
queue.context[0x203] = 0;
|
|
queue.context[0x2dc] = 0xaa01;
|
|
Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "alpha-to-coverage skipped the pixel program");
|
|
queue.context[0x203] = 0x800;
|
|
Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "the Vulkan alpha-to-coverage state lost its pixel program with DB's override set");
|
|
queue.context[0x203] = queue.context[0x2dc] = 0;
|
|
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<VkPhysicalDevice>(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 UnwrittenSampleLocationTests() {
|
|
using AgcDriver::Graphics::DecodeState;
|
|
using AgcDriver::Graphics::DrawRejection;
|
|
using AgcDriver::Graphics::SampleLocations;
|
|
const auto fresh = [](std::uint32_t config, std::uint32_t attributes, std::uint32_t scanMode = 3) {
|
|
auto written = makeState();
|
|
written.context[0x2f8] = config;
|
|
written.context[0x292] = scanMode;
|
|
written.context[0x3b8] = 0x0dc00000u;
|
|
written.context[0x31d] = attributes;
|
|
written.context[0x1b3] = 2;
|
|
written.context[0x1b4] = 2;
|
|
AgcDriver::QueueState queue;
|
|
queue.shader = written.shader;
|
|
queue.userConfig = written.userConfig;
|
|
written.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { queue.context[offset] = value; });
|
|
return queue;
|
|
};
|
|
AgcDriver::QueueState initial;
|
|
for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) Require(initial.context.contains(offset) && initial.context.at(offset) == 0, "an unwritten PA_SC_AA_SAMPLE_LOCS register did not hold its clear-state value 0");
|
|
for (const auto samples : {2u, 4u, 8u}) {
|
|
const auto log2 = static_cast<std::uint32_t>(std::countr_zero(samples));
|
|
auto queue = fresh((log2 << 20u) | log2, (log2 << 12u) | (log2 << 15u));
|
|
const auto state = DecodeState(queue);
|
|
Require(state.samples == samples && state.colors.size() == 1 && state.colors[0].samples == samples, "a multisampled draw with unwritten sample locations did not decode its sample count");
|
|
Require(state.customSampleLocations, "unwritten sample locations did not decode as the clear-state pattern");
|
|
for (std::uint32_t sample = 0; sample < samples; ++sample) Require(state.sampleLocations[sample] == 0, "an unwritten sample location was not at the pixel center");
|
|
const auto centered = SampleLocations(state);
|
|
for (std::uint32_t sample = 0; sample < samples; ++sample) Require(centered[sample].x == 0.5f && centered[sample].y == 0.5f, "unwritten sample locations did not place every sample at the pixel center");
|
|
const auto rejection = DrawRejection(queue, false);
|
|
Require(rejection.empty(), "the precheck refused a multisampled draw with unwritten sample locations: " + rejection);
|
|
}
|
|
auto queue = fresh(0x00200002, 0x12000);
|
|
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae;
|
|
auto state = DecodeState(queue);
|
|
Require(state.samples == 4 && !state.customSampleLocations, "explicit standard 4x sample locations changed");
|
|
queue = fresh(0x00200002, 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, "explicit custom 4x sample locations changed");
|
|
const auto custom = SampleLocations(state);
|
|
const std::array<VkSampleLocationEXT, 4> 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(custom[sample].x == expected[sample].x && custom[sample].y == expected[sample].y, "explicit custom 4x sample coordinates changed");
|
|
queue.ClearContext();
|
|
for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) Require(queue.context.at(offset) == 0, "CLEAR_STATE did not reset PA_SC_AA_SAMPLE_LOCS");
|
|
expectFailure([&] { DecodeState(fresh(0x00400004, 0x12000)); }, "more than eight samples");
|
|
expectFailure([&] { DecodeState(fresh(0x00100002, 0x12000)); }, "exposing fewer samples");
|
|
queue = fresh(0, 0, 3);
|
|
state = DecodeState(queue);
|
|
Require(state.samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target with unwritten sample locations was refused");
|
|
}
|
|
|
|
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[0x203] &= ~0x800u;
|
|
queue.context[0x2dc] = 0xaa01;
|
|
refused(queue, "MSAA_ENABLE on a single-sample target with alpha-to-coverage");
|
|
queue = enabled();
|
|
queue.context[0x2dc] = 0xaa01;
|
|
refused(queue, "MSAA_ENABLE on a single-sample target with alpha-to-coverage");
|
|
queue.context[0x2dc] = 0x18701;
|
|
Require(!DecodeState(queue).alphaToCoverage && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target ignored the dithered alpha disable override");
|
|
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[0x203] &= ~0x800u;
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
{
|
|
auto vertex = makeState();
|
|
vertex.context[0x2d5] = 0x02002008u;
|
|
const auto state = AgcDriver::Graphics::DecodeState(vertex);
|
|
Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Vertex
|
|
&& state.stages.registerValue == 0x02002008u,
|
|
"the reported vertex-routing state did not decode as vertex");
|
|
vertex.context[0x2d5] |= 0x10u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(vertex); }, "reserved LS_EN, ES_EN or VS_EN");
|
|
vertex.context[0x2d5] = 0x02002008u | 0x800000u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(vertex); }, "unsupported vertex routing");
|
|
vertex.context[0x2d5] = 0x2008u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(vertex); }, "unsupported vertex routing");
|
|
vertex.context.erase(0x2d5);
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(vertex); }, "missing register");
|
|
}
|
|
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");
|
|
queue.shader[0x8a] = 0x222c0007u;
|
|
queue.shader[0x8b] = 0x00c00010u;
|
|
stages = AgcDriver::Graphics::DecodeState(queue).stages;
|
|
Require(stages.path == AgcDriver::Graphics::ShaderPath::Geometry && stages.mesh && stages.mesh->primitivesPerGroup == 21 && stages.mesh->esgsItemSize == 4, "a geometry program with fewer GS and ES input VGPRs did not decode");
|
|
queue.shader[0x8a] = 3u << 29u;
|
|
queue.shader[0x8b] = 3u << 16u;
|
|
{
|
|
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");
|
|
}
|
|
{
|
|
auto fast = makeState();
|
|
fast.userConfig[0x242] = 1;
|
|
fast.context[0x2d5] = 0x482030;
|
|
fast.userConfig[0x25b] = 0x201;
|
|
fast.context[0x1ff] = 32;
|
|
fast.context[0x2ce] = 32;
|
|
fast.context[0x2d3] = 64;
|
|
fast.context[0x29b] = 2;
|
|
fast.context[0x2ab] = 4;
|
|
fast.shader[0x8a] = 0x022c0245;
|
|
fast.shader[0x8b] = 0x9002a;
|
|
const auto state = AgcDriver::Graphics::DecodeState(fast);
|
|
Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.vertexWaveSize == 32 && state.stages.mesh, "a fast-launch draw did not decode as wave32 geometry");
|
|
const auto& mesh = *state.stages.mesh;
|
|
Require(mesh.inputPrimitive == 1 && mesh.primitivesPerGroup == 1 && mesh.verticesPerGroup == 1 && mesh.maxVertices == 32 && mesh.maxPrimitives == 64 && mesh.threadsPerGroup == 64 && mesh.ldsSizeDwords == 128, "a fast-launch subgroup did not decode as one point amplified by PRIM_AMP_FACTOR");
|
|
fast.context[0x29b] = 1;
|
|
const auto fastLines = AgcDriver::Graphics::DecodeState(fast);
|
|
Require(fastLines.stages.mesh->outputPrimitiveSize == 2 && fastLines.stages.mesh->maxPrimitives == 64 && AgcDriver::Graphics::RasterPrimitiveOf(fastLines) == ShaderRecompiler::RasterPrimitive::Line, "a fast-launch line strip did not take its line count from PRIM_AMP_FACTOR");
|
|
fast.context[0x29b] = 2;
|
|
fast.context[0x2d5] = 0x502030;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "unsupported geometry routing, fast launch or wave-ID state");
|
|
fast.context[0x2d5] = 0x482030;
|
|
fast.userConfig[0x25b] = 0x402;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup");
|
|
fast.userConfig[0x25b] = 0x201;
|
|
fast.userConfig[0x242] = 4;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup");
|
|
fast.userConfig[0x242] = 1;
|
|
fast.context[0x2d3] = 0;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "amplifies to no primitives");
|
|
}
|
|
{
|
|
auto lines = makeState();
|
|
lines.userConfig[0x242] = 1;
|
|
lines.context[0x2d5] = 0x2030;
|
|
lines.userConfig[0x25b] = 0x6633;
|
|
lines.context[0x1ff] = 255;
|
|
lines.context[0x2ce] = 5;
|
|
lines.context[0x29b] = 1;
|
|
lines.context[0x2ab] = 4;
|
|
lines.shader[0x8a] = 3u << 29u;
|
|
lines.shader[0x8b] = 3u << 16u;
|
|
auto state = AgcDriver::Graphics::DecodeState(lines);
|
|
Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.mesh && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Line, "a geometry program that outputs line strips did not decode as geometry rasterized as lines");
|
|
auto mesh = *state.stages.mesh;
|
|
Require(mesh.inputPrimitive == 1 && mesh.primitivesPerGroup == 51 && mesh.verticesPerGroup == 51 && mesh.maxVertices == 255 && mesh.maxPrimitives == 255 && mesh.threadsPerGroup == 256 && mesh.outputPrimitiveSize == 2, "a line-strip subgroup did not decode as 51 points with a line slot per output vertex");
|
|
lines.context[0x29b] = 2;
|
|
state = AgcDriver::Graphics::DecodeState(lines);
|
|
mesh = *state.stages.mesh;
|
|
Require(mesh.maxPrimitives == 153 && mesh.threadsPerGroup == 256 && mesh.outputPrimitiveSize == 3 && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Triangle, "the triangle-strip subgroup of the same draw changed");
|
|
lines.context[0x29b] = 1;
|
|
lines.context[0x2ce] = 1;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "unsupported geometry input or output assembly");
|
|
lines.context[0x2ce] = 5;
|
|
for (const auto output : {0u, 3u, 0x80000001u, 0x101u}) {
|
|
lines.context[0x29b] = output;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported");
|
|
}
|
|
lines.userConfig[0x242] = 4;
|
|
lines.context[0x205] = 0x2a40;
|
|
lines.context[0x29b] = 2;
|
|
Require(AgcDriver::Graphics::DecodeState(lines).stages.mesh->primitivesPerGroup == 17, "triangle input with a triangle-strip output and a triangle polygon offset did not decode");
|
|
lines.context[0x29b] = 1;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "polygon mode, depth bias or nonstandard rasterization");
|
|
}
|
|
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.userConfig[0x25b] = 0x1fe55;
|
|
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<AgcDriver::Graphics::RegisterRead> tessellationLog;
|
|
{
|
|
const AgcDriver::Graphics::RegisterReadLogScope logScope(&tessellationLog);
|
|
stages = AgcDriver::Graphics::DecodeState(queue).stages;
|
|
}
|
|
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 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f && tessellation.patchesPerGroup == 85 && tessellation.primitiveGroupSize == 85 && !tessellation.breakAtInstance, "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, "fractional-odd partitioning"}, std::pair{0x40045u, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) {
|
|
queue.context[0x2db] = parameters;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason);
|
|
}
|
|
queue.context[0x2db] = 0x40041;
|
|
Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->partitioning == 0, "integer-partitioned triangle patches decoded differently");
|
|
queue.context[0x2db] = 0x4004d;
|
|
Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->partitioning == 3, "fractional-even-partitioned triangle patches decoded differently");
|
|
queue.context[0x2d6] = 0x1043f;
|
|
queue.context[0x2db] = 0x4006a;
|
|
queue.context[0x286] = 0x41100000;
|
|
queue.userConfig[0x25b] = 0x5fe3f;
|
|
const auto quadStages = AgcDriver::Graphics::DecodeState(queue).stages;
|
|
const auto& quads = quadStages.tessellation;
|
|
Require(quads && quads->inputControlPoints == 4 && quads->outputControlPoints == 4 && quads->domain == 2 && quads->partitioning == 2 && quads->outputTopology == 3 && quadStages.minTessellationLevel == 1.0f && quadStages.maxTessellationLevel == 9.0f && quads->patchesPerGroup == 63 && quads->primitiveGroupSize == 63 && quads->breakAtInstance, "counter-clockwise quad patches with HOS levels [1, 9], 63 patches per group and BREAK_WAVE_AT_EOI decoded differently");
|
|
queue.userConfig[0x25b] = 0x8040;
|
|
const auto shared = AgcDriver::Graphics::DecodeState(queue).stages.tessellation;
|
|
Require(shared && shared->primitiveGroupSize == 64 && !shared->breakAtInstance, "GE_CNTL 0x8040 did not decode as 64 primitives per group with BREAK_WAVE_AT_EOI clear");
|
|
queue.userConfig.erase(0x25b);
|
|
const auto missing = AgcDriver::Graphics::DecodeState(queue).stages.tessellation;
|
|
Require(missing->primitiveGroupSize == 0 && !missing->breakAtInstance, "a draw without GE_CNTL decoded a primitive group size or BREAK_WAVE_AT_EOI");
|
|
queue.context[0x2db] = 0x40069;
|
|
Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->outputTopology == 3, "counter-clockwise triangle patches decoded differently");
|
|
for (const auto [maximum, minimum, reason] : {std::tuple{0x3f800000u, 0x40000000u, "tessellation level clamps"}, std::tuple{0x41100000u, 0xbf800000u, "tessellation level clamps"}, std::tuple{0x3f000000u, 0x00000000u, "tessellation level clamps"}, std::tuple{0x42800000u, 0x42800000u, "tessellation level clamps"}, std::tuple{0x7fc00000u, 0x3f800000u, "non-finite register"}, std::tuple{0x7f800000u, 0x3f800000u, "non-finite register"}}) {
|
|
queue.context[0x286] = maximum;
|
|
queue.context[0x287] = minimum;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason);
|
|
}
|
|
queue.context[0x2d6] = 0xc355;
|
|
queue.context[0x2db] = 0x40049;
|
|
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.context[0x2d6] = 0xc300;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "NUM_PATCHES is zero");
|
|
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");
|
|
}
|
|
queue.context[0x2d5] = 0x12010;
|
|
queue.context[0x29b] = 1;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported");
|
|
}
|
|
|
|
constexpr std::array<std::uint8_t, 8> IdentityExports{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u};
|
|
|
|
std::vector<spv::BuiltIn> 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<std::uint32_t, 3> 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<std::uint32_t, spv::BuiltIn> builtins;
|
|
std::vector<spv::BuiltIn> read;
|
|
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
|
const auto op = static_cast<spv::Op>(words[at] & 0xffffu);
|
|
if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn) builtins[words[at + 1]] = static_cast<spv::BuiltIn>(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<spv::BuiltIn>& read, spv::BuiltIn builtin) {
|
|
return std::find(read.begin(), read.end(), builtin) != read.end();
|
|
}
|
|
|
|
std::vector<std::uint32_t> pixelNoPerspectiveLocations(bool barycentricEnabled, std::uint32_t inputs = 0x22u) {
|
|
auto queue = makeState();
|
|
queue.context[0x1b3] = inputs;
|
|
queue.context[0x1b4] = inputs;
|
|
queue.context[0x1b6] = 2u;
|
|
queue.context[0x191] = 0u;
|
|
queue.context[0x192] = 1u;
|
|
const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports);
|
|
const std::array<std::uint32_t, 8> 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<std::uint32_t, std::uint32_t> locations;
|
|
std::vector<std::uint32_t> 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<spv::Op>(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");
|
|
if (!barycentricEnabled) Require(!result.barycentricEmulation.active, "v_interp_p1/p2 of their own I/J pairs did not keep host interpolation");
|
|
std::vector<std::uint32_t> 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<std::byte> 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<void>(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<std::uint32_t, 1> 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<std::byte> header(userDataOffset + userDataBytes);
|
|
Shader shader{};
|
|
shader.user_data = reinterpret_cast<ShaderUserData*>(header.data() + userDataOffset);
|
|
std::memcpy(header.data(), &shader, sizeof(shader));
|
|
const auto headerAddress = reinterpret_cast<std::uintptr_t>(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<std::byte> storage(2 * blockBytes + side * side * 4);
|
|
const auto block = (reinterpret_cast<std::uintptr_t>(storage.data()) + blockBytes - 1) / blockBytes * blockBytes;
|
|
auto queue = makeState();
|
|
queue.context[0x3b8] |= static_cast<std::uint32_t>(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<std::uint32_t>(address >> 8u);
|
|
queue.context[0x390] = static_cast<std::uint32_t>(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<std::uint32_t>(address >> 8u);
|
|
queue.context[0x390] = static_cast<std::uint32_t>(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));
|
|
}
|
|
queue.context[0x3b8] = 0x0dc68000u;
|
|
for (const std::uint32_t pipeBankXor : {0u, 0x4600u, 0xcf00u}) {
|
|
const auto address = block + pipeBankXor;
|
|
queue.context[0x318] = static_cast<std::uint32_t>(address >> 8u);
|
|
queue.context[0x390] = static_cast<std::uint32_t>(address >> 40u);
|
|
const auto color = AgcDriver::Graphics::DecodeState(queue).color;
|
|
Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::D64KBX && color.address == block && color.surfaceAddress == block && color.pipeBankXor == pipeBankXor, "a SW_64KB_D_X color base did not split its block address and XOR");
|
|
}
|
|
queue.context[0x3b8] = 0x4dc60000u;
|
|
for (const std::uint32_t pipeBankXor : {0u, 0x4600u, 0xff00u}) {
|
|
const auto address = block + pipeBankXor;
|
|
queue.context[0x318] = static_cast<std::uint32_t>(address >> 8u);
|
|
queue.context[0x390] = static_cast<std::uint32_t>(address >> 40u);
|
|
const auto color = AgcDriver::Graphics::DecodeState(queue).color;
|
|
Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Z64KBX && color.address == block && color.surfaceAddress == block && color.pipeBankXor == pipeBankXor, "a SW_64KB_Z_X color base did not split its block address and XOR");
|
|
}
|
|
queue.context[0x3b8] = 0x0dc68000u;
|
|
for (const std::uint32_t columnBits : {0x1000u, 0x2000u, 0x3000u}) {
|
|
queue.context[0x318] = static_cast<std::uint32_t>((block + columnBits) >> 8u);
|
|
queue.context[0x390] = static_cast<std::uint32_t>((block + columnBits) >> 40u);
|
|
expectFailure([&queue] { AgcDriver::Graphics::DecodeColorBuffer(queue.context, 0); }, "unsupported display color pipe/bank XOR column bits");
|
|
}
|
|
static std::vector<std::byte> mipStorage(16u * blockBytes);
|
|
const auto mipBase = (reinterpret_cast<std::uintptr_t>(mipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes;
|
|
const auto mips = AgcDriver::Graphics::ComputeElementMipLayout(AgcDriver::Graphics::TextureTileMode::kD64KBX, 4, 256, 128, 5);
|
|
const auto sliceBytes = AgcDriver::Graphics::ComputeSurfaceSize(mips, 1);
|
|
queue.context[0x318] = static_cast<std::uint32_t>((mipBase + 0x0200u) >> 8u);
|
|
queue.context[0x390] = static_cast<std::uint32_t>((mipBase + 0x0200u) >> 40u);
|
|
queue.context[0x3b0] = (4u << 28u) | (255u << 14u) | 127u;
|
|
queue.context[0x3b8] |= 1u;
|
|
queue.context[0x31b] = (2u << 26u) | 1u | (1u << 13u);
|
|
const auto mipColor = AgcDriver::Graphics::DecodeColorBuffer(queue.context, 0);
|
|
Require(mipColor.mip == 2 && mipColor.mipCount == 5 && mipColor.extent.width == 64 && mipColor.extent.height == 32 && mipColor.surfaceAddress == mipBase + sliceBytes && mipColor.address == mipBase + sliceBytes + mips[2].tiledOffset && mipColor.mipTail == mips[2].tail && mipColor.pipeBankXor == 0x0200u, "a display XOR color view lost its array slice, mip placement or XOR");
|
|
}
|
|
|
|
void HiddenUserWordVertexTablesTests() {
|
|
constexpr std::size_t userDataOffset = 288;
|
|
constexpr std::size_t directOffset = 352;
|
|
constexpr std::size_t semanticOffset = 384;
|
|
static std::vector<std::byte> header(semanticOffset + sizeof(ShaderSemantic));
|
|
static std::array<std::uint32_t, 4> bufferTable{0x1000u, 0x20000u, 64u, 0x3000u};
|
|
static std::array<std::uint32_t, 1> attribTable{(0x2cu << 5u)};
|
|
std::array<std::uint16_t, ShaderRegs::AGC_DIRECT_RESOURCE_TYPE_COUNT> direct{};
|
|
direct.fill(ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET);
|
|
direct[static_cast<std::size_t>(ShaderRegs::AgcDirectResourceType::PtrVertexBufferTable)] = 4;
|
|
direct[static_cast<std::size_t>(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<std::uint16_t*>(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<ShaderUserData*>(header.data() + userDataOffset);
|
|
shader.input_semantics = reinterpret_cast<ShaderSemantic*>(header.data() + semanticOffset);
|
|
shader.num_input_semantics = 1;
|
|
std::memcpy(header.data(), &shader, sizeof(shader));
|
|
const auto headerAddress = reinterpret_cast<std::uintptr_t>(header.data());
|
|
const auto low = [](const void* pointer) { return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer)); };
|
|
const auto high = [](const void* pointer) { return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer) >> 32u); };
|
|
const std::array<std::uint32_t, 8> registers{0x31016facu, 0x7efff390u, 0x100002u, 0x49u, low(bufferTable.data()), high(bufferTable.data()), low(attribTable.data()), high(attribTable.data())};
|
|
std::vector<std::uint32_t> 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<void>(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<std::uint32_t, 4> 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<std::byte, 8> truncated{};
|
|
expectFailure([&] { static_cast<void>(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<std::uint32_t, 1> 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<std::uint32_t, 5> 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");
|
|
Require(!pixel.frontFaceAllBits, "FRONT_FACE_ALL_BITS was decoded from a clear SPI_BARYC_CNTL");
|
|
queue.context[0x1b8] = 1u << 24u;
|
|
Require(decode(0x1146u, 0x1146u).frontFaceAllBits, "SPI_BARYC_CNTL FRONT_FACE_ALL_BITS was not decoded");
|
|
Require(!decode(0x326u, 0x326u).frontFaceAllBits, "FRONT_FACE_ALL_BITS was decoded for a pixel program without the front-face input");
|
|
queue.context[0x1b8] = 0u;
|
|
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<void>(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<std::uint32_t, 1> 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");
|
|
for (const auto inputs : {0x11u, 0x12u, 0x21u}) {
|
|
const auto sampleNoPerspective = pixelNoPerspectiveLocations(false, inputs);
|
|
Require(sampleNoPerspective.size() == 1 && sampleNoPerspective[0] == 1u, "a parameter interpolated through a sample I/J pair lost its interpolation mode: inputs " + std::to_string(inputs));
|
|
Require(pixelNoPerspectiveLocations(true, inputs).empty(), "explicit interpolation through a sample I/J pair interpolated parameter arrays a second time: inputs " + std::to_string(inputs));
|
|
}
|
|
for (const auto source : {0u, 1u}) {
|
|
const auto read = pixelBuiltinsRead(0x3u, 0x3u, source);
|
|
Require(readsBuiltin(read, spv::BuiltInBaryCoordKHR) && !readsBuiltin(read, spv::BuiltInFragCoord), "PERSP_SAMPLE does not hold the perspective barycentrics: v" + std::to_string(source));
|
|
}
|
|
for (const auto source : {2u, 3u}) {
|
|
const auto read = pixelBuiltinsRead(0x12u, 0x12u, source);
|
|
Require(readsBuiltin(read, spv::BuiltInBaryCoordNoPerspKHR) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "LINEAR_SAMPLE does not hold the linear barycentrics: v" + std::to_string(source));
|
|
}
|
|
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 BlendBypassTests() {
|
|
auto queue = makeState();
|
|
for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0;
|
|
queue.context[0x1e0] = (1u << 30u) | 4u | (5u << 8u);
|
|
Require(AgcDriver::Graphics::DecodeState(queue).blends.at(0).blendEnable, "enabled blending was not decoded");
|
|
queue.context[0x31c] |= 0x10000u;
|
|
const auto bypassed = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(!bypassed.blends.at(0).blendEnable && bypassed.blends.at(0).colorWriteMask != 0, "a target with BLEND_BYPASS still blends");
|
|
}
|
|
|
|
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<std::uint32_t>(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");
|
|
Require(depthOnly.depth->firstSlice == 0, "a depth view of slice 0 decoded another slice");
|
|
queue.context[0x002] = 3;
|
|
const auto thirdSlice = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(thirdSlice.depth && thirdSlice.depth->firstSlice == 3 && thirdSlice.depth->address == depthOnly.depth->address + 3u * AgcDriver::Graphics::DepthSliceBytes(depthOnly.depth->extent, 4u), "a depth view of slice 3 lost its slice or its Z offset");
|
|
queue.context[0x002] = 0;
|
|
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<bool> 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<AgcDriver::Graphics::CompiledShader> shaders) { return AgcDriver::Graphics::ShadersWriteMemory(std::vector<AgcDriver::Graphics::CompiledShader>(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;
|
|
for (const auto offset : {0x105u, 0x106u, 0x107u, 0x108u}) queue.context[offset] = 0;
|
|
const auto blended = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(blended.blends[0].blendEnable && blended.blends[0].srcColorBlendFactor == VK_BLEND_FACTOR_ONE && blended.blends[0].dstColorBlendFactor == VK_BLEND_FACTOR_ZERO && blended.blends[0].srcAlphaBlendFactor == VK_BLEND_FACTOR_ONE && blended.blends[0].dstAlphaBlendFactor == VK_BLEND_FACTOR_ZERO && blended.reversedBlend == 0u && !blended.dualSourceBlend, "a ONE + ZERO blend into a reversed 8_8_8_8 target did not stay a plain Vulkan blend");
|
|
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");
|
|
}
|
|
}
|
|
|
|
AgcDriver::QueueState reversedBlendQueue(std::uint32_t format, std::uint32_t number, std::uint32_t swap, std::uint32_t control) {
|
|
auto queue = makeState();
|
|
queue.context[0x31c] = (queue.context[0x31c] & ~0x1f7cu) | (format << 2u) | (number << 8u) | (swap << 11u);
|
|
const auto address = reinterpret_cast<std::uintptr_t>(sliceMemory.data());
|
|
queue.context[0x318] = static_cast<std::uint32_t>(address >> 8u);
|
|
queue.context[0x390] = static_cast<std::uint32_t>(address >> 40u);
|
|
queue.context[0x1b3] = 2;
|
|
queue.context[0x1b4] = 2;
|
|
queue.context[0x1e0] = control;
|
|
for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = 0;
|
|
return queue;
|
|
}
|
|
|
|
void ReversedComponentOrderBlendTests() {
|
|
using AgcDriver::Graphics::DecodeState;
|
|
struct Target {
|
|
std::uint32_t format;
|
|
std::uint32_t number;
|
|
VkFormat vkFormat;
|
|
};
|
|
struct Case {
|
|
std::uint32_t control;
|
|
VkBlendFactor source;
|
|
VkBlendFactor destination;
|
|
VkBlendFactor sourceAlpha;
|
|
VkBlendFactor destinationAlpha;
|
|
VkBlendOp operation;
|
|
std::uint32_t code;
|
|
bool opaqueDestination;
|
|
};
|
|
const std::array<Target, 3> targets{{
|
|
{10u, 0u, VK_FORMAT_R8G8B8A8_UNORM}, {12u, 7u, VK_FORMAT_R16G16B16A16_SFLOAT}, {12u, 0u, VK_FORMAT_R16G16B16A16_UNORM}
|
|
}};
|
|
const std::array<Case, 10> cases{{
|
|
{0x65010504u, VK_BLEND_FACTOR_SRC1_COLOR, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA, VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA, VK_BLEND_OP_ADD, 0x570001u, false},
|
|
{0x65810584u, VK_BLEND_FACTOR_SRC1_COLOR, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA, VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA, VK_BLEND_OP_REVERSE_SUBTRACT, 0x570001u, false},
|
|
{0x40000501u, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA, VK_BLEND_OP_ADD, 0x540001u, false},
|
|
{0x61000104u, VK_BLEND_FACTOR_SRC1_COLOR, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, 0x560001u, false},
|
|
{0x61020302u, VK_BLEND_FACTOR_SRC_COLOR, VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, 0x020001u, false},
|
|
{0x61010504u, VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR, VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA, VK_BLEND_OP_ADD, 0x560301u, false},
|
|
{0x60060008u, VK_BLEND_FACTOR_DST_COLOR, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_DST_ALPHA, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, 0u, false},
|
|
{0x40000101u, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, 0u, false},
|
|
{0x40000141u, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MIN, 0u, false},
|
|
{0x40000706u, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, 0u, true},
|
|
}};
|
|
const std::array<std::pair<std::uint32_t, const char*>, 10> refused{{
|
|
{0x40000706u, "with a factor of the destination alpha in the color equation"},
|
|
{0x4000000au, "with a factor of the destination alpha in the color equation"},
|
|
{0x61000008u, "with factors the host blend cannot form from copies of the export is not implemented"},
|
|
{0x61000501u, "with factors the host blend cannot form from copies of the export is not implemented"},
|
|
{0x65050302u, "with factors the host blend cannot form from copies of the export is not implemented"},
|
|
{0x4000000du, "with a constant blend factor"},
|
|
{0x40000013u, "with a constant blend factor"},
|
|
{0x4000000fu, "with a second-source factor is not implemented, as dual-source blending into a component-swapped color target is unsupported"},
|
|
{0x65240504u, "with different color and alpha operations"},
|
|
{0x40000544u, "with a MIN or MAX operation whose factors are not ONE"},
|
|
}};
|
|
for (const auto& [swap, mapping] : {std::pair{2u, std::uint8_t{0x1bu}}, std::pair{3u, std::uint8_t{0x93u}}}) {
|
|
for (const auto& target : targets) {
|
|
for (const auto& test : cases) {
|
|
auto queue = reversedBlendQueue(target.format, target.number, swap, test.control);
|
|
if (test.opaqueDestination) queue.context[0x31d] = 0x20000u;
|
|
const auto state = DecodeState(queue);
|
|
const auto code = test.code;
|
|
char what[128];
|
|
std::snprintf(what, sizeof(what), "CB_BLEND0_CONTROL 0x%08x into color format %u number type %u with component mapping 0x%02x", test.control, target.format, target.number, mapping);
|
|
Require(state.colors.size() == 1 && state.colors[0].format == target.vkFormat && state.colors[0].componentMapping == mapping, std::string(what) + ": the target decoded differently");
|
|
const auto& blend = state.blends.at(0);
|
|
Require(blend.blendEnable && blend.srcColorBlendFactor == test.source && blend.dstColorBlendFactor == test.destination && blend.srcAlphaBlendFactor == test.sourceAlpha && blend.dstAlphaBlendFactor == test.destinationAlpha && blend.colorBlendOp == test.operation && blend.alphaBlendOp == test.operation && blend.colorWriteMask == 0xfu, std::string(what) + ": wrong Vulkan blend state");
|
|
Require(state.reversedBlend == code && !state.dualSourceBlend, std::string(what) + ": wrong pixel shader blend code " + std::to_string(state.reversedBlend));
|
|
Require(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state)).targetExportMapping[0] == mapping, std::string(what) + ": the export mapping changed");
|
|
const auto direct = AgcDriver::Graphics::TranslateReversedBlend(test.control, mapping, target.vkFormat, test.opaqueDestination);
|
|
Require(direct.shaderCode == code && direct.blend.srcColorBlendFactor == blend.srcColorBlendFactor && direct.blend.dstColorBlendFactor == blend.dstColorBlendFactor && direct.blend.srcAlphaBlendFactor == blend.srcAlphaBlendFactor && direct.blend.dstAlphaBlendFactor == blend.dstAlphaBlendFactor && direct.blend.colorBlendOp == blend.colorBlendOp && direct.blend.alphaBlendOp == blend.alphaBlendOp, std::string(what) + ": TranslateReversedBlend disagrees with DecodeState");
|
|
}
|
|
}
|
|
for (const auto& [control, reason] : refused) {
|
|
const auto queue = reversedBlendQueue(10u, 0u, swap, control);
|
|
expectFailure([&] { DecodeState(queue); }, std::string("blending into a color target with a reversed component order ") + reason);
|
|
}
|
|
char registers[96];
|
|
std::snprintf(registers, sizeof(registers), "(CB_BLEND_CONTROL 0x65010504, component mapping 0x%02x, VkFormat %d)", mapping, static_cast<int>(VK_FORMAT_R8G8B8A8_SRGB));
|
|
expectFailure([&] { DecodeState(reversedBlendQueue(10u, 6u, swap, 0x65010504u)); }, std::string("reversed component order is not implemented for sRGB targets ") + registers);
|
|
expectFailure([&] { DecodeState(reversedBlendQueue(10u, 1u, swap, 0x65010504u)); }, "reversed component order is not implemented for signed normalized targets");
|
|
expectFailure([&] { DecodeState(reversedBlendQueue(12u, 1u, swap, 0x65010504u)); }, "reversed component order is not implemented for signed normalized targets");
|
|
expectFailure([&] { DecodeState(reversedBlendQueue(12u, 4u, swap, 0x65010504u)); }, "reversed component order is not implemented for integer targets");
|
|
auto masked = reversedBlendQueue(10u, 0u, swap, 0x65010504u);
|
|
masked.context[0x8e] = 7u;
|
|
Require(DecodeState(masked).blends.at(0).colorWriteMask == 0xeu, "a reversed target's blend did not keep the export-order write mask of its first three exports");
|
|
auto twoTargets = reversedBlendQueue(10u, 0u, swap, 0x65010504u);
|
|
for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x31du}) twoTargets.context[offset + 0xfu] = twoTargets.context.at(offset);
|
|
for (const auto offset : {0x390u, 0x3b0u, 0x3b8u}) twoTargets.context[offset + 1u] = twoTargets.context.at(offset);
|
|
twoTargets.context[0x31c + 0xfu] &= ~(3u << 11u);
|
|
twoTargets.context[0x1c5] = 0x99u;
|
|
twoTargets.context[0x8e] = 0xffu;
|
|
twoTargets.context[0x8f] = 0xffu;
|
|
twoTargets.context[0x1e1] = 0;
|
|
expectFailure([&] { DecodeState(twoTargets); }, "reversed component order through a second source color needs that target to be the shader's only color export");
|
|
twoTargets.context[0x1e0] = 0x40000101u;
|
|
const auto two = DecodeState(twoTargets);
|
|
Require(two.colors.size() == 2 && two.reversedBlend == 0u && two.blends.at(0).srcColorBlendFactor == VK_BLEND_FACTOR_ONE && two.blends.at(0).dstColorBlendFactor == VK_BLEND_FACTOR_ONE, "a reversed target blended by Vulkan alone was refused next to a second target");
|
|
twoTargets.context[0x31c + 0xfu] |= swap << 11u;
|
|
twoTargets.context[0x1e0] = 0x65010504u;
|
|
twoTargets.context[0x1e1] = 0x40000101u;
|
|
expectFailure([&] { DecodeState(twoTargets); }, "reversed component order through a second source color needs that target to be the shader's only color export");
|
|
}
|
|
}
|
|
|
|
void ReversedBlendRoutingTests() {
|
|
namespace Code = ShaderRecompiler::ReversedBlendCode;
|
|
using Term = std::pair<bool, std::uint32_t>;
|
|
constexpr Term zero{false, 0u};
|
|
constexpr Term one{true, 0u};
|
|
const auto flip = [](Term term) { return Term{!term.first, term.second}; };
|
|
const auto guest = [&](std::uint32_t factor, std::uint32_t component) -> std::optional<Term> {
|
|
const std::uint32_t exported = 10u + component;
|
|
const std::uint32_t stored = 20u + component;
|
|
switch (factor) {
|
|
case 0: return zero;
|
|
case 1: return one;
|
|
case 2: return Term{false, exported};
|
|
case 3: return Term{true, exported};
|
|
case 4: return Term{false, 10u};
|
|
case 5: return Term{true, 10u};
|
|
case 6: return Term{false, 20u};
|
|
case 7: return Term{true, 20u};
|
|
case 8: return Term{false, stored};
|
|
case 9: return Term{true, stored};
|
|
default: return component == 0u ? std::optional<Term>{one} : std::nullopt;
|
|
}
|
|
};
|
|
const auto product = [&](Term first, Term second) {
|
|
if (first == zero || second == zero) return std::vector<Term>{zero};
|
|
std::vector<Term> factors;
|
|
for (const auto term : {first, second}) {
|
|
if (term != one) factors.push_back(term);
|
|
}
|
|
std::sort(factors.begin(), factors.end());
|
|
return factors;
|
|
};
|
|
std::uint32_t routed = 0;
|
|
std::uint32_t native = 0;
|
|
for (std::uint32_t factors = 0; factors < 11u * 11u * 11u * 11u; ++factors) {
|
|
const std::array<std::uint32_t, 4> guestFactors{factors % 11u, factors / 11u % 11u, factors / 121u % 11u, factors / 1331u};
|
|
const auto control = guestFactors[0] | (guestFactors[1] << 8u) | (guestFactors[2] << 16u) | (guestFactors[3] << 24u) | 0x20000000u;
|
|
std::optional<AgcDriver::Graphics::ReversedBlend> translated;
|
|
try {
|
|
translated = AgcDriver::Graphics::TranslateReversedBlend(control, 0x1bu, VK_FORMAT_R8G8B8A8_UNORM, false);
|
|
} catch (const std::runtime_error& error) {
|
|
Require(std::string(error.what()).find("reversed component order with") != std::string::npos, std::string("an unexpected reversed-order blend refusal: ") + error.what());
|
|
continue;
|
|
}
|
|
const auto code = translated->shaderCode;
|
|
Require(code == 0u || (code & Code::Enable) != 0u, "a routed reversed-order blend code lacks its enable bit");
|
|
const auto route = [&](std::uint32_t shift, std::uint32_t component) {
|
|
switch ((code >> (shift + Code::RouteBits * component)) & Code::RouteMask) {
|
|
case Code::Own: return Term{false, 10u + component};
|
|
case Code::Component0: return Term{false, 10u};
|
|
case Code::Zero: return zero;
|
|
default: return one;
|
|
}
|
|
};
|
|
const auto host = [&](VkBlendFactor factor, std::uint32_t component) -> std::optional<Term> {
|
|
switch (factor) {
|
|
case VK_BLEND_FACTOR_ZERO: return zero;
|
|
case VK_BLEND_FACTOR_ONE: return one;
|
|
case VK_BLEND_FACTOR_SRC_COLOR: return route(Code::ColorShift, component);
|
|
case VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR: return flip(route(Code::ColorShift, component));
|
|
case VK_BLEND_FACTOR_SRC_ALPHA: return route(Code::ColorShift, 3u);
|
|
case VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA: return flip(route(Code::ColorShift, 3u));
|
|
case VK_BLEND_FACTOR_DST_COLOR: return Term{false, 20u + component};
|
|
case VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR: return Term{true, 20u + component};
|
|
case VK_BLEND_FACTOR_DST_ALPHA: return Term{false, 23u};
|
|
case VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA: return Term{true, 23u};
|
|
case VK_BLEND_FACTOR_SRC1_COLOR: return route(Code::SecondShift, component);
|
|
case VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR: return flip(route(Code::SecondShift, component));
|
|
case VK_BLEND_FACTOR_SRC1_ALPHA: return route(Code::SecondShift, 3u);
|
|
case VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA: return flip(route(Code::SecondShift, 3u));
|
|
default: return std::nullopt;
|
|
}
|
|
};
|
|
const auto& blend = translated->blend;
|
|
const auto secondColor = [](VkBlendFactor factor) { return factor == VK_BLEND_FACTOR_SRC1_COLOR || factor == VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR || factor == VK_BLEND_FACTOR_SRC1_ALPHA || factor == VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA; };
|
|
Require(code != 0u || !(secondColor(blend.srcColorBlendFactor) || secondColor(blend.dstColorBlendFactor) || secondColor(blend.srcAlphaBlendFactor) || secondColor(blend.dstAlphaBlendFactor)), "a reversed-order blend without a second color reads one");
|
|
for (std::uint32_t component = 0; component < 4; ++component) {
|
|
const auto wantedSource = guest(guestFactors[component == 0u ? 2u : 0u], component);
|
|
const auto wantedDestination = guest(guestFactors[component == 0u ? 3u : 1u], component);
|
|
const auto source = host(component == 3u ? blend.srcAlphaBlendFactor : blend.srcColorBlendFactor, component);
|
|
const auto destination = host(component == 3u ? blend.dstAlphaBlendFactor : blend.dstColorBlendFactor, component);
|
|
Require(wantedSource && wantedDestination && source && destination, "a reversed-order blend was accepted with a factor the routing check cannot express");
|
|
char what[160];
|
|
std::snprintf(what, sizeof(what), "CB_BLEND_CONTROL 0x%08x, code 0x%06x, component %u", control, code, component);
|
|
Require(product(route(Code::ColorShift, component), *source) == product(Term{false, 10u + component}, *wantedSource), std::string(what) + ": the routed source term differs from the guest's");
|
|
Require(*destination == *wantedDestination, std::string(what) + ": the routed destination factor differs from the guest's");
|
|
}
|
|
++(code == 0u ? native : routed);
|
|
}
|
|
Require(native == 121u && routed == 932u, "the reversed-order blend routing accepts " + std::to_string(native) + " native and " + std::to_string(routed) + " routed factor combinations, not 121 and 932");
|
|
}
|
|
|
|
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<std::byte, 1024> slotFourMemory{};
|
|
const auto slotFour = reinterpret_cast<std::uintptr_t>(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<std::uint32_t>(slotFour >> 8u);
|
|
queue.context[0x390 + 4u] = static_cast<std::uint32_t>(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<std::uintptr_t>(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;
|
|
{
|
|
AgcDriver::QueueState unwritten;
|
|
unwritten.shader = queue.shader;
|
|
unwritten.userConfig = queue.userConfig;
|
|
queue.context[0x200] = 0x00700791;
|
|
for (int reset = 0; reset < 2; ++reset) {
|
|
queue.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { unwritten.context[offset] = value; });
|
|
const auto state = AgcDriver::Graphics::DecodeState(unwritten);
|
|
Require(state.stencilTest && state.depth, "unwritten stencil registers rejected a stencil draw");
|
|
for (const auto face : {state.stencilFront, state.stencilBack}) {
|
|
Require(face.failOp == VK_STENCIL_OP_KEEP && face.passOp == VK_STENCIL_OP_KEEP && face.depthFailOp == VK_STENCIL_OP_KEEP && face.compareOp == VK_COMPARE_OP_ALWAYS, "unwritten stencil operations changed");
|
|
Require(face.compareMask == 0 && face.writeMask == 0 && face.reference == 0, "unwritten stencil reference or masks changed");
|
|
}
|
|
Require(AgcDriver::Graphics::DrawRejection(unwritten, false).empty(), "precheck rejected unwritten stencil registers");
|
|
unwritten.context[0x10b] = 0x00070070;
|
|
unwritten.context[0x10c] = unwritten.context[0x10d] = 0x01ffff09;
|
|
const auto explicitState = AgcDriver::Graphics::DecodeState(unwritten);
|
|
for (const auto face : {explicitState.stencilFront, explicitState.stencilBack}) Require(face.passOp == VK_STENCIL_OP_INVERT && face.compareMask == 0xff && face.writeMask == 0xff && face.reference == 9, "explicit stencil state was replaced by defaults");
|
|
unwritten.ClearContext();
|
|
for (const auto offset : {0x10bu, 0x10cu, 0x10du}) Require(unwritten.context.at(offset) == 0, "context clear did not reset stencil state");
|
|
}
|
|
}
|
|
queue.context[0x10b] = 0x00050050;
|
|
queue.context[0x10c] = 0x01ffff01;
|
|
queue.context[0x10d] = 0x01000001;
|
|
queue.context[0x200] = 0x00700711;
|
|
std::vector<AgcDriver::Graphics::RegisterRead> log;
|
|
auto state = [&] {
|
|
const AgcDriver::Graphics::RegisterReadLogScope logScope(&log);
|
|
return AgcDriver::Graphics::DecodeState(queue);
|
|
}();
|
|
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");
|
|
for (const auto clear : {0u, 1u, 2u, 3u}) {
|
|
auto retained = queue;
|
|
retained.context[0x8e] = retained.context[0x8f] = retained.context[0x203] = 0;
|
|
retained.context[0x000] = clear;
|
|
retained.context[0x200] = 0x00700777u;
|
|
auto unbound = retained;
|
|
unbound.shader[0x008] = unbound.shader[0x009] = 0;
|
|
const auto expected = AgcDriver::Graphics::DecodeState(unbound);
|
|
for (const auto format : {1u, 2u, 3u, 9u}) {
|
|
retained.context[0x1c4] = format;
|
|
Require(AgcDriver::Graphics::PixelProgramSkipped(retained), "a retained format ran the pixel program during a depth/stencil pass");
|
|
const auto actual = AgcDriver::Graphics::DecodeState(retained);
|
|
Require(actual.depth && expected.depth && actual.depth->address == expected.depth->address && actual.depth->stencilAddress == expected.depth->stencilAddress && actual.depth->format == expected.depth->format && actual.depth->clearDepth == expected.depth->clearDepth && actual.depth->clearStencil == expected.depth->clearStencil, "null substitution changed the depth/stencil surface");
|
|
Require(actual.depthTest == expected.depthTest && actual.depthWrite == expected.depthWrite && actual.depthCompare == expected.depthCompare && actual.depthBoundsTest == expected.depthBoundsTest && actual.stencilTest == expected.stencilTest, "null substitution changed fixed-function depth/stencil tests");
|
|
Require(std::memcmp(&actual.stencilFront, &expected.stencilFront, sizeof(VkStencilOpState)) == 0 && std::memcmp(&actual.stencilBack, &expected.stencilBack, sizeof(VkStencilOpState)) == 0, "null substitution changed stencil operations");
|
|
Require(actual.viewport.minDepth == expected.viewport.minDepth && actual.viewport.maxDepth == expected.viewport.maxDepth && actual.renderExtent.width == expected.renderExtent.width && actual.renderExtent.height == expected.renderExtent.height, "null substitution changed depth coverage");
|
|
Require(AgcDriver::Graphics::DrawRejection(retained, true).empty(), "a retained-format depth/stencil pass was rejected");
|
|
}
|
|
}
|
|
{
|
|
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();
|
|
{
|
|
const AgcDriver::Graphics::RegisterReadLogScope logScope(&log);
|
|
state = AgcDriver::Graphics::DecodeState(queue);
|
|
}
|
|
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");
|
|
{
|
|
const auto view = queue.context[0x002];
|
|
queue.context[0x002] = 2u | (2u << 13u);
|
|
const auto slice = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(slice.depth->htileAddress == (0x30000ull | (1ull << 40u)) + 2u * AgcDriver::Graphics::HtileMetadataBytes(slice.depth->extent), "slice 2 of a depth array must use the third slice of its HTILE");
|
|
queue.context[0x002] = view;
|
|
}
|
|
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<std::uint32_t>(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<std::uintptr_t>(sliceMemory.data());
|
|
queue.context[0x327] = static_cast<std::uint32_t>(secondColor >> 8u);
|
|
queue.context[0x391] = static_cast<std::uint32_t>(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 select : {0xc0u, 0xf00u}) {
|
|
queue.context[0x000] = 0x23u | select;
|
|
state = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS && clears(state.stencilFront), "a copy sample selection without a copy changed a clear draw");
|
|
}
|
|
for (const auto control : {4u, 8u, 0x84u, 0x108u}) {
|
|
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;
|
|
state = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(!state.depth && !state.depthTest && !state.depthWrite && !state.stencilTest && state.colors.size() == 2, "clear flags without depth/stencil planes changed color-only rendering");
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected clear flags without depth/stencil planes");
|
|
queue = makeState();
|
|
queue.context[0x010] = 0x80000180;
|
|
queue.context[0x011] = 0x20000180;
|
|
queue.context[0x200] = 0x776;
|
|
queue.context[0x1b3] = 2;
|
|
queue.context[0x1b4] = 2;
|
|
const auto colorOnly = AgcDriver::Graphics::DecodeState(queue);
|
|
for (const auto control : {1u, 2u, 3u}) {
|
|
queue.context[0x000] = control;
|
|
state = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(!state.depth && !state.depthTest && !state.depthWrite && !state.stencilTest, "stale clear flags enabled absent depth/stencil planes");
|
|
Require(state.color.address == colorOnly.color.address && state.blends[0].colorWriteMask == colorOnly.blends[0].colorWriteMask && state.viewport.minDepth == colorOnly.viewport.minDepth && state.viewport.maxDepth == colorOnly.viewport.maxDepth, "stale clear flags changed color writes or viewport depth");
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected a color-only draw with stale clear flags: " + AgcDriver::Graphics::DrawRejection(queue, false));
|
|
}
|
|
queue = makeState();
|
|
queue.context[0x31b] = 1u << 26u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "mip exceeds");
|
|
queue.context[0x31b] = 0;
|
|
const auto plain = AgcDriver::Graphics::DecodeState(queue);
|
|
queue.context[0x31b] = 35u << 13u;
|
|
const auto plainRange = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(plainRange.color.address == plain.color.address && plainRange.color.bytes == plain.color.bytes && plainRange.color.arrayLastSlice == plain.color.arrayLastSlice, "a color view of slices 0 to 35 of a target with no array slices did not render into its only slice");
|
|
queue.context[0x31b] = 1u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "ends before its first slice");
|
|
queue.context[0x31b] = 1u << 13u;
|
|
const auto firstSlice = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(firstSlice.color.address == reinterpret_cast<std::uintptr_t>(colorMemory.data()), "a color view of several slices did not retain its first slice");
|
|
queue.context[0x207] = 1u << 18u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "layered draws into a color view of several array slices");
|
|
queue.context[0x207] = 0;
|
|
const auto sliced = reinterpret_cast<std::uintptr_t>(sliceMemory.data());
|
|
queue.context[0x318] = static_cast<std::uint32_t>(sliced >> 8u);
|
|
queue.context[0x390] = static_cast<std::uint32_t>(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] = 1u << 13u;
|
|
const auto range = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(range.color.address == sliced && range.color.bytes == 1024u, "a color view of slices 0 to 1 of a 2D array target did not render into its first slice");
|
|
queue.context[0x207] = 1u << 18u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "layered draws into a color view of several array slices");
|
|
queue.context[0x207] = 0;
|
|
queue.context[0x31b] = 2u | (2u << 13u);
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beyond the array surface");
|
|
queue.context[0x31b] = 1u << 14u;
|
|
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");
|
|
Require(volume.color.layers == 1 && volume.renderLayers == 1 && volume.layerExports == 0, "a color view of one 3D depth slice became layered");
|
|
queue.context[0x31b] = 4u | (4u << 13u);
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "outside the 3D surface");
|
|
queue.context[0x31b] = 2u | (1u << 13u);
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "ends before its first slice");
|
|
queue.context[0x31b] = 1u | (3u << 13u);
|
|
queue.context[0x207] = (1u << 18u) | (1u << 21u) | (1u << 24u);
|
|
const auto layered = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(layered.color.depthSlice == 1u && layered.color.layers == 3u && layered.renderLayers == 3u && layered.layerExports == (1u << 18u), "a color view of three 3D depth slices did not render three layers from the vertex layer export");
|
|
{
|
|
auto pointSize = queue;
|
|
pointSize.context[0x1b3] = pointSize.context[0x1b4] = 2;
|
|
Require(AgcDriver::Graphics::DrawRejection(pointSize, false).empty(), "layered volume test state failed the draw precheck");
|
|
pointSize.context[0x207] |= 1u << 16u;
|
|
const auto rejection = AgcDriver::Graphics::DrawRejection(pointSize, false);
|
|
Require(rejection.empty(), "inactive point size rejected a layered volume draw: " + rejection);
|
|
const auto decoded = AgcDriver::Graphics::DecodeState(pointSize);
|
|
Require(decoded.color.depthSlice == layered.color.depthSlice && decoded.color.layers == layered.color.layers && decoded.renderLayers == layered.renderLayers && decoded.layerExports == layered.layerExports, "inactive point size changed layered volume routing");
|
|
Require(decoded.paClVsOutCntl == pointSize.context[0x207], "inactive point size discarded the layered export state");
|
|
}
|
|
queue.context[0x31b] = 1u | (8u << 13u);
|
|
Require(AgcDriver::Graphics::DecodeState(queue).renderLayers == 3u, "a color view past the 3D surface was not clamped to its last slice");
|
|
queue.context[0x207] = 0;
|
|
queue.context[0x31b] = 0;
|
|
queue.context[0x31c] |= 0x10000000;
|
|
const auto volumeKeys = reinterpret_cast<std::uintptr_t>(volumeKeyMemory.data());
|
|
queue.context[0x325] = static_cast<std::uint32_t>(volumeKeys >> 8u);
|
|
queue.context[0x3a8] = static_cast<std::uint32_t>(volumeKeys >> 40u);
|
|
Require(AgcDriver::Graphics::DecodeState(queue).color.dccAddress == volumeKeys, "a DCC 3D color target lost its keys");
|
|
}
|
|
|
|
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<std::uintptr_t>(colorMemory.data()) && line.color.depth == 1u, "a 1D color target did not decode as one row");
|
|
}
|
|
|
|
void htileDepthClearTests() {
|
|
static constexpr std::array<std::uint32_t, 17> kernel{0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, 0xbf810000u};
|
|
const std::array<std::uint32_t, 3> wave{64, 1, 1};
|
|
const auto address = [&](std::span<const std::uint32_t> code, std::array<std::uint32_t, 6> userData, std::array<std::uint32_t, 3> 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 memoryValueFillTests() {
|
|
static constexpr std::array<std::uint32_t, 9> kernel{0xd7460000u, 0x04010c08u, 0xf4201a82u, 0xfa000000u, 0xbf8cc07fu, 0x7e02026au, 0xe0102000u, 0x80000100u, 0xbf810000u};
|
|
const std::array<std::uint32_t, 3> wave{64, 1, 1};
|
|
const std::array<std::uint32_t, 8> userData{0x50b00000u, 0x00040020u, 0x20000u, 0x00014204u, 0x34be64e0u, 0x00100020u, 1u, 0x0004dfacu};
|
|
const auto match = [&](std::span<const std::uint32_t> code, std::array<std::uint32_t, 8> words, std::array<std::uint32_t, 3> threads, std::array<std::uint32_t, 3> groups) {
|
|
return AgcDriver::Graphics::MatchMemoryValueFill(code, words, threads, groups);
|
|
};
|
|
const auto fill = match(kernel, userData, wave, {2048, 1, 1});
|
|
Require(fill.has_value() && fill->address == 0x2050b00000ull && fill->bytes == 0x80000u && fill->valueAddress == 0x2034be64e0ull, "Kena's HTILE fill from a value in memory was not recognized");
|
|
const auto partial = match(kernel, userData, wave, {1024, 1, 1});
|
|
Require(partial.has_value() && partial->bytes == 0x40000u, "a fill of half the groups did not cover half the buffer");
|
|
auto records = userData;
|
|
records[2] = 0x100u;
|
|
const auto clipped = match(kernel, records, wave, {2048, 1, 1});
|
|
Require(clipped.has_value() && clipped->bytes == 0x400u, "a fill past NUM_RECORDS was not clipped to it");
|
|
Require(!match(kernel, userData, {32, 1, 1}, {2048, 1, 1}), "the memory value fill matched with another group size");
|
|
Require(!match(kernel, userData, wave, {2048, 2, 1}), "a two-dimensional dispatch was taken for a linear fill");
|
|
auto strided = userData;
|
|
strided[1] = 0x00080020u;
|
|
Require(!match(kernel, strided, wave, {2048, 1, 1}), "a fill with an 8-byte stride was taken for a dword fill");
|
|
auto indexed = userData;
|
|
indexed[3] |= 1u << 23u;
|
|
Require(!match(kernel, indexed, wave, {2048, 1, 1}), "a buffer with ADD_TID_ENABLE was taken for a plain fill");
|
|
auto nowhere = userData;
|
|
nowhere[4] = 0;
|
|
nowhere[5] = 0;
|
|
Require(!match(kernel, nowhere, wave, {2048, 1, 1}), "a fill from a null value pointer was recognized");
|
|
auto empty = userData;
|
|
empty[6] = 0;
|
|
Require(!match(kernel, empty, wave, {2048, 1, 1}), "a fill whose value buffer has no records was taken for a fill from memory");
|
|
auto raw = userData;
|
|
raw[5] = 0x20u;
|
|
raw[6] = 4u;
|
|
Require(match(kernel, raw, wave, {2048, 1, 1}).has_value(), "a fill from a 4-byte raw value buffer was not recognized");
|
|
raw[6] = 3u;
|
|
Require(!match(kernel, raw, wave, {2048, 1, 1}), "a fill from a value buffer shorter than a dword was taken for a fill from memory");
|
|
for (const std::uint32_t format : {20u, 21u, 22u}) {
|
|
auto typed = userData;
|
|
typed[3] = (typed[3] & ~(0x7fu << 12u)) | (format << 12u);
|
|
Require(match(kernel, typed, wave, {2048, 1, 1}).has_value(), "a fill through a 32-bit buffer format was not recognized");
|
|
}
|
|
for (const std::uint32_t format : {0x27u, 0x0du, 0x4bu}) {
|
|
auto converted = userData;
|
|
converted[3] = (converted[3] & ~(0x7fu << 12u)) | (format << 12u);
|
|
Require(!match(kernel, converted, wave, {2048, 1, 1}), "a fill through a converting buffer format was taken for a raw dword fill");
|
|
}
|
|
auto other = kernel;
|
|
other[6] ^= 1u;
|
|
Require(!match(other, userData, wave, {2048, 1, 1}), "a different kernel was taken for the memory value fill");
|
|
Require(!match(std::span(kernel).first(8), userData, wave, {2048, 1, 1}), "a truncated kernel was taken for the memory value fill");
|
|
}
|
|
|
|
void depthMaintenanceTests() {
|
|
for (const auto mode : {0x4u, 0x8u, 0x84u, 0x108u, 0xf88u, 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, 0x80u, 0xc0u, 0x100u, 0xf00u, 0x2000u, 0x2063u, 0x2fe3u}) {
|
|
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] = 3;
|
|
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<std::uint8_t, 4> 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<std::uintptr_t>(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");
|
|
auto bound = queue;
|
|
bound.context[0x010] = 0x3;
|
|
bound.context[0x011] = 0x1;
|
|
bound.context[0x200] = 0x72;
|
|
Require(DecodeColorMetadataPass(bound).has_value(), "an always-pass depth test without writes refused the pass");
|
|
bound.context[0x200] = 0x76;
|
|
expectFailure([&] { DecodeColorMetadataPass(bound); }, "depth or stencil work");
|
|
bound.context[0x200] = 0x7a;
|
|
expectFailure([&] { DecodeColorMetadataPass(bound); }, "depth or stencil work");
|
|
queue.context[0x91] = 0x40020;
|
|
expectFailure([&] { DecodeColorMetadataPass(queue); }, "over part of a color target");
|
|
queue.context[0x91] = 0x40040;
|
|
queue.context[0x10f] = std::bit_cast<std::uint32_t>(16.0f);
|
|
expectFailure([&] { DecodeColorMetadataPass(queue); }, "over part of a color target");
|
|
queue.context[0x10f] = std::bit_cast<std::uint32_t>(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<std::uintptr_t>(dccKeys.data());
|
|
queue.context[0x325] = static_cast<std::uint32_t>(keysAddress >> 8u);
|
|
queue.context[0x3a8] = static_cast<std::uint32_t>(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<std::uint32_t, 4> 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<std::byte> tiledBlock(blockAlignment + tiledBytes + keyCount);
|
|
const auto tiledAddress = (reinterpret_cast<std::uintptr_t>(tiledBlock.data()) + blockAlignment - 1) / blockAlignment * blockAlignment;
|
|
auto* tiledTexels = reinterpret_cast<std::uint8_t*>(tiledAddress);
|
|
auto* tiledKeys = tiledTexels + tiledBytes;
|
|
for (const bool pipeAligned : {true, false}) {
|
|
auto tiled = queue;
|
|
tiled.context[0x3b8] |= (static_cast<std::uint32_t>(AgcDriver::Graphics::ColorTileMode::RenderTarget) << 14u) | (pipeAligned ? 1u << 30u : 0u);
|
|
tiled.context[0x3b0] = ((tiledSide - 1u) << 14u) | (tiledSide - 1u);
|
|
tiled.context[0x318] = static_cast<std::uint32_t>(tiledAddress >> 8u);
|
|
tiled.context[0x390] = static_cast<std::uint32_t>(tiledAddress >> 40u);
|
|
tiled.context[0x325] = static_cast<std::uint32_t>((tiledAddress + tiledBytes) >> 8u);
|
|
tiled.context[0x3a8] = static_cast<std::uint32_t>((tiledAddress + tiledBytes) >> 40u);
|
|
for (const auto offset : {0xdu, 0x82u, 0x91u, 0x95u}) tiled.context[offset] = (tiledSide << 16u) | tiledSide;
|
|
tiled.context[0x10f] = std::bit_cast<std::uint32_t>(64.0f);
|
|
tiled.context[0x110] = std::bit_cast<std::uint32_t>(64.0f);
|
|
tiled.context[0x111] = std::bit_cast<std::uint32_t>(-64.0f);
|
|
tiled.context[0x112] = std::bit_cast<std::uint32_t>(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::size_t>(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<std::uint32_t> original(tiledBytes / 4);
|
|
for (std::size_t index = 0; index < original.size(); ++index) original[index] = 0x01000000u + static_cast<std::uint32_t>(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::size_t>(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<std::uint8_t, 4096> cmaskMemory{};
|
|
|
|
void cmaskTests() {
|
|
using AgcDriver::Graphics::DecodeColorBuffer;
|
|
auto queue = makeState();
|
|
queue.context[0x31c] |= 0x2000;
|
|
const auto cmaskAddress = reinterpret_cast<std::uintptr_t>(cmaskMemory.data());
|
|
queue.context[0x31f] = static_cast<std::uint32_t>(cmaskAddress >> 8u);
|
|
queue.context[0x398] = static_cast<std::uint32_t>(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<AgcDriver::Graphics::RegisterRead> log;
|
|
{
|
|
const AgcDriver::Graphics::RegisterReadLogScope logScope(&log);
|
|
static_cast<void>(DecodeColorBuffer(queue.context, 0));
|
|
}
|
|
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");
|
|
constexpr std::size_t blockBytes = 65536u;
|
|
static std::vector<std::byte> mipStorage(16u * blockBytes);
|
|
const auto mipBase = (reinterpret_cast<std::uintptr_t>(mipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes;
|
|
auto higherMip = queue;
|
|
higherMip.context[0x318] = static_cast<std::uint32_t>(mipBase >> 8u);
|
|
higherMip.context[0x390] = static_cast<std::uint32_t>(mipBase >> 40u);
|
|
higherMip.context[0x3b0] = (4u << 28u) | (255u << 14u) | 127u;
|
|
higherMip.context[0x31b] = 1u << 26u;
|
|
const auto higherTarget = DecodeColorBuffer(higherMip.context, 0);
|
|
Require(!higherTarget.cmaskFastClear && higherTarget.cmaskAddress == 0, "a higher mip of a fast-clear target decoded a CMASK");
|
|
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<std::uint8_t>((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);
|
|
AgcDriver::Graphics::RunColorMetadataPass(context, *pass);
|
|
Require(tiles([](std::uint32_t tile) { return tile == 2; }) && cmaskIs(0xff), "a tile with a CMASK code other than 0 or 15 did not keep its texels");
|
|
|
|
queue.context[0x31c] |= 0x10000000;
|
|
const auto keysAddress = reinterpret_cast<std::uintptr_t>(dccKeys.data());
|
|
queue.context[0x325] = static_cast<std::uint32_t>(keysAddress >> 8u);
|
|
queue.context[0x3a8] = static_cast<std::uint32_t>(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::DecodeColorBuffer;
|
|
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<std::uintptr_t>(cmaskMemory.data());
|
|
const auto setCmaskAddress = [&](auto& target, std::uint32_t slot = 0u) {
|
|
const auto stride = slot * 0xfu;
|
|
target.context[0x31f + stride] = static_cast<std::uint32_t>(cmaskAddress >> 8u);
|
|
target.context[0x398 + slot] = static_cast<std::uint32_t>(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);
|
|
Require(!state.perSampleShading, "2x multisampling requested sample shading without PS_ITER_SAMPLE");
|
|
queue.context[0x293] = 0x10000u;
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "PS_ITER_SAMPLE on a 2x target was refused");
|
|
Require(DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE on a 2x target did not request sample shading");
|
|
queue.context[0x293] = 0;
|
|
queue.context[0x302] = 0x44cc;
|
|
state = DecodeState(queue);
|
|
Require(state.customSampleLocations && state.sampleLocationGrid, "a standard 2x pattern with its samples swapped in one pixel did not decode as a grid");
|
|
for (const auto [pixel, word] : {std::pair{0u, 0x0c04u}, std::pair{1u, 0x0c04u}, std::pair{2u, 0x040cu}, std::pair{3u, 0x040cu}}) queue.context[0x2fe + pixel * 4u] = word;
|
|
state = DecodeState(queue);
|
|
Require(state.samples == 2 && state.customSampleLocations && state.sampleLocationGrid, "the 2x checkerboard pattern (PPSA27836 and PPSA01556) did not decode as a grid");
|
|
const auto grid = AgcDriver::Graphics::SampleLocationGrid(state);
|
|
Require(grid.width == 2 && grid.height == 2, "a pattern varying between pixels did not use a 2x2 grid");
|
|
const auto checkerboard = AgcDriver::Graphics::SampleLocations(state);
|
|
const std::array<VkSampleLocationEXT, 8> expectedCheckerboard{{{0.75f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}}};
|
|
for (std::size_t index = 0; index < expectedCheckerboard.size(); ++index) Require(checkerboard[index].x == expectedCheckerboard[index].x && checkerboard[index].y == expectedCheckerboard[index].y, "grid sample locations are not in Vulkan's (x + y * width) * samples + sample order");
|
|
AgcDriver::Graphics::Context gridContext{};
|
|
gridContext.sampleLocationSampleCounts = VK_SAMPLE_COUNT_2_BIT;
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(gridContext, state); }, "2x2 sample location grid");
|
|
gridContext.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_4_BIT;
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(gridContext, state); }, "2x2 sample location grid");
|
|
gridContext.sampleLocationGridSampleCounts |= VK_SAMPLE_COUNT_2_BIT;
|
|
Require(AgcDriver::Graphics::ValidateSampleLocations(gridContext, state), "a supported 2x2 sample location grid was refused");
|
|
queue.context[0x31d] = 0;
|
|
state = DecodeState(queue);
|
|
Require(state.samples == 1 && state.colors.size() == 1 && state.colors[0].samples == 1 && state.customSampleLocations && state.sampleLocationGrid, "2x checkerboard rasterization into a one-sample target (PPSA27836) did not rasterize sample 0 alone");
|
|
const auto sampleZero = AgcDriver::Graphics::SampleLocations(state);
|
|
const std::array<VkSampleLocationEXT, 4> expectedSampleZero{{{0.75f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.25f, 0.5f}}};
|
|
for (std::size_t pixel = 0; pixel < expectedSampleZero.size(); ++pixel) Require(sampleZero[pixel].x == expectedSampleZero[pixel].x && sampleZero[pixel].y == expectedSampleZero[pixel].y, "a one-sample target did not take sample 0's location in each pixel of the grid");
|
|
gridContext.sampleLocationSampleCounts = VK_SAMPLE_COUNT_2_BIT;
|
|
gridContext.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_2_BIT;
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(gridContext, state); }, "VK_EXT_sample_locations");
|
|
gridContext.sampleLocationSampleCounts |= VK_SAMPLE_COUNT_1_BIT;
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(gridContext, state); }, "2x2 sample location grid");
|
|
gridContext.sampleLocationGridSampleCounts |= VK_SAMPLE_COUNT_1_BIT;
|
|
Require(AgcDriver::Graphics::ValidateSampleLocations(gridContext, state), "one-sample grid locations were refused on a device that supports them");
|
|
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44;
|
|
state = DecodeState(queue);
|
|
Require(state.samples == 1 && state.customSampleLocations && !state.sampleLocationGrid, "a uniform 2x pattern into a one-sample target did not use sample 0's location");
|
|
auto stenciled = queue;
|
|
for (const auto [pixel, word] : {std::pair{0u, 0x0c04u}, std::pair{1u, 0x0c04u}, std::pair{2u, 0x040cu}, std::pair{3u, 0x040cu}}) stenciled.context[0x2fe + pixel * 4u] = word;
|
|
for (const auto [offset, word] : {std::pair{0x010u, 0x80000180u}, std::pair{0x011u, 0x20000181u}, std::pair{0x012u, 0x00001000u}, std::pair{0x013u, 0x00002000u}, std::pair{0x015u, 0x00002000u}, std::pair{0x007u, 0x003f003fu}, std::pair{0x000u, 0u}, std::pair{0x002u, 0u}, std::pair{0x00au, 0u}, std::pair{0x00bu, 0x3f800000u}, std::pair{0x10bu, 0u}, std::pair{0x10cu, 0x01ffff00u}, std::pair{0x10du, 0x01ffff00u}, std::pair{0x200u, 0x007007b3u}}) stenciled.context[offset] = word;
|
|
state = DecodeState(stenciled);
|
|
Require(state.samples == 1 && state.depth && state.depth->samples == 1 && state.stencilTest && state.sampleLocationGrid && state.colors.size() == 1 && state.colors[0].samples == 1, "2x checkerboard rasterization into one-sample stencil and color targets (PPSA27836) did not rasterize sample 0 alone");
|
|
stenciled.context[0x31d] = 0x9000;
|
|
expectFailure([&] { DecodeState(stenciled); }, "a color target's sample count differs from the rasterizer's");
|
|
stenciled.context[0x31d] = 0;
|
|
stenciled.context[0x1b3] |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid);
|
|
stenciled.context[0x1b4] |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid);
|
|
expectFailure([&] { DecodeState(stenciled); }, "the depth target's sample count differs from the rasterizer's");
|
|
const auto withInputs = [](AgcDriver::QueueState base, std::initializer_list<ShaderRecompiler::PixelInput> inputs) {
|
|
for (const auto input : inputs) {
|
|
base.context[0x1b3] |= ShaderRecompiler::PixelInputBit(input);
|
|
base.context[0x1b4] |= ShaderRecompiler::PixelInputBit(input);
|
|
}
|
|
return base;
|
|
};
|
|
for (const auto input : {ShaderRecompiler::PixelInput::PerspectiveCentroid, ShaderRecompiler::PixelInput::LinearCentroid, ShaderRecompiler::PixelInput::SampleCoverage}) expectFailure([&] { DecodeState(withInputs(queue, {input})); }, "sample count differs from the rasterizer's");
|
|
for (const auto input : {ShaderRecompiler::PixelInput::PerspectiveSample, ShaderRecompiler::PixelInput::LinearSample, ShaderRecompiler::PixelInput::Ancillary}) {
|
|
state = DecodeState(withInputs(queue, {input}));
|
|
Require(state.samples == 1 && !state.perSampleShading, "a sample or sample-index input into a one-sample target did not rasterize sample 0 alone");
|
|
}
|
|
for (const auto position : {ShaderRecompiler::PixelInput::PositionX, ShaderRecompiler::PixelInput::PositionY}) expectFailure([&] { DecodeState(withInputs(queue, {ShaderRecompiler::PixelInput::Ancillary, position})); }, "sample count differs from the rasterizer's");
|
|
auto iterated = queue;
|
|
iterated.context[0x293] = 0x06030000u;
|
|
state = DecodeState(iterated);
|
|
Require(state.samples == 1 && !state.perSampleShading, "PS_ITER_SAMPLE into a one-sample target (PPSA27836) did not rasterize sample 0 alone");
|
|
state = DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::PerspectiveSample, ShaderRecompiler::PixelInput::PositionX, ShaderRecompiler::PixelInput::PositionY}));
|
|
Require(state.samples == 1 && !state.perSampleShading, "PS_ITER_SAMPLE with sample and position inputs (PPSA27836) did not rasterize sample 0 alone");
|
|
state = DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::PerspectiveCentroid, ShaderRecompiler::PixelInput::LinearCentroid}));
|
|
Require(state.samples == 1, "PS_ITER_SAMPLE with centroid inputs did not rasterize sample 0 alone");
|
|
expectFailure([&] { DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::SampleCoverage})); }, "sample count differs from the rasterizer's");
|
|
expectFailure([&] { DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::PositionX})); }, "sample count differs from the rasterizer's");
|
|
auto iteratedStencil = withInputs(stenciled, {});
|
|
iteratedStencil.context[0x1b3] &= ~ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid);
|
|
iteratedStencil.context[0x1b4] &= ~ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid);
|
|
state = DecodeState(withInputs(iteratedStencil, {ShaderRecompiler::PixelInput::PerspectiveSample, ShaderRecompiler::PixelInput::Ancillary}));
|
|
Require(state.samples == 1 && state.depth && state.depth->samples == 1, "sample and sample-index inputs into a one-sample depth target (PPSA07411) did not rasterize sample 0 alone");
|
|
iteratedStencil.context[0x293] = 0x06030000u;
|
|
state = DecodeState(iteratedStencil);
|
|
Require(state.samples == 1 && state.depth && state.depth->samples == 1 && !state.perSampleShading, "PS_ITER_SAMPLE into one-sample depth and color targets did not rasterize sample 0 alone");
|
|
auto singleScan = queue;
|
|
singleScan.context[0x292] = 2;
|
|
expectFailure([&] { DecodeState(singleScan); }, "a color target's sample count differs from the rasterizer's");
|
|
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 && !state.sampleLocationGrid, "uniform custom sample locations were not decoded as one pixel's pattern");
|
|
const auto locations = AgcDriver::Graphics::SampleLocations(state);
|
|
const std::array<VkSampleLocationEXT, 4> 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<VkSampleLocationEXT, 4> 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);
|
|
for (const auto log2 : {0u, 1u, 2u, 3u}) {
|
|
queue = log2 == 0 ? makeState() : multisampled();
|
|
queue.context[0x1b3] = 2u;
|
|
queue.context[0x1b4] = 2u;
|
|
queue.context[0x2f8] = (log2 << 20u) | log2 | 0x20000000u;
|
|
queue.context[0x31d] = (log2 << 12u) | (log2 << 15u);
|
|
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) {
|
|
queue.context[0x2fe + pixel * 4u] = log2 == 3 ? 0xbd153fd1u : log2 == 2 ? 0x622ae6aeu : 0xcc44u;
|
|
queue.context[0x2ff + pixel * 4u] = log2 == 3 ? 0x9773f95bu : 0u;
|
|
}
|
|
std::vector<AgcDriver::Graphics::RegisterRead> sampleReads;
|
|
std::optional<AgcDriver::Graphics::RegisterReadLogScope> sampleLog(std::in_place, &sampleReads);
|
|
const auto coveredCenter = DecodeState(queue);
|
|
const auto coveredRejection = AgcDriver::Graphics::DrawRejection(queue, false);
|
|
sampleLog.reset();
|
|
Require(coveredCenter.samples == (1u << log2) && !coveredCenter.customSampleLocations, "covered-centroid mode changed the sample count or locations without centroid inputs");
|
|
for (const auto read : sampleReads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a covered-centroid register");
|
|
Require(coveredRejection.empty(), "covered-centroid mode without centroid inputs was refused: " + coveredRejection);
|
|
for (const auto centroid : {4u, 64u, 68u}) {
|
|
queue.context[0x1b3] = 2u | centroid;
|
|
queue.context[0x1b4] = 2u | centroid;
|
|
expectFailure([&] { DecodeState(queue); }, "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs");
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs") != std::string::npos, "the precheck accepted active covered-centroid interpolation");
|
|
queue.context[0x1b4] = 2u;
|
|
Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "an unaddressed centroid input changed covered-centroid mode");
|
|
queue.context[0x1b3] = 2u;
|
|
queue.context[0x1b4] = 2u | centroid;
|
|
Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a disabled centroid input changed covered-centroid mode");
|
|
}
|
|
queue.context[0x1b3] = 2u;
|
|
queue.context[0x1b4] = 2u;
|
|
for (const auto offset : {0x1b3u, 0x1b4u}) {
|
|
queue.context.erase(offset);
|
|
expectFailure([&] { DecodeState(queue); }, "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs");
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs") != std::string::npos, "the precheck accepted unspecified centroid inputs");
|
|
queue.context[offset] = 2u;
|
|
}
|
|
const auto withProgram = queue;
|
|
queue.shader[0x008] = 0;
|
|
queue.context[0x8e] = 0;
|
|
queue.context.erase(0x1b3);
|
|
queue.context.erase(0x1b4);
|
|
Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "covered-centroid mode was refused with no pixel program");
|
|
queue = withProgram;
|
|
queue.context[0x2f8] |= 0x10u;
|
|
expectFailure([&] { DecodeState(queue); }, "coverage conversion or nonstandard multisampling");
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("coverage conversion or nonstandard multisampling") != std::string::npos, "covered-centroid handling admitted unrelated AA bits");
|
|
}
|
|
queue = multisampled();
|
|
queue.context[0x31d] = 0x1000;
|
|
expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples");
|
|
queue.context[0x31d] = 0;
|
|
queue.context[0x293] = 0x10000u;
|
|
Require(DecodeState(queue).samples == 1 && !DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE into a one-sample target did not rasterize sample 0 alone");
|
|
queue = multisampled();
|
|
queue.context[0x292] = 2;
|
|
state = DecodeState(queue);
|
|
Require(state.samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 2 && state.customSampleLocations, "a 2x target rasterized without MSAA_ENABLE did not rasterize the target's samples");
|
|
const auto aliased = AgcDriver::Graphics::SampleLocations(state);
|
|
for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(aliased[sample].x == 0.5f && aliased[sample].y == 0.5f, "a 2x target rasterized without MSAA_ENABLE did not place every sample at the pixel center");
|
|
queue.context[0x2dc] = 0x0000aa01u;
|
|
expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage");
|
|
queue = multisampled();
|
|
queue.context[0x292] = 2;
|
|
queue.context[0x302] = 0x44cc;
|
|
state = DecodeState(queue);
|
|
Require(state.customSampleLocations && !state.sampleLocationGrid, "a 2x target rasterized without MSAA_ENABLE kept the per-pixel sample location grid of its centred samples");
|
|
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] |= 0x10000000u;
|
|
queue.context[0x325] = 0;
|
|
expectFailure([&] { DecodeState(queue); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X");
|
|
expectFailure([&] { DecodeColorBuffer(queue.context, 0); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X");
|
|
queue = multisampled();
|
|
queue.context[0x31c] |= 0x2000;
|
|
setCmaskAddress(queue);
|
|
std::vector<AgcDriver::Graphics::RegisterRead> reads;
|
|
{
|
|
const AgcDriver::Graphics::RegisterReadLogScope logScope(&reads);
|
|
static_cast<void>(DecodeState(queue));
|
|
}
|
|
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");
|
|
const auto rasterizedResolve = queue;
|
|
queue.context[0x2f8] = 0;
|
|
pass = DecodeColorMetadataPass(queue);
|
|
Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::Resolve && pass->source.has_value() && pass->source->samples == 2 && pass->targets.size() == 1, "a CB resolve with a single-sample rasterizer did not take its sample count from the source target");
|
|
queue = rasterizedResolve;
|
|
queue.context[0x2f8] = 0x00200002;
|
|
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6aeu;
|
|
expectFailure([&] { DecodeColorMetadataPass(queue); }, "differs from the rasterizer's");
|
|
queue = rasterizedResolve;
|
|
std::vector<std::byte> resolvedBlock(2u * 65536u);
|
|
const auto resolvedAddress = (reinterpret_cast<std::uintptr_t>(resolvedBlock.data()) + 65535u) / 65536u * 65536u;
|
|
for (const auto mode : {AgcDriver::Graphics::ColorTileMode::Standard4KB, AgcDriver::Graphics::ColorTileMode::Standard64KB, AgcDriver::Graphics::ColorTileMode::RenderTarget}) {
|
|
auto tiled = queue;
|
|
tiled.context[0x327] = static_cast<std::uint32_t>(resolvedAddress >> 8u);
|
|
tiled.context[0x391] = static_cast<std::uint32_t>(resolvedAddress >> 40u);
|
|
tiled.context[0x3b9] = (tiled.context[0x3b9] & ~(0x1fu << 14u)) | (static_cast<std::uint32_t>(mode) << 14u);
|
|
const auto tiledPass = DecodeColorMetadataPass(tiled);
|
|
Require(tiledPass.has_value() && tiledPass->mode == ColorMetadataPass::Mode::Resolve && tiledPass->targets.size() == 1 && tiledPass->targets[0].tileMode == mode && tiledPass->targets[0].samples == 1, "a CB resolve into a tile mode " + std::to_string(static_cast<std::uint32_t>(mode)) + " destination did not decode");
|
|
}
|
|
queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u);
|
|
expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents");
|
|
queue = rasterizedResolve;
|
|
queue.context[0x31c] |= 0x10000000u;
|
|
queue.context[0x325] = 0;
|
|
expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X");
|
|
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");
|
|
queue.context[0x31c] |= 0x10000000u;
|
|
queue.context[0x325] = 0;
|
|
expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X");
|
|
queue = multisampled();
|
|
queue.context[0x2f8] = 0;
|
|
queue.context[0x0] = 0;
|
|
queue.context[0x202] = 0x00cc0050;
|
|
queue.context[0x31c] = 0x1002e028;
|
|
queue.context[0x31d] = 0x00012000;
|
|
queue.context[0x31e] = 0x00100038u;
|
|
queue.context[0x325] = 0x2000;
|
|
queue.context[0x323] = 0x3f800000;
|
|
queue.context[0x324] = 0x3f800000;
|
|
setCmaskAddress(queue);
|
|
auto fmaskPass = DecodeColorMetadataPass(queue);
|
|
Require(fmaskPass.has_value() && fmaskPass->mode == ColorMetadataPass::Mode::FmaskDecompress && fmaskPass->targets.size() == 1 && fmaskPass->targets[0].samples == 4 && fmaskPass->targets[0].cmaskAddress == cmaskAddress && fmaskPass->targets[0].dccAddress != 0, "an FMASK decompress of a 4x multisampled target with CMASK and DCC did not decode");
|
|
Require(fmaskPass->targets[0].clearWords[0] == 0x3f800000 && fmaskPass->targets[0].clearWords[1] == 0x3f800000, "fast clear words changed");
|
|
Require(fmaskPass->targets[0].dccControl == 0x00100038u, "FMASK decompress lost its DCC control");
|
|
queue.context[0x31e] |= 0x80u;
|
|
Require(DecodeColorMetadataPass(queue)->targets[0].dccControl == 0x001000b8u, "FMASK decompress hid an unsupported DCC control from the importer");
|
|
queue = makeState();
|
|
queue.context[0x0] = 0;
|
|
queue.context[0x202] = 0x00cc0050;
|
|
expectFailure([&] { DecodeColorMetadataPass(queue); }, "over a single-sample target");
|
|
queue = multisampled();
|
|
queue.context[0x2f8] = 0;
|
|
queue.context[0x0] = 0;
|
|
queue.context[0x202] = 0x00cc0050;
|
|
queue.context[0x31c] = 0x00008028;
|
|
queue.context[0x31d] = 0x00012000;
|
|
expectFailure([&] { DecodeColorMetadataPass(queue); }, "FMASK decompress of a target without a CMASK address");
|
|
|
|
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");
|
|
Require(!seeded.TakeClear(), "a subsequent use applied the clear again");
|
|
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");
|
|
|
|
using AgcDriver::Graphics::ColorTarget;
|
|
using AgcDriver::Graphics::ColorTileMode;
|
|
using AgcDriver::Graphics::MultisampledColorBytes;
|
|
using AgcDriver::Graphics::MultisampledColorFill;
|
|
using AgcDriver::Graphics::MultisampledFillClearColor;
|
|
ColorTarget halfFloat{};
|
|
halfFloat.address = 0x40000000;
|
|
halfFloat.extent = {960, 540};
|
|
halfFloat.format = VK_FORMAT_R16G16B16A16_SFLOAT;
|
|
halfFloat.tileMode = ColorTileMode::RenderTarget;
|
|
halfFloat.elementBytes = 8;
|
|
halfFloat.samples = 4;
|
|
Require(MultisampledColorBytes(halfFloat) == 0xff0000, "a 4x 960x540 8-byte target is not 64x32 pixels per 64 KiB block");
|
|
auto srgb = halfFloat;
|
|
srgb.extent = {1536, 1536};
|
|
srgb.format = VK_FORMAT_R8G8B8A8_SRGB;
|
|
srgb.elementBytes = 4;
|
|
Require(MultisampledColorBytes(srgb) == 0x2400000, "a 4x 1536x1536 4-byte target is not 64x64 pixels per 64 KiB block");
|
|
auto narrow = srgb;
|
|
narrow.extent = {130, 60};
|
|
narrow.samples = 2;
|
|
Require(MultisampledColorBytes(narrow) == 0x30000, "a 2x 4-byte target is not 64x128 pixels per 64 KiB block");
|
|
narrow.samples = 8;
|
|
Require(MultisampledColorBytes(narrow) == 0x50000, "an 8x 4-byte target is not 32x64 pixels per 64 KiB block");
|
|
srgb.extent = {100, 100};
|
|
Require(MultisampledColorBytes(srgb) == 0x40000, "a 4x 100x100 4-byte target does not span 2x2 64 KiB blocks");
|
|
srgb.tileMode = ColorTileMode::Standard4KB;
|
|
Require(MultisampledColorBytes(srgb) == 0x31000, "a 4x 4-byte target is not 16x16 pixels per 4 KiB block");
|
|
srgb.tileMode = ColorTileMode::Linear;
|
|
Require(MultisampledColorBytes(srgb) == 0, "a linear multisampled target has a footprint");
|
|
|
|
MultisampledColorFill fill(halfFloat);
|
|
Require(!fill.TakeFill(), "an unfilled target cleared");
|
|
fill.NoteFill(0x40000000, 0xff0000, 0);
|
|
const auto zero = fill.TakeFill();
|
|
Require(zero && *zero == 0 && !fill.TakeFill(), "a fill of the whole color memory did not clear once");
|
|
fill.NoteFill(0x40ff0000, 0x10000, 0);
|
|
fill.NoteFill(0x3fff0000, 0x10000, 0);
|
|
Require(!fill.TakeFill(), "a fill next to the color memory cleared it");
|
|
fill.NoteFill(0x40000000, 0xfe0000, 0);
|
|
expectFailure([&] { fill.TakeFill(); }, "partly covers");
|
|
fill.NoteFill(0x3fff0000, 0x1010000, 0x3c003c00u);
|
|
const auto one = fill.TakeFill();
|
|
Require(one && *one == 0x3c003c00u, "a fill over and around the color memory did not clear it");
|
|
MultisampledColorFill linear(srgb);
|
|
linear.NoteFill(0x40001000, 0x1000, 0);
|
|
Require(!linear.TakeFill(), "a fill past the start of a target with no modeled footprint was taken");
|
|
linear.NoteFill(0x40000000, 0x1000, 0);
|
|
expectFailure([&] { linear.TakeFill(); }, "footprint is not modeled");
|
|
|
|
VkClearColorValue clear{};
|
|
Require(MultisampledFillClearColor(halfFloat, 0x3c003c00u, clear) && clear.float32[0] == 1.0f && clear.float32[3] == 1.0f, "a half-float fill of 1.0 did not clear to 1.0");
|
|
Require(MultisampledFillClearColor(srgb, 0, clear) && clear.float32[0] == 0.0f && clear.float32[3] == 0.0f, "an sRGB fill of 0 did not clear to 0");
|
|
Require(MultisampledFillClearColor(srgb, 0x80ffffffu, clear) && clear.float32[2] == 1.0f && clear.float32[3] == 128.0f / 255.0f, "an sRGB fill of white with linear alpha 0x80 did not clear to it");
|
|
Require(!MultisampledFillClearColor(srgb, 0x00000080u, clear), "an sRGB fill whose color decodes inexactly cleared");
|
|
|
|
using AgcDriver::Graphics::TakeMultisampledClear;
|
|
halfFloat.clearWords = {0x3c003c00u, 0x3c003c00u};
|
|
MultisampledCmask clearedCmask(0x50000000, 0x1000);
|
|
MultisampledColorFill pending(halfFloat);
|
|
Require(!TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a target with no CMASK clear and no fill cleared");
|
|
pending.NoteFill(0x40000000, 0xff0000, 0);
|
|
Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 0.0f, "a fill of the whole color memory did not clear to its value");
|
|
pending.NoteFill(0x40000000, 0xfe0000, 0);
|
|
expectFailure([&] { TakeMultisampledClear(clearedCmask, pending, halfFloat, clear); }, "partly covers");
|
|
clearedCmask.NoteFill(0x50000000, 0x1000, 0);
|
|
Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 1.0f, "a pending CMASK clear did not take precedence over a partial fill of the color memory");
|
|
Require(!TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a partial fill of the color memory outlived the CMASK clear over it");
|
|
pending.NoteFill(0x40000000, 0xff0000, 0);
|
|
clearedCmask.NoteFill(0x50000000, 0x1000, 0);
|
|
Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 1.0f && !TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a pending CMASK clear and a whole fill did not clear once to the CMASK clear color");
|
|
}
|
|
|
|
void multisampleDccStateTests() {
|
|
static std::array<std::byte, 131072> color{};
|
|
const auto address = (reinterpret_cast<std::uintptr_t>(color.data()) + 65535u) & ~std::uintptr_t{65535u};
|
|
for (const auto log2 : {1u, 2u, 3u}) {
|
|
auto queue = makeState();
|
|
queue.context[0x318] = static_cast<std::uint32_t>(address >> 8u);
|
|
queue.context[0x390] = static_cast<std::uint32_t>(address >> 40u);
|
|
queue.context[0x31c] |= 0x10000000u;
|
|
queue.context[0x31d] = (log2 << 12u) | (log2 << 15u);
|
|
queue.context[0x31e] = 0x00100038u;
|
|
queue.context[0x325] = 0x1234u;
|
|
queue.context[0x3b8] = 0x4dc6c000u;
|
|
queue.context[0x2f8] = (log2 << 20u) | log2;
|
|
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) {
|
|
queue.context[0x2fe + pixel * 4u] = 0;
|
|
queue.context[0x2ff + pixel * 4u] = 0;
|
|
}
|
|
queue.context[0x292] = 3;
|
|
queue.context[0x1b3] = queue.context[0x1b4] = 2;
|
|
const auto state = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(state.colors.size() == 1 && state.colors.front().samples == (1u << log2) && state.colors.front().dccAddress == 0x123400u && state.colors.front().dccPipeAligned, "normal-render multisampled DCC state did not decode");
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "normal-render multisampled DCC state failed the draw precheck");
|
|
auto changed = queue;
|
|
changed.context[0x31e] |= 0x80u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "color transforms or lossy");
|
|
changed = queue;
|
|
changed.context[0x3b8] &= ~0x40000000u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer");
|
|
changed = queue;
|
|
changed.context[0x325] = 0;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer");
|
|
changed = queue;
|
|
changed.context[0x3b8] |= 1u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer");
|
|
changed = queue;
|
|
changed.context[0x3b0] |= 1u << 28u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "multisampled color mips");
|
|
}
|
|
}
|
|
|
|
void clipDistanceTests() {
|
|
auto queue = makeState();
|
|
queue.context[0x207] = 0x0040000f;
|
|
Require(AgcDriver::Graphics::DecodeState(queue).paClVsOutCntl == 0x0040000fu, "DecodeState dropped the PA_CL_VS_OUT_CNTL value the validator quotes");
|
|
queue.context[0x1b3] = 2;
|
|
queue.context[0x1b4] = 2;
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "four clip distances were rejected by the precheck");
|
|
queue.context[0x207] = 0x004001ff;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "more than eight clip and cull distances");
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "nine clip distances passed the precheck");
|
|
}
|
|
|
|
void framebufferLayerTests() {
|
|
AgcDriver::Graphics::Context context{};
|
|
context.limits.maxFramebufferWidth = 64;
|
|
context.limits.maxFramebufferHeight = 64;
|
|
context.limits.maxFramebufferLayers = 3;
|
|
AgcDriver::Graphics::Framebuffer accepted(context, VK_NULL_HANDLE, {}, {64, 64}, 3);
|
|
for (const VkRenderPass pass : std::array<VkRenderPass, 2>{VK_NULL_HANDLE, reinterpret_cast<VkRenderPass>(1)}) {
|
|
expectFailure([&] { AgcDriver::Graphics::Framebuffer rejected(context, pass, {}, {64, 64}, 0); }, "framebuffer layers exceed device limits");
|
|
expectFailure([&] { AgcDriver::Graphics::Framebuffer rejected(context, pass, {}, {64, 64}, 4); }, "framebuffer layers exceed device limits");
|
|
}
|
|
}
|
|
|
|
void quadPixelMaskTests() {
|
|
using AgcDriver::Graphics::DecodeState;
|
|
using AgcDriver::Graphics::DrawRejection;
|
|
using AgcDriver::Graphics::HostSampleMask;
|
|
using AgcDriver::Graphics::PartialSampleMask;
|
|
using AgcDriver::Graphics::QuadPixelMask;
|
|
using AgcDriver::Graphics::UniformSampleMask;
|
|
using Masks = std::array<std::uint16_t, 4>;
|
|
const auto masked = [](std::uint32_t top, std::uint32_t bottom, std::uint32_t log2 = 0u) {
|
|
auto queue = makeState();
|
|
queue.context[0x30e] = top;
|
|
queue.context[0x30f] = bottom;
|
|
queue.context[0x1b3] = 2;
|
|
queue.context[0x1b4] = 2;
|
|
if (log2 != 0u) {
|
|
queue.context[0x2f8] = (log2 << 20u) | log2;
|
|
queue.context[0x292] = 3;
|
|
queue.context[0x3b8] = 0x0dc00000u;
|
|
queue.context[0x31d] = (log2 << 12u) | (log2 << 15u);
|
|
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = log2 == 1u ? 0xcc44u : 0x622ae6aeu;
|
|
}
|
|
return queue;
|
|
};
|
|
const auto accepted = [](const AgcDriver::QueueState& queue, const std::string& what) {
|
|
std::vector<AgcDriver::Graphics::RegisterRead> reads;
|
|
AgcDriver::Graphics::RegisterReadLog() = &reads;
|
|
const auto state = DecodeState(queue);
|
|
const auto rejection = DrawRejection(queue, false);
|
|
AgcDriver::Graphics::RegisterReadLog() = nullptr;
|
|
Require(rejection.empty(), what + " was refused by the precheck: " + rejection);
|
|
for (const auto read : reads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a register decoded for " + what);
|
|
return state;
|
|
};
|
|
const auto refused = [](const AgcDriver::QueueState& queue, std::string_view reason) {
|
|
expectFailure([&] { DecodeState(queue); }, reason);
|
|
Require(DrawRejection(queue, false).find(reason) != std::string::npos, "the precheck accepted a draw DecodeState refuses for " + std::string(reason));
|
|
};
|
|
auto state = accepted(masked(0x0000ffffu, 0u), "the X0Y0 single-sample mask");
|
|
Require(state.quadSampleMask == Masks{1, 0, 0, 0} && PartialSampleMask(state) && !UniformSampleMask(state) && QuadPixelMask(state) == 0x1u, "PA_SC_AA_MASK 0x0000ffff/0 did not cover only the even-x, even-y pixel of each quad");
|
|
state = accepted(masked(0xffff0000u, 0u), "the X1Y0 single-sample mask");
|
|
Require(QuadPixelMask(state) == 0x2u, "PA_SC_AA_MASK 0xffff0000/0 did not cover only the odd-x, even-y pixel of each quad");
|
|
state = accepted(masked(0xfffe0001u, 0x0001fffeu), "a single-sample mask with bits above sample 0");
|
|
Require(state.quadSampleMask == Masks{1, 0, 0, 1} && QuadPixelMask(state) == 0x9u, "a single-sample mask did not keep only bit 0 of each pixel's field");
|
|
state = accepted(masked(0x00010001u, 0x00010001u), "a single-sample mask with only sample 0 set");
|
|
Require(!PartialSampleMask(state) && QuadPixelMask(state) == 0xfu && HostSampleMask(state) == 1u, "PA_SC_AA_MASK 0x00010001 at one sample is not a full mask");
|
|
state = accepted(masked(0u, 0u), "an empty single-sample mask");
|
|
Require(PartialSampleMask(state) && UniformSampleMask(state) && HostSampleMask(state) == 0u && QuadPixelMask(state) == 0xfu, "an empty mask equal for the whole quad did not become a host sample mask of 0");
|
|
Require(AgcDriver::Graphics::DecodePixelStageInfo(masked(0x0000ffffu, 0u).context, {}).quadPixelMask == 0xfu, "the pixel stage decode read the sample mask registers instead of the decoded state");
|
|
auto queue = masked(0x0000ffffu, 0u);
|
|
queue.context[0x203] = 0x10u;
|
|
Require(accepted(queue, "an early-Z masked draw").pixelEarlyZ, "DB_SHADER_CONTROL Z_ORDER EARLY_Z_THEN_LATE_Z did not decode as early Z");
|
|
queue.context[0x203] = 0x50u;
|
|
Require(!accepted(queue, "a killing masked draw").pixelEarlyZ, "a pixel program with kill decoded as early Z");
|
|
queue.context[0x313] = 0x6001u;
|
|
refused(queue, "sample masks with conservative rasterization");
|
|
queue = masked(0xffffffffu, 0xffffffffu);
|
|
queue.context[0x313] = 0x6001u;
|
|
Require(!PartialSampleMask(accepted(queue, "a full mask with conservative rasterization")), "a full mask with conservative rasterization decoded as partial");
|
|
state = accepted(masked(0x00050005u, 0x00050005u, 2u), "a 4x mask equal for the whole quad");
|
|
Require(state.samples == 4 && PartialSampleMask(state) && UniformSampleMask(state) && HostSampleMask(state) == 0x5u && QuadPixelMask(state) == 0xfu, "a 4x mask 0x5 equal for the whole quad did not become the host sample mask 0x5");
|
|
state = accepted(masked(0xfff5fff5u, 0xfff5fff5u, 2u), "a 4x mask with bits above sample 3");
|
|
Require(HostSampleMask(state) == 0x5u && state.quadSampleMask == Masks{5, 5, 5, 5}, "a 4x mask kept bits of samples the rasterizer does not have");
|
|
state = accepted(masked(0x0000000fu, 0u, 2u), "a 4x mask that covers whole pixels");
|
|
Require(!UniformSampleMask(state) && QuadPixelMask(state) == 0x1u && state.quadSampleMask == Masks{0xf, 0, 0, 0}, "a 4x mask covering all samples of one quad pixel did not become a quad pixel mask");
|
|
state = accepted(masked(0x00000003u, 0u, 1u), "a 2x mask that covers whole pixels");
|
|
Require(QuadPixelMask(state) == 0x1u, "a 2x mask covering both samples of one quad pixel did not become a quad pixel mask");
|
|
using AgcDriver::Graphics::QuadSampleMasks;
|
|
constexpr Masks NoSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu};
|
|
state = accepted(masked(0x00020001u, 0x00080004u, 2u), "a 4x mask with one sample per quad pixel");
|
|
Require(QuadPixelMask(state) == 0xfu && QuadSampleMasks(state) == Masks{1, 2, 4, 8}, "a 4x mask of sample q for quad pixel q did not become per-pixel sample masks");
|
|
state = accepted(masked(0x0000000fu, 0x00030000u, 2u), "a 4x mask mixing full, empty and partial pixels");
|
|
Require(QuadPixelMask(state) == 0x9u && QuadSampleMasks(state) == Masks{0xffff, 0, 0, 3}, "a 4x mask mixing full, empty and partial pixels did not keep full pixels as all samples");
|
|
Require(QuadSampleMasks(accepted(masked(0x0000000fu, 0u, 2u), "a whole-pixel 4x mask")) == NoSampleMask && QuadSampleMasks(accepted(masked(0x00050005u, 0x00050005u, 2u), "a uniform 4x mask")) == NoSampleMask && QuadSampleMasks(accepted(masked(0x0000ffffu, 0u), "a single-sample mask")) == NoSampleMask, "a mask without partial pixels that differ produced per-pixel sample masks");
|
|
for (const auto centroid : {4u, 64u}) {
|
|
queue = masked(0x00020001u, 0x00080004u, 2u);
|
|
queue.context[0x1b3] = 2u | centroid;
|
|
queue.context[0x1b4] = 2u | centroid;
|
|
Require(QuadSampleMasks(accepted(queue, "centroid interpolation under per-pixel sample masks")) == Masks{1, 2, 4, 8}, "centroid interpolation refused per-pixel sample masks");
|
|
}
|
|
for (const auto centroid : {4u, 64u}) {
|
|
queue = masked(0x00050005u, 0x00050005u, 2u);
|
|
queue.context[0x1b3] = 2u | centroid;
|
|
queue.context[0x1b4] = 2u | centroid;
|
|
refused(queue, "sample masks that remove some samples of a pixel with centroid interpolation");
|
|
queue.context[0x1b4] = 2u;
|
|
Require(HostSampleMask(accepted(queue, "an unaddressed centroid input")) == 0x5u, "an unaddressed centroid input refused a 4x mask");
|
|
queue = masked(0x0000000fu, 0u, 2u);
|
|
queue.context[0x1b3] = 2u | centroid;
|
|
queue.context[0x1b4] = 2u | centroid;
|
|
Require(QuadPixelMask(accepted(queue, "centroid interpolation under a whole-pixel mask")) == 0x1u, "centroid interpolation refused a mask that covers whole pixels");
|
|
}
|
|
queue = masked(0x0000ffffu, 0u);
|
|
queue.context[0x31d] = 0x12000;
|
|
state = accepted(queue, "a single-sample mask into a 4x target");
|
|
Require(state.samples == 4 && state.quadSampleMask == Masks{0xf, 0, 0, 0} && QuadPixelMask(state) == 0x1u, "single-sample rasterization into a 4x target did not apply each pixel's sample 0 bit to all of its samples");
|
|
queue.context[0x30e] = queue.context[0x30f] = 0xfffefffeu;
|
|
state = accepted(queue, "an empty single-sample mask into a 4x target");
|
|
Require(UniformSampleMask(state) && HostSampleMask(state) == 0u, "an empty single-sample mask into a 4x target did not remove every sample");
|
|
|
|
AgcDriver::Graphics::Context context{};
|
|
ShaderRecompiler::RecompileResult early;
|
|
early.spirv = std::vector<std::uint32_t>{spv::MagicNumber, 0x10300u, 0u, 2u, 0u, (3u << 16u) | spv::OpExecutionMode, 1u, spv::ExecutionModeEarlyFragmentTests, (5u << 16u) | spv::OpFunction, 0u, 1u, 0u, 0u};
|
|
ShaderRecompiler::RecompileResult late;
|
|
late.spirv = std::vector<std::uint32_t>{spv::MagicNumber, 0x10300u, 0u, 2u, 0u, (3u << 16u) | spv::OpExecutionMode, 1u, spv::ExecutionModeOriginUpperLeft};
|
|
Require(AgcDriver::Graphics::DeclaresEarlyFragmentTests(early) && !AgcDriver::Graphics::DeclaresEarlyFragmentTests(late), "early fragment tests were not read from the SPIR-V execution modes");
|
|
using Stage = ShaderRecompiler::ShaderStage;
|
|
AgcDriver::Graphics::State uniform{};
|
|
uniform.samples = 4;
|
|
uniform.quadSampleMask = {5, 5, 5, 5};
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 1> earlyShaders{{{Stage::Fragment, &early, 0}}};
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 1> lateShaders{{{Stage::Fragment, &late, 0}}};
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, uniform, earlyShaders); }, "earlyFragmentSampleMaskTestBeforeSampleCounting");
|
|
AgcDriver::Graphics::ValidateSampleMask(context, uniform, lateShaders);
|
|
context.earlyFragmentSampleMaskTestBeforeSampleCounting = true;
|
|
AgcDriver::Graphics::ValidateSampleMask(context, uniform, earlyShaders);
|
|
ShaderRecompiler::RecompileResult stores;
|
|
ShaderRecompiler::DescriptorBinding written{};
|
|
written.kind = ShaderRecompiler::DescriptorKind::StorageBuffer;
|
|
written.role = ShaderRecompiler::DescriptorRole::GuestBuffers;
|
|
written.count = 1;
|
|
written.bufferWritten = {true};
|
|
stores.bindings.push_back(written);
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> storing{{{Stage::Vertex, &late, 0}, {Stage::Fragment, &stores, 0}}};
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> vertexStoring{{{Stage::Vertex, &stores, 0}, {Stage::Fragment, &late, 0}}};
|
|
AgcDriver::Graphics::State varying{};
|
|
varying.quadSampleMask = {1, 0, 0, 0};
|
|
varying.pixelEarlyZ = true;
|
|
varying.depth = AgcDriver::Graphics::DepthTarget{};
|
|
varying.depthTest = true;
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad");
|
|
AgcDriver::Graphics::ValidateSampleMask(context, varying, vertexStoring);
|
|
for (const auto change : {0, 1, 2}) {
|
|
auto relaxed = varying;
|
|
if (change == 0) relaxed.pixelEarlyZ = false;
|
|
if (change == 1) relaxed.depthTest = false;
|
|
if (change == 2) relaxed.quadSampleMask = {1, 1, 1, 1};
|
|
AgcDriver::Graphics::ValidateSampleMask(context, relaxed, storing);
|
|
}
|
|
varying.depthTest = false;
|
|
varying.stencilTest = true;
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad");
|
|
varying.stencilTest = false;
|
|
varying.depthBoundsTest = true;
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad");
|
|
}
|
|
|
|
void DepthClipTests() {
|
|
auto queue = makeState();
|
|
const auto direct = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(!direct.negativeOneToOne && direct.viewport.minDepth == 0 && direct.viewport.maxDepth == 1, "zero-to-one depth transform changed");
|
|
queue.context[0x204] = 0;
|
|
queue.context[0x113] = std::bit_cast<std::uint32_t>(0.5f);
|
|
queue.context[0x114] = std::bit_cast<std::uint32_t>(0.5f);
|
|
const auto symmetric = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(symmetric.negativeOneToOne && symmetric.viewport.minDepth == 0 && symmetric.viewport.maxDepth == 1, "negative-one-to-one depth transform is incorrect");
|
|
queue.context[0x113] = std::bit_cast<std::uint32_t>(-0.5f);
|
|
const auto reversed = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(reversed.viewport.minDepth == 1 && reversed.viewport.maxDepth == 0, "reversed depth transform is incorrect");
|
|
queue.context[0x113] = std::bit_cast<std::uint32_t>(1.0f);
|
|
queue.context[0x114] = 0;
|
|
const auto unrestricted = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(unrestricted.viewport.minDepth == -1 && unrestricted.viewport.maxDepth == 1, "unrestricted viewport depth was normalized");
|
|
queue.context[0x113] = std::bit_cast<std::uint32_t>(-1.0f);
|
|
const auto unrestrictedReversed = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(unrestrictedReversed.viewport.minDepth == 1 && unrestrictedReversed.viewport.maxDepth == -1, "reversed unrestricted viewport depth changed");
|
|
queue.context[0x113] = 0x7f7fffff;
|
|
queue.context[0x114] = 0x7f7fffff;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported viewport transform");
|
|
queue.context[0x114] = std::bit_cast<std::uint32_t>(0.5f);
|
|
queue.context[0x113] = std::bit_cast<std::uint32_t>(0.5f);
|
|
queue.context[0xb4] = std::bit_cast<std::uint32_t>(2.0f);
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "inverted viewport depth clamp");
|
|
queue.context[0xb4] = 0;
|
|
auto collapsedQueue = makeState();
|
|
collapsedQueue.context[0x10f] = 0;
|
|
collapsedQueue.context[0x110] = 0;
|
|
collapsedQueue.context[0x111] = std::bit_cast<std::uint32_t>(-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<std::uint32_t>(-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<std::uint32_t>(-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 UnditheredAlphaToCoverageTests() {
|
|
auto queue = makeState();
|
|
queue.context[0x203] = 0x8010;
|
|
queue.context[0x2dc] = 0xaa01;
|
|
queue.context[0x1b3] = 2;
|
|
queue.context[0x1b4] = 2;
|
|
for (const auto samples : {1u, 8u}) {
|
|
if (samples == 8) {
|
|
queue.context[0x201] = 0x00133303;
|
|
queue.context[0x2f8] = 0x0030e003;
|
|
queue.context[0x292] = 3;
|
|
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;
|
|
}
|
|
}
|
|
for (const auto disabled : {false, true}) {
|
|
queue.context[0x203] = 0x8010u | (disabled ? 0x800u : 0u);
|
|
const auto state = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(state.alphaToCoverage && !state.ditheredAlphaToCoverage && state.samples == samples, "the existing undithered alpha-to-coverage state changed");
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck changed the existing undithered alpha-to-coverage state");
|
|
}
|
|
}
|
|
}
|
|
|
|
void AlphaToCoverageTests() {
|
|
UnditheredAlphaToCoverageTests();
|
|
auto dithered = makeState();
|
|
dithered.context[0x203] = 0x8010;
|
|
dithered.context[0x201] = 0x00133303;
|
|
dithered.context[0x2dc] = 0x18701;
|
|
dithered.context[0x2f8] = 0x0030e003;
|
|
dithered.context[0x292] = 3;
|
|
dithered.context[0x31d] = 0x1b000;
|
|
dithered.context[0x1b3] = 2;
|
|
dithered.context[0x1b4] = 2;
|
|
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) {
|
|
dithered.context[0x2fe + pixel * 4u] = 0xbd153fd1u;
|
|
dithered.context[0x2ff + pixel * 4u] = 0x9773f95bu;
|
|
}
|
|
const auto rejection = AgcDriver::Graphics::DrawRejection(dithered, false);
|
|
Require(rejection.empty(), "eight-sample default dither refused: " + rejection);
|
|
const auto ditheredState = AgcDriver::Graphics::DecodeState(dithered);
|
|
Require(ditheredState.alphaToCoverage && ditheredState.samples == 8, "default dither did not enable eight-sample coverage");
|
|
Require(ditheredState.ditheredAlphaToCoverage, "default dither did not reach the shader state");
|
|
for (const auto eqaa : {0x00133333u, 0x00132303u, 0x00133203u, 0x00333303u}) {
|
|
dithered.context[0x201] = eqaa;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "DB_EQAA");
|
|
Require(AgcDriver::Graphics::DrawRejection(dithered, false).find("DB_EQAA") != std::string::npos, "the precheck accepted unmatched dithered coverage samples");
|
|
}
|
|
dithered.context[0x201] = 0x00133303;
|
|
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) dithered.context[0x2fe + pixel * 4u] ^= 1u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "custom sample locations");
|
|
Require(AgcDriver::Graphics::DrawRejection(dithered, false).find("custom sample locations") != std::string::npos, "the precheck accepted unmeasured dithered sample locations");
|
|
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) dithered.context[0x2fe + pixel * 4u] ^= 1u;
|
|
for (const auto format : {3u, 5u, 6u}) {
|
|
dithered.context[0x1c5] = format;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "FP16_ABGR or 32_ABGR");
|
|
Require(AgcDriver::Graphics::DrawRejection(dithered, false).find("FP16_ABGR or 32_ABGR") != std::string::npos, "the precheck accepted an unmeasured dithered export format");
|
|
}
|
|
dithered.context[0x1c5] = 4;
|
|
Require(AgcDriver::Graphics::DecodeState(dithered).ditheredAlphaToCoverage, "FP16 default dither was refused");
|
|
dithered.context[0x203] |= 0x800u;
|
|
Require(!AgcDriver::Graphics::DecodeState(dithered).alphaToCoverage, "the shader disable override was ignored");
|
|
Require(AgcDriver::Graphics::DrawRejection(dithered, false).empty(), "the precheck ignored the shader disable override");
|
|
dithered.context[0x2dc] |= 2u;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "alpha-to-coverage");
|
|
auto queue = makeState();
|
|
queue.context[0x203] = 0;
|
|
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");
|
|
queue.context[0x203] |= 0x800u;
|
|
Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "the existing undithered single-sample state changed under the shader disable override");
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck changed the existing undithered single-sample state under the shader disable override");
|
|
queue.context[0x203] &= ~0x800u;
|
|
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 std::uint32_t exportFormat = queue.context.at(0x1c5);
|
|
for (const auto format : {3u, 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}) {
|
|
queue.context[0x1c5] = (exportFormat & ~0xfu) | format;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "floating-point alpha");
|
|
}
|
|
const auto targetMask = queue.context.at(0x8e);
|
|
queue.context[0x8e] = 0;
|
|
queue.context[0x1c5] = (exportFormat & ~0xfu) | 3u;
|
|
const auto depthOnly = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(depthOnly.alphaToCoverage && depthOnly.colors.empty(), "alpha-to-coverage without a written color target and a 32_AR MRT0 export was refused or kept a target");
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected alpha-to-coverage without a written color target");
|
|
queue.context[0x1c5] = exportFormat & ~0xfu;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "floating-point alpha");
|
|
queue.context[0x8e] = targetMask;
|
|
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<std::uintptr_t>(colorMemory.data()) && state.color.bytes == colorMemory.size(), "sparse guest setup lost its render target");
|
|
Require(queue.context.at(0x206) == 0x43f, "initial homogeneous viewport mode changed");
|
|
for (const auto control : {0x3fu, 0x43eu, 0x53fu, 0x63fu, 0x8000043fu}) {
|
|
queue.context[0x206] = control;
|
|
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_CL_VTE_CNTL=0x");
|
|
}
|
|
queue.context[0x206] = 0x43f;
|
|
queue.context[0x203] &= ~0x800u;
|
|
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<VkDescriptorBufferInfo> buffers;
|
|
std::vector<VkDescriptorImageInfo> images;
|
|
};
|
|
|
|
struct MockVulkan {
|
|
std::uint64_t next = 1;
|
|
std::int64_t live = 0;
|
|
std::map<VkBuffer, VkDeviceSize> bufferSizes;
|
|
std::map<VkBuffer, VkBufferUsageFlags> bufferUsage;
|
|
std::map<VkDeviceMemory, VkMemoryAllocateFlags> allocationFlags;
|
|
std::map<VkBuffer, VkDeviceMemory> bufferMemory;
|
|
std::map<VkBuffer, VkDeviceSize> bufferOffset;
|
|
std::map<VkDeviceMemory, std::vector<std::byte>> memories;
|
|
std::map<VkDeviceMemory, VkDeviceSize> allocationSizes;
|
|
VkDeviceSize allocatedBytes = 0;
|
|
std::optional<VkDeviceSize> memoryLimit;
|
|
std::uint64_t allocationAttempts = 0;
|
|
std::uint32_t bufferMemoryTypeBits = 1;
|
|
std::vector<std::uint32_t> allocationTypes;
|
|
std::vector<VkDescriptorSetLayoutBinding> layoutBindings;
|
|
VkDescriptorSetLayoutCreateFlags layoutFlags = 0;
|
|
std::vector<VkDescriptorBindingFlags> layoutBindingFlags;
|
|
std::map<VkDescriptorSetLayout, std::uint32_t> variableLayoutCounts;
|
|
std::vector<VkDescriptorPoolSize> 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<VkDescriptorPool, DescriptorPool> descriptorPools;
|
|
std::uint32_t descriptorPoolLimit = 1000000;
|
|
VkResult descriptorResetResult = VK_SUCCESS;
|
|
std::optional<std::uint32_t> variableDescriptorCount;
|
|
std::vector<MockDescriptorWrite> writes;
|
|
std::map<VkSampler, VkSamplerCreateFlags> samplerFlags;
|
|
std::uint32_t samplerCreateCount = 0;
|
|
std::vector<VkCopyDescriptorSet> copies;
|
|
bool allowCopies = false;
|
|
std::uint32_t boundSets = 0;
|
|
std::uint32_t boundFirst = 0;
|
|
VkPipelineBindPoint boundPoint = VK_PIPELINE_BIND_POINT_MAX_ENUM;
|
|
std::map<VkPipelineLayout, VkDeviceSize> pipelineLayoutPushConstantSize;
|
|
std::map<VkPipelineLayout, std::vector<VkDescriptorSetLayout>> pipelineLayoutSets;
|
|
std::map<VkPipelineLayout, VkPipelineLayoutCreateFlags> pipelineLayoutFlags;
|
|
std::map<VkPipelineLayout, std::vector<VkPushConstantRange>> pipelineLayoutPushRanges;
|
|
std::map<VkGraphicsPipelineLibraryFlagsEXT, VkPipelineLayout> libraryLayouts;
|
|
std::uint32_t pipelineCreateCount = 0;
|
|
std::vector<std::array<std::uint32_t, 3>> pipelineSpecializations;
|
|
VkPipeline boundPipeline = VK_NULL_HANDLE;
|
|
std::vector<std::byte> lastPushConstants;
|
|
struct { std::uint32_t x = 0, y = 0, z = 0; } lastDispatchGroups;
|
|
};
|
|
|
|
MockVulkan mock;
|
|
|
|
template<typename THandle>
|
|
THandle makeHandle() {
|
|
const auto value = mock.next++;
|
|
if constexpr (std::is_pointer_v<THandle>) return reinterpret_cast<THandle>(static_cast<std::uintptr_t>(value));
|
|
else return static_cast<THandle>(value);
|
|
}
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateInfo* info, const VkAllocationCallbacks*, VkBuffer* buffer) {
|
|
*buffer = makeHandle<VkBuffer>();
|
|
mock.bufferSizes[*buffer] = info->size;
|
|
mock.bufferUsage[*buffer] = info->usage;
|
|
++mock.live;
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer buffer, VkMemoryRequirements* requirements) {
|
|
*requirements = {mock.bufferSizes.at(buffer), 1, mock.bufferMemoryTypeBits};
|
|
}
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) {
|
|
++mock.allocationAttempts;
|
|
mock.allocationTypes.push_back(info->memoryTypeIndex);
|
|
if (mock.memoryLimit.has_value() && mock.allocatedBytes + info->allocationSize > *mock.memoryLimit) return VK_ERROR_OUT_OF_DEVICE_MEMORY;
|
|
*memory = makeHandle<VkDeviceMemory>();
|
|
mock.memories[*memory] = std::vector<std::byte>(info->allocationSize);
|
|
mock.allocationSizes[*memory] = info->allocationSize;
|
|
mock.allocatedBytes += info->allocationSize;
|
|
if (info->pNext != nullptr) {
|
|
const auto* flags = static_cast<const VkMemoryAllocateFlagsInfo*>(info->pNext);
|
|
mock.allocationFlags[*memory] = flags->flags;
|
|
Require(flags->sType == VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO && flags->flags == VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT, "invalid BDA allocation flags");
|
|
}
|
|
++mock.live;
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL mockBindBufferMemory(VkDevice, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) {
|
|
Require(offset + 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<std::byte> mockBufferMemory(VkBuffer buffer) {
|
|
auto& memory = mock.memories.at(mock.bufferMemory.at(buffer));
|
|
return std::span<std::byte>(memory).subspan(static_cast<std::size_t>(mock.bufferOffset.at(buffer)), static_cast<std::size_t>(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<VkDescriptorSetLayout>();
|
|
mock.layoutBindings.assign(info->pBindings, info->pBindings + info->bindingCount);
|
|
mock.layoutFlags = info->flags;
|
|
const auto* bindingFlags = static_cast<const VkDescriptorSetLayoutBindingFlagsCreateInfo*>(info->pNext);
|
|
if (bindingFlags != nullptr) mock.layoutBindingFlags.assign(bindingFlags->pBindingFlags, bindingFlags->pBindingFlags + bindingFlags->bindingCount);
|
|
else mock.layoutBindingFlags.clear();
|
|
for (std::size_t index = 0; index < mock.layoutBindingFlags.size(); ++index) {
|
|
if ((mock.layoutBindingFlags[index] & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) != 0) mock.variableLayoutCounts[*layout] = info->pBindings[index].descriptorCount;
|
|
}
|
|
++mock.live;
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockDestroyDescriptorSetLayout(VkDevice, VkDescriptorSetLayout layout, const VkAllocationCallbacks*) {
|
|
mock.variableLayoutCounts.erase(layout);
|
|
--mock.live;
|
|
}
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL mockCreateDescriptorPool(VkDevice, const VkDescriptorPoolCreateInfo* info, const VkAllocationCallbacks*, VkDescriptorPool* pool) {
|
|
*pool = makeHandle<VkDescriptorPool>();
|
|
mock.poolSizes.assign(info->pPoolSizes, info->pPoolSizes + info->poolSizeCount);
|
|
mock.poolMaxSets = info->maxSets;
|
|
mock.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;
|
|
mock.variableDescriptorCount.reset();
|
|
if (info->pNext != nullptr) {
|
|
const auto* variable = static_cast<const VkDescriptorSetVariableDescriptorCountAllocateInfo*>(info->pNext);
|
|
Require(variable->sType == VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_ALLOCATE_INFO && variable->descriptorSetCount == 1, "variable descriptor allocation is malformed");
|
|
mock.variableDescriptorCount = variable->pDescriptorCounts[0];
|
|
Require(*mock.variableDescriptorCount <= mock.variableLayoutCounts.at(info->pSetLayouts[0]), "variable descriptor count exceeds its layout binding");
|
|
}
|
|
sets[0] = makeHandle<VkDescriptorSet>();
|
|
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 VkResult VKAPI_CALL mockCreateSampler(VkDevice, const VkSamplerCreateInfo* info, const VkAllocationCallbacks*, VkSampler* sampler) {
|
|
*sampler = makeHandle<VkSampler>();
|
|
mock.samplerFlags[*sampler] = info->flags;
|
|
++mock.samplerCreateCount;
|
|
++mock.live;
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockDestroySampler(VkDevice, VkSampler sampler, const VkAllocationCallbacks*) {
|
|
mock.samplerFlags.erase(sampler);
|
|
--mock.live;
|
|
}
|
|
|
|
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, {}, {}};
|
|
if (writes[i].pBufferInfo != nullptr) write.buffers.assign(writes[i].pBufferInfo, writes[i].pBufferInfo + writes[i].descriptorCount);
|
|
if (writes[i].pImageInfo != nullptr) write.images.assign(writes[i].pImageInfo, writes[i].pImageInfo + 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<std::uintptr_t>(info->buffer) * 0x10000;
|
|
}
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL mockCreatePipelineLayout(VkDevice, const VkPipelineLayoutCreateInfo* info, const VkAllocationCallbacks*, VkPipelineLayout* layout) {
|
|
*layout = makeHandle<VkPipelineLayout>();
|
|
mock.pipelineLayoutPushConstantSize[*layout] = info->pushConstantRangeCount > 0 ? info->pPushConstantRanges[0].size : 0;
|
|
if (info->setLayoutCount != 0) mock.pipelineLayoutSets[*layout].assign(info->pSetLayouts, info->pSetLayouts + info->setLayoutCount);
|
|
mock.pipelineLayoutFlags[*layout] = info->flags;
|
|
if (info->pushConstantRangeCount != 0) mock.pipelineLayoutPushRanges[*layout].assign(info->pPushConstantRanges, info->pPushConstantRanges + info->pushConstantRangeCount);
|
|
++mock.live;
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL mockCreateGraphicsPipelines(VkDevice, VkPipelineCache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks*, VkPipeline* pipelines) {
|
|
Require(count == 1, "mock expects exactly one graphics pipeline per call");
|
|
if ((infos[0].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) == 0) {
|
|
const auto* linked = static_cast<const VkPipelineLibraryCreateInfoKHR*>(infos[0].pNext);
|
|
Require(linked != nullptr && linked->sType == VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR && linked->libraryCount != 0, "mock expects a graphics pipeline library or a pipeline linked from libraries");
|
|
*pipelines = makeHandle<VkPipeline>();
|
|
++mock.pipelineCreateCount;
|
|
++mock.live;
|
|
return VK_SUCCESS;
|
|
}
|
|
const auto* library = static_cast<const VkGraphicsPipelineLibraryCreateInfoEXT*>(infos[0].pNext);
|
|
Require(library != nullptr && library->sType == VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_LIBRARY_CREATE_INFO_EXT, "missing graphics pipeline library description");
|
|
mock.libraryLayouts[library->flags] = infos[0].layout;
|
|
*pipelines = makeHandle<VkPipeline>();
|
|
++mock.live;
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockDestroyPipelineLayout(VkDevice, VkPipelineLayout, const VkAllocationCallbacks*) {
|
|
--mock.live;
|
|
}
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL mockCreateShaderModule(VkDevice, const VkShaderModuleCreateInfo*, const VkAllocationCallbacks*, VkShaderModule* module) {
|
|
*module = makeHandle<VkShaderModule>();
|
|
++mock.live;
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockDestroyShaderModule(VkDevice, VkShaderModule module, const VkAllocationCallbacks*) {
|
|
if (module != VK_NULL_HANDLE) --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<std::uint32_t, 3> values{};
|
|
Require(infos[0].stage.pSpecializationInfo->dataSize >= sizeof(values), "compute pipeline specialization data has an unexpected size");
|
|
std::memcpy(values.data(), infos[0].stage.pSpecializationInfo->pData, sizeof(values));
|
|
*pipelines = makeHandle<VkPipeline>();
|
|
mock.pipelineSpecializations.push_back(values);
|
|
++mock.pipelineCreateCount;
|
|
++mock.live;
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockDestroyPipeline(VkDevice, VkPipeline, const VkAllocationCallbacks*) {
|
|
--mock.live;
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockCmdBindPipeline(VkCommandBuffer, VkPipelineBindPoint, VkPipeline pipeline) {
|
|
mock.boundPipeline = pipeline;
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockCmdPushConstants(VkCommandBuffer, VkPipelineLayout, VkShaderStageFlags, std::uint32_t, std::uint32_t size, const void* values) {
|
|
const auto* bytes = static_cast<const std::byte*>(values);
|
|
mock.lastPushConstants.assign(bytes, bytes + size);
|
|
}
|
|
|
|
VKAPI_ATTR void VKAPI_CALL mockCmdDispatch(VkCommandBuffer, std::uint32_t x, std::uint32_t y, std::uint32_t z) {
|
|
mock.lastDispatchGroups = {x, y, z};
|
|
}
|
|
|
|
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<std::size_t>(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<VkCommandBuffer>();
|
|
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<VkFence>(); ++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<std::string_view, PFN_vkVoidFunction> table{
|
|
{"vkAllocateCommandBuffers", reinterpret_cast<PFN_vkVoidFunction>(mockAllocateCommandBuffers)},
|
|
{"vkFreeCommandBuffers", reinterpret_cast<PFN_vkVoidFunction>(mockFreeCommandBuffers)},
|
|
{"vkCreateFence", reinterpret_cast<PFN_vkVoidFunction>(mockCreateFence)},
|
|
{"vkDestroyFence", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyFence)},
|
|
{"vkGetFenceStatus", reinterpret_cast<PFN_vkVoidFunction>(mockGetFenceStatus)},
|
|
{"vkResetFences", reinterpret_cast<PFN_vkVoidFunction>(mockResetFences)},
|
|
{"vkWaitForFences", reinterpret_cast<PFN_vkVoidFunction>(mockWaitForFences)},
|
|
{"vkBeginCommandBuffer", reinterpret_cast<PFN_vkVoidFunction>(mockBeginCommandBuffer)},
|
|
{"vkEndCommandBuffer", reinterpret_cast<PFN_vkVoidFunction>(mockEndCommandBuffer)},
|
|
{"vkQueueSubmit", reinterpret_cast<PFN_vkVoidFunction>(mockQueueSubmit)},
|
|
{"vkCmdPipelineBarrier", reinterpret_cast<PFN_vkVoidFunction>(mockCmdPipelineBarrier)},
|
|
{"vkCmdBeginQuery", reinterpret_cast<PFN_vkVoidFunction>(mockCmdBeginQuery)},
|
|
{"vkCmdEndQuery", reinterpret_cast<PFN_vkVoidFunction>(mockCmdEndQuery)},
|
|
{"vkCmdResetQueryPool", reinterpret_cast<PFN_vkVoidFunction>(mockCmdResetQueryPool)},
|
|
{"vkCmdCopyQueryPoolResults", reinterpret_cast<PFN_vkVoidFunction>(mockCmdCopyQueryPoolResults)},
|
|
|
|
{"vkGetBufferDeviceAddressKHR", reinterpret_cast<PFN_vkVoidFunction>(mockGetBufferDeviceAddress)},
|
|
{"vkGetMemoryHostPointerPropertiesEXT", reinterpret_cast<PFN_vkVoidFunction>(mockGetMemoryHostPointerProperties)},
|
|
{"vkCreateBuffer", reinterpret_cast<PFN_vkVoidFunction>(mockCreateBuffer)},
|
|
{"vkGetBufferMemoryRequirements", reinterpret_cast<PFN_vkVoidFunction>(mockGetBufferMemoryRequirements)},
|
|
{"vkAllocateMemory", reinterpret_cast<PFN_vkVoidFunction>(mockAllocateMemory)},
|
|
{"vkBindBufferMemory", reinterpret_cast<PFN_vkVoidFunction>(mockBindBufferMemory)},
|
|
{"vkMapMemory", reinterpret_cast<PFN_vkVoidFunction>(mockMapMemory)},
|
|
{"vkUnmapMemory", reinterpret_cast<PFN_vkVoidFunction>(mockUnmapMemory)},
|
|
{"vkDestroyBuffer", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyBuffer)},
|
|
{"vkFreeMemory", reinterpret_cast<PFN_vkVoidFunction>(mockFreeMemory)},
|
|
{"vkCreateDescriptorSetLayout", reinterpret_cast<PFN_vkVoidFunction>(mockCreateDescriptorSetLayout)},
|
|
{"vkDestroyDescriptorSetLayout", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyDescriptorSetLayout)},
|
|
{"vkCreateDescriptorPool", reinterpret_cast<PFN_vkVoidFunction>(mockCreateDescriptorPool)},
|
|
{"vkDestroyDescriptorPool", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyDescriptorPool)},
|
|
{"vkAllocateDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockAllocateDescriptorSets)},
|
|
{"vkResetDescriptorPool", reinterpret_cast<PFN_vkVoidFunction>(mockResetDescriptorPool)},
|
|
{"vkCreateSampler", reinterpret_cast<PFN_vkVoidFunction>(mockCreateSampler)},
|
|
{"vkDestroySampler", reinterpret_cast<PFN_vkVoidFunction>(mockDestroySampler)},
|
|
{"vkUpdateDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockUpdateDescriptorSets)},
|
|
{"vkCmdBindDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockCmdBindDescriptorSets)},
|
|
{"vkCreateGraphicsPipelines", reinterpret_cast<PFN_vkVoidFunction>(mockCreateGraphicsPipelines)},
|
|
{"vkCreatePipelineLayout", reinterpret_cast<PFN_vkVoidFunction>(mockCreatePipelineLayout)},
|
|
{"vkDestroyPipelineLayout", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyPipelineLayout)},
|
|
{"vkCreateShaderModule", reinterpret_cast<PFN_vkVoidFunction>(mockCreateShaderModule)},
|
|
{"vkDestroyShaderModule", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyShaderModule)},
|
|
{"vkCreateComputePipelines", reinterpret_cast<PFN_vkVoidFunction>(mockCreateComputePipelines)},
|
|
{"vkDestroyPipeline", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyPipeline)},
|
|
{"vkCmdBindPipeline", reinterpret_cast<PFN_vkVoidFunction>(mockCmdBindPipeline)},
|
|
{"vkCmdPushConstants", reinterpret_cast<PFN_vkVoidFunction>(mockCmdPushConstants)},
|
|
{"vkCmdDispatch", reinterpret_cast<PFN_vkVoidFunction>(mockCmdDispatch)},
|
|
{"vkCmdUpdateBuffer", reinterpret_cast<PFN_vkVoidFunction>(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;
|
|
}
|
|
|
|
void addressSpaceTeardownTests() {
|
|
#ifdef _WIN32
|
|
_putenv_s("APS5_NO_LEASE_MIRROR", "1");
|
|
#else
|
|
setenv("APS5_NO_LEASE_MIRROR", "1", 1);
|
|
#endif
|
|
auto context = mockContext();
|
|
context.device = makeHandle<VkDevice>();
|
|
context.bufferDeviceAddress = true;
|
|
context.limits.maxStorageBufferRange = 1u << 27;
|
|
void* block = GuestHeap::GuestHeapAlignForGpu_nid_postfix(16, 64);
|
|
const auto before = AgcDriver::Graphics::LeaseCounters().cacheDrops;
|
|
{
|
|
AgcDriver::Graphics::GuestBufferMemory memory(context);
|
|
memory.AcquireRegistered();
|
|
Require(memory.HoldsLease(), "address-based build holds no lease before device teardown");
|
|
memory.Upload(true);
|
|
memory.WriteBack();
|
|
Require(!memory.HoldsLease(), "completed address-based build kept its own lease");
|
|
}
|
|
AgcDriver::Graphics::ClearImageMirrors(context.device);
|
|
GuestHeap::GuestHeapFree_nid_postfix(block);
|
|
Require(AgcDriver::Graphics::LeaseCounters().cacheDrops == before, "device teardown left the cached address space pinning guest allocations");
|
|
Require(mock.live == 0, "address-space teardown test leaked Vulkan objects");
|
|
}
|
|
|
|
using Role = ShaderRecompiler::DescriptorRole;
|
|
|
|
void bufferBudgetPlacementTests() {
|
|
using namespace AgcDriver::Graphics;
|
|
for (const std::size_t bytes : {std::size_t{512}, std::size_t{3} << 20u}) {
|
|
for (const bool fail : {false, true}) {
|
|
mock = MockVulkan{};
|
|
mock.bufferMemoryTypeBits = 3;
|
|
if (fail) mock.memoryLimit = 0;
|
|
{
|
|
auto context = mockContext();
|
|
constexpr auto coherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
|
context.memory.memoryTypeCount = 2;
|
|
context.memory.memoryHeapCount = 2;
|
|
context.memory.memoryHeaps[0] = {4u << 20u, 0};
|
|
context.memory.memoryHeaps[1] = {8u << 20u, 0};
|
|
context.memory.memoryTypes[0] = {coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, 0};
|
|
context.memory.memoryTypes[1] = {coherent, 1};
|
|
context.physical = reinterpret_cast<VkPhysicalDevice>(std::uintptr_t{1});
|
|
context.memoryProperties2 = [](VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) {
|
|
auto* reported = static_cast<VkPhysicalDeviceMemoryBudgetPropertiesEXT*>(properties->pNext);
|
|
reported->heapBudget[0] = 1u << 20u;
|
|
reported->heapUsage[0] = reported->heapBudget[0] - 1024u;
|
|
reported->heapBudget[1] = 8u << 20u;
|
|
};
|
|
const auto allocate = [&] {
|
|
Buffer buffer(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
|
Require(buffer.Mapped() && buffer.Bytes().size() == bytes, "a budget-selected buffer lost its mapping or size");
|
|
buffer.Bytes().front() = std::byte{0x5a};
|
|
Require(mockBufferMemory(buffer.Handle()).front() == std::byte{0x5a}, "a budget-selected buffer bound the wrong allocation");
|
|
};
|
|
if (fail) expectFailure(allocate, "Vulkan result -2");
|
|
else allocate();
|
|
Require(mock.allocationTypes == std::vector<std::uint32_t>{1} && mock.allocationAttempts == 1, "a coherent buffer did not select the other heap before its single allocation attempt");
|
|
if (!fail) Require(mock.allocatedBytes == (bytes == 512 ? BufferPool::SlabBlockBytes(512) : bytes), "a coherent buffer budget check lost its full allocation extent");
|
|
}
|
|
Require(mock.live == 0, "a budget-selected buffer leaked Vulkan objects");
|
|
}
|
|
}
|
|
}
|
|
using Kind = ShaderRecompiler::DescriptorKind;
|
|
|
|
alignas(16) std::array<std::uint32_t, 4> guestFirst{0x11111111, 0x22222222, 0x33333333, 0x44444444};
|
|
alignas(16) std::array<std::uint32_t, 8> guestSecond{1, 2, 3, 4, 5, 6, 7, 8};
|
|
alignas(16) std::array<std::uint32_t, 2> guestThird{0xaaaaaaaa, 0xbbbbbbbb};
|
|
|
|
std::vector<std::uint32_t> vsharp(const void* pointer, std::uint32_t bytes) {
|
|
const auto address = reinterpret_cast<std::uintptr_t>(pointer);
|
|
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u) & 0xffffu, bytes, 0x31000000u};
|
|
}
|
|
|
|
std::vector<std::uint32_t> join(std::vector<std::uint32_t> first, const std::vector<std::uint32_t>& second) {
|
|
first.insert(first.end(), second.begin(), second.end());
|
|
return first;
|
|
}
|
|
|
|
ShaderRecompiler::DescriptorBinding makeBinding(Role role, std::uint32_t binding, std::uint32_t count, std::vector<std::uint32_t> words) {
|
|
ShaderRecompiler::DescriptorBinding result;
|
|
result.kind = Kind::StorageBuffer;
|
|
result.role = role;
|
|
result.descriptorSet = 0;
|
|
result.binding = binding;
|
|
result.count = count;
|
|
result.guestDescriptor = std::move(words);
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::uint32_t> ShaderDataWords(std::initializer_list<std::uint32_t> userData) {
|
|
std::vector<std::uint32_t> 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<const std::byte> memory, const void* expected, std::size_t bytes) {
|
|
return memory.size() >= bytes && std::memcmp(memory.data(), expected, bytes) == 0;
|
|
}
|
|
|
|
const MockDescriptorWrite& findWrite(std::uint32_t binding) {
|
|
for (const auto& write : mock.writes) {
|
|
if (write.binding == binding) return write;
|
|
}
|
|
throw std::runtime_error("expected descriptor write is missing for binding " + std::to_string(binding));
|
|
}
|
|
|
|
const VkDescriptorSetLayoutBinding& findLayoutBinding(std::uint32_t binding) {
|
|
for (const auto& item : mock.layoutBindings) {
|
|
if (item.binding == binding) return item;
|
|
}
|
|
throw std::runtime_error("expected descriptor set layout binding is missing for binding " + std::to_string(binding));
|
|
}
|
|
|
|
std::span<const std::byte> 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{};
|
|
auto context = mockContext();
|
|
context.emptyBuffer = reinterpret_cast<VkBuffer>(std::uintptr_t{0x1000});
|
|
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 oversizedDescriptorTests() {
|
|
constexpr std::size_t blockBytes = 65536;
|
|
constexpr std::size_t tailBytes = 8192;
|
|
void* block = ::operator new(blockBytes, std::align_val_t{blockBytes});
|
|
std::memset(block, 0x5a, blockBytes);
|
|
{
|
|
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
|
GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, blockBytes, true, true, true);
|
|
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
|
}
|
|
const auto oversized = [](const void* pointer) { return makeBinding(Role::GuestBuffers, 0, 1, vsharp(pointer, 0xffffffffu)); };
|
|
const auto* tail = static_cast<const std::byte*>(block) + blockBytes - tailBytes;
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
ShaderRecompiler::RecompileResult fragment;
|
|
vertex.bindings.push_back(oversized(tail));
|
|
mock = MockVulkan{};
|
|
const auto context = mockContext();
|
|
const auto color = AgcDriver::Graphics::DecodeState(makeState()).color;
|
|
{
|
|
AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0);
|
|
const auto& write = findWrite(0);
|
|
Require(write.buffers.size() == 1 && write.buffers[0].range == tailBytes && sameBytes(bufferBytes(write.buffers[0].buffer), tail, tailBytes), "a V# past the range limit was not bound up to the end of its registered allocation");
|
|
}
|
|
Require(mock.live == 0, "a V# bound up to its registered allocation leaked Vulkan objects");
|
|
}
|
|
expectSingleFailure(oversized(block), "descriptor range limit");
|
|
expectSingleAccepted(oversized(reinterpret_cast<const void*>(0x1000)), "an unregistered V# past the range limit");
|
|
{
|
|
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
|
GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block);
|
|
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
|
}
|
|
::operator delete(block, std::align_val_t{blockBytes});
|
|
}
|
|
|
|
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<std::uint32_t, 4> key{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT};
|
|
const std::array<VkDescriptorBindingFlags, 1> flags{indexed};
|
|
const auto layout = cache.Layout(key, std::span(&binding, 1), false, flags);
|
|
Require(mock.layoutFlags == 0 && mock.layoutBindingFlags == std::vector<VkDescriptorBindingFlags>{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<VkBuffer>(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<std::uint32_t>(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<VkDescriptorBindingFlags>(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<std::uint32_t, 4> 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<std::uint32_t, 4> 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<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 8}}};
|
|
Require(AgcDriver::Graphics::PushConstantStages(shaders) == (VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT), "push constant stage union changed");
|
|
const auto bytes = AgcDriver::Graphics::AssemblePushConstants(shaders);
|
|
Require(bytes.size() == 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<ShaderResources::MovedBuffer> 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<std::uint32_t>(bytes / 4, static_cast<std::uint32_t>(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");
|
|
}
|
|
}
|
|
|
|
alignas(16) std::array<std::uint32_t, 8> guestMovedFirst{0x51515151, 0x52525252, 0x53535353, 0x54545454, 0x55555555, 0x56565656, 0x57575757, 0x58585858};
|
|
alignas(16) std::array<std::uint32_t, 8> guestMovedSecond{0x61616161, 0x62626262, 0x63636363, 0x64646464, 0x65656565, 0x66666666, 0x67676767, 0x68686868};
|
|
|
|
void dispatchMoveTests() {
|
|
using AgcDriver::Graphics::CompiledShader;
|
|
using AgcDriver::Graphics::DescriptorCache;
|
|
using AgcDriver::Graphics::Recorder;
|
|
using AgcDriver::Graphics::ShaderResources;
|
|
using Refusal = ShaderResources::MoveRefusal;
|
|
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 built;
|
|
auto inputs = makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)));
|
|
inputs.bufferWritten = {false, false};
|
|
built.bindings.push_back(inputs);
|
|
auto output = makeBinding(Role::GuestBuffers, 4, 1, vsharp(guestThird.data(), 8));
|
|
output.bufferWritten = {true};
|
|
built.bindings.push_back(output);
|
|
const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &built, 0};
|
|
ShaderResources resources(context, shader);
|
|
const auto moveTo = [&](std::size_t element, const void* where, std::uint32_t bytes) {
|
|
auto program = built;
|
|
const auto words = vsharp(where, bytes);
|
|
std::copy(words.begin(), words.end(), program.bindings[0].guestDescriptor.begin() + static_cast<std::ptrdiff_t>(element) * 4);
|
|
return program;
|
|
};
|
|
const auto prove = [&](const ShaderRecompiler::RecompileResult& program, std::size_t maxBytes, ShaderResources::MoveProof& proof) {
|
|
return resources.MovedDispatchBuffers(CompiledShader{ShaderRecompiler::ShaderStage::Compute, &program, 0}, &recorder, maxBytes, proof);
|
|
};
|
|
const auto refuses = [&](const ShaderRecompiler::RecompileResult& program, std::size_t maxBytes, Refusal why, const char* what) {
|
|
ShaderResources::MoveProof refusal;
|
|
const auto listed = prove(program, maxBytes, refusal);
|
|
Require(!listed.has_value() && refusal.refusal == why, what);
|
|
};
|
|
|
|
ShaderResources::MoveProof proof;
|
|
auto moved = prove(built, 0, proof);
|
|
Require(moved.has_value() && moved->empty() && proof.refusal == Refusal::None && proof.buffers == 0, "a dispatch with every buffer where it was built moved something");
|
|
|
|
const auto relocated = moveTo(1, guestMovedSecond.data(), 24);
|
|
const CompiledShader relocatedShader{ShaderRecompiler::ShaderStage::Compute, &relocated, 0};
|
|
Require(ShaderResources::ContentKey(shader, false, true) == ShaderResources::ContentKey(relocatedShader, false, true), "moving a read-only buffer changed the movable content key");
|
|
Require(ShaderResources::ContentKey(shader, false, false) != ShaderResources::ContentKey(relocatedShader, false, false), "the fixed content key ignored a moved buffer");
|
|
|
|
moved = prove(relocated, 0, proof);
|
|
Require(moved.has_value() && moved->size() == 1 && proof.refusal == Refusal::None && proof.buffers == 1 && proof.bytes >= 24, "a moved read-only buffer was not listed");
|
|
Require(moved->front().address == reinterpret_cast<std::uintptr_t>(guestMovedSecond.data()) && moved->front().size == 24 && moved->front().words.empty(), "the listed buffer is not the moved range");
|
|
|
|
moved = prove(moveTo(0, guestFirst.data(), 8), 0, proof);
|
|
Require(moved.has_value() && moved->size() == 1 && moved->front().address == reinterpret_cast<std::uintptr_t>(guestFirst.data()) && moved->front().size == 8, "a buffer that only changed its size was not listed");
|
|
|
|
auto both = relocated;
|
|
const auto firstWords = vsharp(guestMovedFirst.data(), 12);
|
|
std::copy(firstWords.begin(), firstWords.end(), both.bindings[0].guestDescriptor.begin());
|
|
moved = prove(both, 1024, proof);
|
|
Require(moved.has_value() && moved->size() == 2 && proof.buffers == 2 && proof.bytes >= 36, "two moved buffers were not both listed");
|
|
refuses(both, 30, Refusal::Limit, "moved buffers past the byte budget together were served");
|
|
|
|
auto written = relocated;
|
|
written.bindings[0].bufferWritten[1] = true;
|
|
refuses(written, 0, Refusal::Written, "a moved buffer the dispatch writes was served");
|
|
refuses(moveTo(1, guestMovedSecond.data(), 20000), 0, Refusal::Written, "a moved buffer over the storage buffer range that no registered allocation holds was served");
|
|
refuses(relocated, 16, Refusal::Limit, "a moved buffer over the byte budget was served");
|
|
refuses(moveTo(1, reinterpret_cast<const void*>(std::uintptr_t{0x10}), 24), 0, Refusal::Inaccessible, "a moved buffer over unmapped memory was served");
|
|
refuses(moveTo(1, guestThird.data(), 8), 0, Refusal::Overlap, "a moved buffer over the dispatch's own written range was served");
|
|
auto shaped = relocated;
|
|
shaped.bindings[0].guestDescriptor.resize(4);
|
|
refuses(shaped, 0, Refusal::Shape, "a dispatch with another binding shape was served");
|
|
recorder.Sync();
|
|
}
|
|
{
|
|
constexpr std::size_t blockBytes = 65536;
|
|
void* block = ::operator new(blockBytes, std::align_val_t{blockBytes});
|
|
std::memset(block, 0x5a, blockBytes);
|
|
{
|
|
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
|
GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, blockBytes, true, true, true);
|
|
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
|
}
|
|
const auto* end = static_cast<const std::byte*>(block) + blockBytes;
|
|
const auto strided = [](const void* pointer, std::uint32_t stride, std::uint32_t records) {
|
|
auto words = vsharp(pointer, records);
|
|
words[1] |= stride << 16u;
|
|
return words;
|
|
};
|
|
DescriptorCache cache(context);
|
|
context.descriptorCache = &cache;
|
|
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
|
Recorder recorder(context);
|
|
const auto proveAgainst = [&](const ShaderRecompiler::RecompileResult& built, const std::vector<std::uint32_t>& incoming, ShaderResources::MoveProof& proof, std::optional<std::vector<ShaderResources::MovedBuffer>>& moved) {
|
|
const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &built, 0};
|
|
ShaderResources resources(context, shader);
|
|
auto program = built;
|
|
program.bindings[0].guestDescriptor = incoming;
|
|
moved = resources.MovedDispatchBuffers(CompiledShader{ShaderRecompiler::ShaderStage::Compute, &program, 0}, &recorder, 0, proof);
|
|
};
|
|
const auto readOnly = [&](const std::vector<std::uint32_t>& words) {
|
|
ShaderRecompiler::RecompileResult built;
|
|
auto binding = makeBinding(Role::GuestBuffers, 0, 1, words);
|
|
binding.bufferWritten = {false};
|
|
built.bindings.push_back(binding);
|
|
return built;
|
|
};
|
|
const auto address = [](const void* pointer) { return reinterpret_cast<std::uintptr_t>(pointer); };
|
|
ShaderResources::MoveProof proof;
|
|
std::optional<std::vector<ShaderResources::MovedBuffer>> moved;
|
|
|
|
const auto* tail = end - 8192;
|
|
const auto* nearEnd = end - 4096;
|
|
const auto oversized = readOnly(vsharp(tail, 0xffffffffu));
|
|
proveAgainst(oversized, vsharp(tail, 0xffffffffu), proof, moved);
|
|
Require(moved.has_value() && moved->empty() && proof.refusal == Refusal::None && proof.buffers == 0, "an unchanged V# the build cut to its allocation end was not an exact hit");
|
|
proveAgainst(oversized, vsharp(nearEnd, 0xffffffffu), proof, moved);
|
|
Require(moved.has_value() && moved->size() == 1 && proof.buffers == 1 && proof.bytes == 4096 && moved->front().address == address(nearEnd) && moved->front().size == 4096, "a moved V# past the range limit was not snapshotted at its clamped range");
|
|
proveAgainst(oversized, vsharp(nearEnd, 0xfffffff0u), proof, moved);
|
|
Require(moved.has_value() && moved->size() == 1 && moved->front().size == 4096, "another oversized size over the same allocation end listed another range");
|
|
proveAgainst(oversized, vsharp(tail, 4096), proof, moved);
|
|
Require(moved.has_value() && moved->size() == 1 && moved->front().address == address(tail) && moved->front().size == 4096, "a V# that now fits the range limit was not listed at its own size");
|
|
|
|
proveAgainst(oversized, vsharp(block, 0xffffffffu), proof, moved);
|
|
Require(!moved.has_value() && proof.refusal == Refusal::Limit, "a moved V# the allocation holds past the range limit was served");
|
|
|
|
const auto overhang = readOnly(strided(tail, 24, 342));
|
|
proveAgainst(overhang, strided(tail, 24, 342), proof, moved);
|
|
Require(moved.has_value() && moved->empty() && proof.refusal == Refusal::None, "an unchanged V# with a stride overhang past the allocation was not an exact hit");
|
|
proveAgainst(overhang, strided(nearEnd, 24, 171), proof, moved);
|
|
Require(moved.has_value() && moved->size() == 1 && moved->front().address == address(nearEnd) && moved->front().size == 4096 && proof.bytes == 4096, "a moved V# with a stride overhang was snapshotted past the allocation end");
|
|
proveAgainst(overhang, strided(nearEnd, 24, 100), proof, moved);
|
|
Require(moved.has_value() && moved->size() == 1 && moved->front().size == 2400, "a moved V# inside the allocation was not listed at its own size");
|
|
recorder.Sync();
|
|
{
|
|
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
|
GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block);
|
|
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
|
}
|
|
::operator delete(block, std::align_val_t{blockBytes});
|
|
}
|
|
Require(mock.live == 0, "dispatch move resources leaked Vulkan objects");
|
|
}
|
|
|
|
void resourceTests() {
|
|
mock = MockVulkan{};
|
|
const auto context = mockContext();
|
|
const auto state = AgcDriver::Graphics::DecodeState(makeState());
|
|
const auto commands = reinterpret_cast<VkCommandBuffer>(std::uintptr_t{1});
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
ShaderRecompiler::RecompileResult fragment;
|
|
vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32))));
|
|
vertex.bindings.push_back(makeBinding(Role::ShaderData, 5, 1, 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<std::uint32_t, 2> srt{1, 2};
|
|
Require(findWrite(43).buffers.size() == 1 && findWrite(43).buffers[0].range == 8 && sameBytes(bufferBytes(findWrite(43).buffers[0].buffer), srt.data(), 8), "flattened SRT buffer is incorrect");
|
|
Require(findWrite(44).buffers.size() == 1 && findWrite(44).buffers[0].range == 8 && sameBytes(bufferBytes(findWrite(44).buffers[0].buffer), guestThird.data(), 8), "fragment guest buffer is incorrect");
|
|
resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, VK_NULL_HANDLE);
|
|
Require(mock.boundPoint == VK_PIPELINE_BIND_POINT_GRAPHICS && mock.boundFirst == 0 && mock.boundSets == 1, "exactly one descriptor set must be bound at set zero");
|
|
auto first = 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<std::uint32_t>(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");
|
|
expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x3fffu << 16u; binding.guestDescriptor[2] = 0xffffffffu; }), "an unregistered V# past the range limit");
|
|
expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor[2] = 32768; }), "an unregistered V# past the range limit");
|
|
expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast<const void*>(0x1000), 8); }), "an unmapped V#");
|
|
expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast<const void*>(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<const void*>(0x1000), 8)); }), "an unmapped V# element");
|
|
expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast<const void*>(state.color.address), 64)); }), "aliases the render target");
|
|
expectSingleFailure(changed([](auto& binding) { binding.count = 17; binding.guestDescriptor.assign(68, 0); }), "per-stage limits");
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
ShaderRecompiler::RecompileResult fragment;
|
|
std::vector<std::uint32_t> words;
|
|
for (int index = 0; index < 17; ++index) words = join(words, vsharp(reinterpret_cast<const void*>(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<const void*>(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<std::uint32_t>(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<std::uint32_t>(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<AgcDriver::Graphics::CompiledShader, 2> 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<std::uint32_t, 4> 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<VkDescriptorPoolSize, 2> 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<VkDescriptorPoolSize, 1> 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<VkDescriptorPoolSize, 1> 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<VkDescriptorPoolSize, 1> 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<VkDescriptorPoolSize, 1> 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<VkDescriptorPoolSize, 1> 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");
|
|
mock = MockVulkan{};
|
|
context.limits.maxDescriptorSetStorageBuffers = 16;
|
|
context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 8192;
|
|
context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 16384;
|
|
context.limits.maxPerStageDescriptorSampledImages = 2048;
|
|
{
|
|
AgcDriver::Graphics::DescriptorCache cache(context);
|
|
const std::array<VkDescriptorSetLayoutBinding, 2> bindings{{{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4097, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}, {1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 32, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}}};
|
|
constexpr auto tableFlags = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT;
|
|
const std::array<VkDescriptorBindingFlags, 2> flags{VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT, tableFlags};
|
|
const std::array<std::uint32_t, 9> key{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4097, VK_SHADER_STAGE_FRAGMENT_BIT, 1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 32, VK_SHADER_STAGE_FRAGMENT_BIT, VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT};
|
|
const auto layout = cache.Layout(key, bindings, true, flags);
|
|
Require(mock.layoutFlags == VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT && mock.layoutBindingFlags == std::vector<VkDescriptorBindingFlags>{VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT, tableFlags}, "a variable image table lost its flags when the storage binding used update-after-bind");
|
|
Require(cache.Layout(key, bindings, true, flags) == layout && cache.Counters().layoutHits == 1, "the combined descriptor layout was not reused");
|
|
const std::array<VkDescriptorPoolSize, 2> oversized{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4097}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 3}}};
|
|
const auto dedicated = cache.Allocate(layout, oversized, true, 3u);
|
|
Require(mock.poolMaxSets == 1 && mock.poolFlags == VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT && mock.variableDescriptorCount == 3u, "a dedicated update-after-bind pool lost the variable image count");
|
|
cache.Free(dedicated);
|
|
auto fittingBindings = bindings;
|
|
fittingBindings[0].descriptorCount = 17;
|
|
auto fittingKey = key;
|
|
fittingKey[2] = 17;
|
|
const auto fittingLayout = cache.Layout(fittingKey, fittingBindings, true, flags);
|
|
const std::array<VkDescriptorPoolSize, 2> fitting{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 17}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 3}}};
|
|
const auto chained = cache.Allocate(fittingLayout, fitting, true, 3u);
|
|
Require(mock.poolMaxSets == 1024 && mock.poolFlags == VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT && mock.variableDescriptorCount == 3u, "a chained update-after-bind pool lost the variable image count");
|
|
cache.Free(chained);
|
|
const auto reused = cache.Allocate(fittingLayout, fitting, true, 0u);
|
|
Require(reused.pool == chained.pool && mock.variableDescriptorCount == 0u, "reusing an update-after-bind pool lost an empty variable image table");
|
|
cache.Free(reused);
|
|
expectFailure([&] { cache.Allocate(fittingLayout, fitting, false, 3u); }, "descriptor set exceeds the device's per-set descriptor limit");
|
|
}
|
|
Require(mock.live == 0, "the combined descriptor cache leaked a pool or layout");
|
|
}
|
|
|
|
void imageTableAllocationTests() {
|
|
namespace Abi = ShaderRecompiler::ImageTableAbi;
|
|
for (const bool cached : {false, true}) {
|
|
for (const std::uint32_t count : {0u, 2u, 3u}) {
|
|
mock = MockVulkan{};
|
|
auto context = mockContext();
|
|
context.imageTableCapacity = 2;
|
|
context.limits.maxDescriptorSetSampledImages = 2;
|
|
context.limits.maxPerStageDescriptorSampledImages = 2;
|
|
{
|
|
AgcDriver::Graphics::DescriptorCache cache(context);
|
|
if (cached) context.descriptorCache = &cache;
|
|
ShaderRecompiler::RecompileResult program;
|
|
auto map = makeBinding(Role::ImageTableMap, static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector<std::uint32_t>(Abi::HeaderWords + count));
|
|
map.guestDescriptor[Abi::HeaderVersion] = Abi::Version;
|
|
map.guestDescriptor[Abi::HeaderElementCount] = count;
|
|
map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords;
|
|
map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + count;
|
|
program.bindings.push_back(map);
|
|
auto images = makeBinding(Role::ImageTable, ShaderRecompiler::RuntimeAbi::ImageTableBinding, count, std::vector<std::uint32_t>(count * 8u));
|
|
images.kind = Kind::SampledImage;
|
|
images.imageShapes.resize(count);
|
|
images.imageDepthCompare.resize(count);
|
|
images.imageSamplers.resize(count);
|
|
program.bindings.push_back(std::move(images));
|
|
const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0};
|
|
{
|
|
AgcDriver::Graphics::ShaderResources resources(context, shader, {}, true);
|
|
Require(findLayoutBinding(ShaderRecompiler::RuntimeAbi::ImageTableBinding).descriptorCount == 2u, "the image table layout changed its device capacity");
|
|
const auto physicalCount = std::max(1u, std::min(count, 2u));
|
|
Require(mock.variableDescriptorCount == physicalCount, "image table allocation did not reserve its bounded physical descriptor count");
|
|
const auto sampled = std::find_if(mock.poolSizes.begin(), mock.poolSizes.end(), [](const auto& size) { return size.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; });
|
|
Require(sampled != mock.poolSizes.end() && sampled->descriptorCount >= physicalCount && (cached || sampled->descriptorCount == physicalCount), "the image table pool did not cover its bounded allocation");
|
|
resources.Complete();
|
|
const auto& bytes = bufferBytes(findWrite(map.binding).buffers.at(0).buffer);
|
|
std::vector<std::uint32_t> actual(bytes.size() / sizeof(std::uint32_t));
|
|
std::memcpy(actual.data(), bytes.data(), bytes.size());
|
|
Require(actual[Abi::HeaderElementCount] == count, "bounding the Vulkan allocation truncated the guest table domain");
|
|
if (count > context.imageTableCapacity) Require((actual[Abi::HeaderWords + 2u] & Abi::PoisonFlag) != 0u, "an image beyond Vulkan capacity lost its deferred fault");
|
|
}
|
|
}
|
|
Require(mock.live == 0, "the bounded image table leaked Vulkan objects");
|
|
}
|
|
}
|
|
}
|
|
|
|
void imageTableSnapshotAllocationTests() {
|
|
namespace Abi = ShaderRecompiler::ImageTableAbi;
|
|
using namespace AgcDriver::Graphics;
|
|
mock = MockVulkan{};
|
|
auto context = mockContext();
|
|
context.imageTableCapacity = 2;
|
|
context.limits.maxDescriptorSetSampledImages = 2;
|
|
context.limits.maxPerStageDescriptorSampledImages = 2;
|
|
{
|
|
DescriptorCache cache(context);
|
|
context.descriptorCache = &cache;
|
|
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
|
Recorder recorder(context);
|
|
ShaderRecompiler::RecompileResult vertex, fragment;
|
|
auto buffer = makeBinding(Role::GuestBuffers, 0, 1, vsharp(guestFirst.data(), 16));
|
|
buffer.bufferWritten = {false};
|
|
vertex.bindings.push_back(buffer);
|
|
auto map = makeBinding(Role::ImageTableMap, static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector<std::uint32_t>(Abi::HeaderWords));
|
|
map.guestDescriptor[Abi::HeaderVersion] = Abi::Version;
|
|
map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords;
|
|
map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords;
|
|
vertex.bindings.push_back(map);
|
|
auto images = makeBinding(Role::ImageTable, ShaderRecompiler::RuntimeAbi::ImageTableBinding, 0, {});
|
|
images.kind = Kind::SampledImage;
|
|
vertex.bindings.push_back(images);
|
|
const auto state = DecodeState(makeState());
|
|
ShaderResources resources(context, vertex, fragment, state.color, 0, 0);
|
|
const auto originalMap = findWrite(map.binding).buffers.at(0);
|
|
const std::array<ShaderResources::MovedBuffer, 1> moved{{{0, 0, 16, {11, 22, 33, 44}}}};
|
|
const auto sets = cache.Counters().sets;
|
|
mock.writes.clear();
|
|
mock.variableDescriptorCount.reset();
|
|
const auto snapshots = resources.PrepareDrawBindings(recorder, moved);
|
|
Require(snapshots != nullptr && snapshots->snapshots.size() == 1 && cache.Counters().sets == sets + 1, "an empty image table prevented draw snapshot allocation");
|
|
Require(mock.variableDescriptorCount == 1u, "an empty draw snapshot table did not reserve one physical image descriptor");
|
|
Require(sameBytes(bufferBytes(snapshots->snapshots[0].buffer->Handle()), moved[0].words.data(), moved[0].size), "an empty image table changed draw snapshot contents");
|
|
const auto& currentMap = findWrite(map.binding).buffers.at(0);
|
|
Require(currentMap.buffer == originalMap.buffer && currentMap.offset == originalMap.offset && currentMap.range == originalMap.range, "draw snapshots replaced the empty image table map");
|
|
std::array<std::uint32_t, Abi::HeaderWords> header{};
|
|
std::memcpy(header.data(), bufferBytes(currentMap.buffer).data() + currentMap.offset, sizeof(header));
|
|
Require(header[Abi::HeaderElementCount] == 0u, "draw snapshots changed the empty guest table domain");
|
|
Require(std::none_of(mock.writes.begin(), mock.writes.end(), [](const auto& write) { return write.binding == ShaderRecompiler::RuntimeAbi::ImageTableBinding; }), "an empty draw snapshot table wrote an image descriptor");
|
|
recorder.Sync();
|
|
}
|
|
Require(mock.live == 0, "the empty image table draw snapshot leaked Vulkan objects");
|
|
}
|
|
|
|
void samplerTableNonSeamlessCubeTests() {
|
|
namespace Abi = ShaderRecompiler::ImageTableAbi;
|
|
constexpr auto count = 2u * Abi::SamplerEntryElements;
|
|
for (const bool supported : {false, true}) {
|
|
for (const std::uint32_t flaggedEntry : {0u, 1u}) {
|
|
mock = MockVulkan{};
|
|
auto context = mockContext();
|
|
context.nonSeamlessCubeMap = supported;
|
|
context.samplerTableCapacity = count;
|
|
context.limits.maxDescriptorSetSamplers = count;
|
|
context.limits.maxPerStageDescriptorSamplers = count;
|
|
context.limits.maxSamplerAnisotropy = 1.0f;
|
|
ShaderRecompiler::RecompileResult program;
|
|
auto map = makeBinding(Role::ImageTableMap, static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector<std::uint32_t>(Abi::HeaderWords + 2u));
|
|
map.guestDescriptor[Abi::HeaderVersion] = Abi::Version;
|
|
map.guestDescriptor[Abi::HeaderSamplerCount] = count;
|
|
map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords;
|
|
map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords;
|
|
program.bindings.push_back(map);
|
|
auto samplers = makeBinding(Role::SamplerTable, static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::SamplerTable), count, {});
|
|
samplers.kind = Kind::Sampler;
|
|
for (std::uint32_t entry = 0; entry < 2u; ++entry) {
|
|
const std::array<std::uint32_t, 4> words{0x92u | (entry == flaggedEntry ? 1u << 28u : 0u), (4u * 256u) << 12u, 0u, 0u};
|
|
for (std::uint32_t variant = 0; variant < Abi::SamplerEntryElements; ++variant) {
|
|
samplers.guestDescriptor.insert(samplers.guestDescriptor.end(), words.begin(), words.end());
|
|
samplers.samplerDepthCompare.push_back(variant >= 2u);
|
|
}
|
|
}
|
|
const auto samplerBinding = samplers.binding;
|
|
program.bindings.push_back(std::move(samplers));
|
|
const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0};
|
|
{
|
|
AgcDriver::Graphics::ShaderResources resources(context, shader, {}, true);
|
|
resources.Complete();
|
|
const auto& bytes = bufferBytes(findWrite(map.binding).buffers.at(0).buffer);
|
|
std::vector<std::uint32_t> actual(bytes.size() / sizeof(std::uint32_t));
|
|
std::memcpy(actual.data(), bytes.data(), bytes.size());
|
|
Require(actual[Abi::HeaderSamplerCount] == count, "non-seamless sampler rejection changed the guest table domain");
|
|
Require(actual[Abi::HeaderPoisonCount] == (supported ? 0u : 1u), "non-seamless sampler table has the wrong deferred poison count");
|
|
for (std::uint32_t entry = 0; entry < 2u; ++entry) {
|
|
const auto expected = !supported && entry == flaggedEntry ? Abi::PoisonCode(0u) : 0u;
|
|
Require(actual[Abi::HeaderWords + entry] == expected, "non-seamless sampler rejection poisoned the wrong entry");
|
|
}
|
|
const auto& write = findWrite(samplerBinding);
|
|
const auto first = supported ? 0u : (1u - flaggedEntry) * Abi::SamplerEntryElements;
|
|
const auto written = supported ? count : Abi::SamplerEntryElements;
|
|
Require(findLayoutBinding(samplerBinding).descriptorCount == count && write.type == VK_DESCRIPTOR_TYPE_SAMPLER && write.arrayElement == first && write.count == written && write.images.size() == written, "non-seamless sampler rejection compacted or truncated descriptor indices");
|
|
Require(mock.samplerFlags.size() == written, "a rejected sampler table entry allocated a Vulkan sampler");
|
|
for (std::uint32_t index = 0; index < written; ++index) {
|
|
const auto expected = supported && (first + index) / Abi::SamplerEntryElements == flaggedEntry ? VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT : 0u;
|
|
Require(mock.samplerFlags.at(write.images[index].sampler) == expected, "a sampler table variant has the wrong non-seamless cube creation flag");
|
|
}
|
|
}
|
|
Require(mock.live == 0 && mock.samplerFlags.empty(), "the non-seamless sampler table leaked Vulkan objects");
|
|
}
|
|
}
|
|
}
|
|
|
|
void samplerTableDeviceLimitTests(std::optional<std::uint32_t> selected = std::nullopt) {
|
|
namespace Abi = ShaderRecompiler::ImageTableAbi;
|
|
constexpr auto count = 2u * Abi::SamplerEntryElements;
|
|
const std::array<std::uint32_t, 4> valid{0x92u, (4u * 256u) << 12u, 0u, 0u};
|
|
const std::array<std::array<std::uint32_t, 4>, 3> invalid{{
|
|
{0x92u, valid[1], 256u, 0u},
|
|
{0x92u | (2u << 9u), valid[1], (2u << 20u) | (2u << 22u), 0u},
|
|
{0x92u | (1u << 29u), valid[1], 0u, 0u}
|
|
}};
|
|
for (std::uint32_t kind = 0; kind < invalid.size(); ++kind) {
|
|
if (selected.has_value() && kind != *selected) continue;
|
|
for (const std::uint32_t badEntry : {0u, 1u}) {
|
|
mock = MockVulkan{};
|
|
auto context = mockContext();
|
|
context.samplerTableCapacity = count;
|
|
context.limits.maxDescriptorSetSamplers = count;
|
|
context.limits.maxPerStageDescriptorSamplers = count;
|
|
context.samplerAnisotropy = true;
|
|
context.samplerFilterMinmax = true;
|
|
context.limits.maxSamplerAnisotropy = 2.0f;
|
|
context.limits.maxSamplerLodBias = 0.5f;
|
|
ShaderRecompiler::RecompileResult program;
|
|
auto map = makeBinding(Role::ImageTableMap, static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector<std::uint32_t>(Abi::HeaderWords + 2u));
|
|
map.guestDescriptor[Abi::HeaderVersion] = Abi::Version;
|
|
map.guestDescriptor[Abi::HeaderSamplerCount] = count;
|
|
map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords;
|
|
map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords;
|
|
program.bindings.push_back(map);
|
|
auto samplers = makeBinding(Role::SamplerTable, static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::SamplerTable), count, {});
|
|
samplers.kind = Kind::Sampler;
|
|
for (std::uint32_t entry = 0; entry < 2u; ++entry) {
|
|
const auto& words = entry == badEntry ? invalid[kind] : valid;
|
|
for (std::uint32_t variant = 0; variant < Abi::SamplerEntryElements; ++variant) {
|
|
samplers.guestDescriptor.insert(samplers.guestDescriptor.end(), words.begin(), words.end());
|
|
samplers.samplerDepthCompare.push_back(variant >= 2u);
|
|
}
|
|
}
|
|
const auto samplerBinding = samplers.binding;
|
|
program.bindings.push_back(std::move(samplers));
|
|
const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0};
|
|
{
|
|
AgcDriver::Graphics::ShaderResources resources(context, shader, {}, true);
|
|
resources.Complete();
|
|
const auto& bytes = bufferBytes(findWrite(map.binding).buffers.at(0).buffer);
|
|
std::vector<std::uint32_t> actual(bytes.size() / sizeof(std::uint32_t));
|
|
std::memcpy(actual.data(), bytes.data(), bytes.size());
|
|
Require(actual[Abi::HeaderSamplerCount] == count && actual[Abi::HeaderPoisonCount] == 1u, "sampler device validation changed the domain or deferred poison count");
|
|
for (std::uint32_t entry = 0; entry < 2u; ++entry) {
|
|
Require(actual[Abi::HeaderWords + entry] == (entry == badEntry ? Abi::PoisonCode(0u) : 0u), "sampler device validation poisoned the valid entry");
|
|
}
|
|
const auto& write = findWrite(samplerBinding);
|
|
Require(write.type == VK_DESCRIPTOR_TYPE_SAMPLER && write.arrayElement == (1u - badEntry) * Abi::SamplerEntryElements && write.count == Abi::SamplerEntryElements && write.images.size() == Abi::SamplerEntryElements, "sampler device validation changed valid descriptor indices");
|
|
Require(mock.samplerCreateCount == Abi::SamplerEntryElements && mock.samplerFlags.size() == Abi::SamplerEntryElements, "a rejected sampler table entry created a Vulkan sampler");
|
|
}
|
|
Require(mock.live == 0 && mock.samplerFlags.empty(), "sampler device validation leaked Vulkan objects");
|
|
}
|
|
}
|
|
}
|
|
|
|
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 storageTextureBudgetTests() {
|
|
using AgcDriver::Graphics::StorageTextureBudget;
|
|
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(StorageTextureBudget(memory, nullptr) == 4 * GiB, "without VK_EXT_memory_budget the storage image cache does not keep a quarter of the 16 GiB heap");
|
|
reported.heapBudget[0] = 100 * GiB;
|
|
reported.heapBudget[1] = 100 * GiB;
|
|
Require(StorageTextureBudget(memory, &reported) == 4 * GiB, "a zero budget for the largest device-local heap did not fall back to a quarter of the heap");
|
|
reported.heapBudget[2] = 10 * GiB;
|
|
Require(StorageTextureBudget(memory, &reported) == 2560 * MiB, "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget");
|
|
reported.heapBudget[2] = 6 * GiB;
|
|
Require(StorageTextureBudget(memory, &reported) == 1536 * MiB, "a 6 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget");
|
|
reported.heapBudget[2] = 1 * GiB;
|
|
Require(StorageTextureBudget(memory, &reported) == 512 * MiB, "a 1 GiB budget does not keep the 512 MiB floor");
|
|
reported.heapBudget[2] = 40 * GiB;
|
|
Require(StorageTextureBudget(memory, &reported) == 4 * GiB, "a budget above the heap size raised the storage image cache past a quarter of the heap");
|
|
memory.memoryHeaps[0].flags = 0;
|
|
memory.memoryHeaps[2].flags = 0;
|
|
Require(StorageTextureBudget(memory, &reported) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images");
|
|
}
|
|
|
|
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<VkCommandBuffer>(std::uintptr_t{1});
|
|
alignas(64) std::array<std::uint32_t, 8> 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<std::uint32_t> 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;
|
|
std::uint32_t clipDistanceLength = 0;
|
|
std::uint32_t cullDistanceLength = 0;
|
|
bool layerOutput = false;
|
|
bool terminateInvocation = false;
|
|
bool omitTerminateExtension = false;
|
|
};
|
|
|
|
void emit(std::vector<std::uint32_t>& out, spv::Op op, std::initializer_list<std::uint32_t> operands) {
|
|
out.push_back((static_cast<std::uint32_t>(operands.size() + 1) << 16u) | static_cast<std::uint32_t>(op));
|
|
out.insert(out.end(), operands.begin(), operands.end());
|
|
}
|
|
|
|
std::vector<std::uint32_t> makeModule(const ModuleShape& shape) {
|
|
std::vector<std::uint32_t> annotations;
|
|
std::vector<std::uint32_t> declarations;
|
|
std::vector<std::uint32_t> function;
|
|
std::uint32_t next = 1;
|
|
const auto id = [&] { return next++; };
|
|
const auto voidType = id();
|
|
const auto functionType = id();
|
|
const auto floatType = id();
|
|
const auto vectorType = id();
|
|
const auto uintType = id();
|
|
const auto outputPointer = id();
|
|
const auto output = id();
|
|
const auto main = id();
|
|
const auto label = id();
|
|
const auto inputPointer = id();
|
|
const auto input = id();
|
|
std::vector<std::uint32_t> extraInterface;
|
|
emit(declarations, spv::OpTypeVoid, {voidType});
|
|
emit(declarations, spv::OpTypeFunction, {functionType, voidType});
|
|
emit(declarations, spv::OpTypeFloat, {floatType, 32});
|
|
emit(declarations, spv::OpTypeVector, {vectorType, floatType, 4});
|
|
emit(declarations, spv::OpTypeInt, {uintType, 32, 0});
|
|
emit(declarations, spv::OpTypePointer, {outputPointer, spv::StorageClassOutput, vectorType});
|
|
emit(declarations, spv::OpVariable, {outputPointer, output, spv::StorageClassOutput});
|
|
if (shape.parameterOutput) {
|
|
const auto parameter = id();
|
|
emit(declarations, spv::OpVariable, {outputPointer, parameter, spv::StorageClassOutput});
|
|
emit(annotations, spv::OpDecorate, {parameter, spv::DecorationLocation, shape.parameterLocation});
|
|
extraInterface.push_back(parameter);
|
|
}
|
|
if (shape.layerOutput) {
|
|
const auto pointer = id();
|
|
const auto layer = id();
|
|
emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, uintType});
|
|
emit(declarations, spv::OpVariable, {pointer, layer, spv::StorageClassOutput});
|
|
emit(annotations, spv::OpDecorate, {layer, spv::DecorationBuiltIn, spv::BuiltInLayer});
|
|
extraInterface.push_back(layer);
|
|
}
|
|
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<std::uint32_t>(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.clipDistanceLength != 0) {
|
|
const auto length = id();
|
|
const auto array = id();
|
|
const auto pointer = id();
|
|
const auto variable = id();
|
|
emit(declarations, spv::OpConstant, {uintType, length, shape.clipDistanceLength});
|
|
emit(declarations, spv::OpTypeArray, {array, floatType, 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::BuiltInClipDistance});
|
|
extraInterface.push_back(variable);
|
|
}
|
|
if (shape.cullDistanceLength != 0) {
|
|
const auto length = id();
|
|
const auto array = id();
|
|
const auto pointer = id();
|
|
const auto variable = id();
|
|
emit(declarations, spv::OpConstant, {uintType, length, shape.cullDistanceLength});
|
|
emit(declarations, spv::OpTypeArray, {array, floatType, 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::BuiltInCullDistance});
|
|
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, shape.terminateInvocation ? spv::OpTerminateInvocation : spv::OpReturn, {});
|
|
emit(function, spv::OpFunctionEnd, {});
|
|
std::vector<std::uint32_t> words{spv::MagicNumber, 0x10300, 0, next, 0};
|
|
emit(words, spv::OpCapability, {spv::CapabilityShader});
|
|
if (shape.sampleId) emit(words, spv::OpCapability, {spv::CapabilitySampleRateShading});
|
|
if (shape.layer) emit(words, spv::OpCapability, {spv::CapabilityGeometry});
|
|
if (shape.clipDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityClipDistance});
|
|
if (shape.cullDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityCullDistance});
|
|
if (shape.layerOutput) {
|
|
emit(words, spv::OpCapability, {spv::CapabilityShaderViewportIndexLayerEXT});
|
|
const std::string extension = "SPV_EXT_shader_viewport_index_layer";
|
|
const auto count = (extension.size() + 4) / 4;
|
|
words.push_back((static_cast<std::uint32_t>(count + 1) << 16u) | spv::OpExtension);
|
|
const auto start = words.size();
|
|
words.resize(start + count, 0);
|
|
for (std::size_t i = 0; i < extension.size(); ++i) words[start + i / 4] |= static_cast<std::uint32_t>(static_cast<unsigned char>(extension[i])) << ((i % 4) * 8);
|
|
}
|
|
if (shape.barycentric) {
|
|
emit(words, spv::OpCapability, {spv::CapabilityFragmentBarycentricKHR});
|
|
const std::string extension = "SPV_KHR_fragment_shader_barycentric";
|
|
const auto count = (extension.size() + 4) / 4;
|
|
words.push_back((static_cast<std::uint32_t>(count + 1) << 16u) | spv::OpExtension);
|
|
const auto start = words.size();
|
|
words.resize(start + count, 0);
|
|
for (std::size_t i = 0; i < extension.size(); ++i) words[start + i / 4] |= static_cast<std::uint32_t>(static_cast<unsigned char>(extension[i])) << ((i % 4) * 8);
|
|
}
|
|
if (shape.terminateInvocation && !shape.omitTerminateExtension) {
|
|
const std::string extension = "SPV_KHR_terminate_invocation";
|
|
const auto count = (extension.size() + 4) / 4;
|
|
words.push_back((static_cast<std::uint32_t>(count + 1) << 16u) | spv::OpExtension);
|
|
const auto start = words.size();
|
|
words.resize(start + count, 0);
|
|
for (std::size_t i = 0; i < extension.size(); ++i) words[start + i / 4] |= static_cast<std::uint32_t>(static_cast<unsigned char>(extension[i])) << ((i % 4) * 8);
|
|
}
|
|
emit(words, spv::OpMemoryModel, {spv::AddressingModelLogical, spv::MemoryModelGLSL450});
|
|
const auto entryPointOffset = words.size();
|
|
if (shape.vertexInput) emit(words, spv::OpEntryPoint, {spv::ExecutionModelVertex, main, 0x6e69616du, 0, output, input});
|
|
else emit(words, spv::OpEntryPoint, {shape.fragment ? spv::ExecutionModelFragment : spv::ExecutionModelVertex, main, 0x6e69616du, 0, output});
|
|
words[entryPointOffset] += static_cast<std::uint32_t>(extraInterface.size()) << 16u;
|
|
words.insert(words.end(), extraInterface.begin(), extraInterface.end());
|
|
if (shape.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 pipelineLibrarySetTests() {
|
|
using namespace AgcDriver::Graphics;
|
|
for (const auto mask : {0u, 1u, 2u, 3u}) {
|
|
mock = MockVulkan{};
|
|
auto context = mockContext();
|
|
context.graphicsPipelineLibrary = true;
|
|
context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT;
|
|
State state{};
|
|
ShaderRecompiler::RecompileResult vertex, fragment;
|
|
vertex.spirv = makeModule({});
|
|
fragment.spirv = makeModule({.fragment = true});
|
|
vertex.variantId = 1;
|
|
fragment.variantId = 2;
|
|
for (std::uint32_t set = 0; set < 2; ++set) {
|
|
ShaderRecompiler::DescriptorBinding binding{};
|
|
binding.descriptorSet = set;
|
|
if ((mask & (1u << set)) != 0u) fragment.bindings.push_back(binding);
|
|
else vertex.bindings.push_back(binding);
|
|
}
|
|
const std::array<CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}};
|
|
std::array<std::uint32_t, 1> mainKey{11}, fragmentKey{22};
|
|
PipelineSetLayouts resources{makeHandle<VkDescriptorSetLayout>(), makeHandle<VkDescriptorSetLayout>(), true, mainKey, fragmentKey};
|
|
const auto prebuild = [&] { Require(Pipeline::Prebuild(context, state, VertexInputLayout{}, resources, shaders), "graphics pipeline library prebuild was skipped"); };
|
|
prebuild();
|
|
for (const auto part : {VK_GRAPHICS_PIPELINE_LIBRARY_PRE_RASTERIZATION_SHADERS_BIT_EXT, VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT}) {
|
|
const auto layout = mock.libraryLayouts.at(part);
|
|
const auto expectedMask = part == VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT ? mask : mask ^ 3u;
|
|
const std::vector<VkDescriptorSetLayout> expected{(expectedMask & 1u) != 0u ? resources.main : VK_NULL_HANDLE, (expectedMask & 2u) != 0u ? resources.fragment : VK_NULL_HANDLE};
|
|
Require(mock.pipelineLayoutSets.at(layout) == expected, "shader library lost a used set or retained an unused set");
|
|
Require(mock.pipelineLayoutFlags.at(layout) == VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT, "shader library lost independent sets");
|
|
const auto& pushes = mock.pipelineLayoutPushRanges.at(layout);
|
|
Require(pushes.size() == 2 && pushes[0].offset == 0 && pushes[0].size == PipelinePushSlotBytes && pushes[0].stageFlags == VK_SHADER_STAGE_VERTEX_BIT && pushes[1].offset == PipelinePushSlotBytes && pushes[1].size == PipelinePushSlotBytes && pushes[1].stageFlags == VK_SHADER_STAGE_FRAGMENT_BIT, "shader library changed push constant slots");
|
|
}
|
|
auto before = PipelineLibraryCountersOf(context.device).built;
|
|
prebuild();
|
|
Require(PipelineLibraryCountersOf(context.device).built == before, "identical shader libraries were rebuilt");
|
|
for (std::uint32_t set = 0; set < 2; ++set) {
|
|
if (set == 0) { ++mainKey[0]; resources.main = makeHandle<VkDescriptorSetLayout>(); }
|
|
else { ++fragmentKey[0]; resources.fragment = makeHandle<VkDescriptorSetLayout>(); }
|
|
prebuild();
|
|
const auto after = PipelineLibraryCountersOf(context.device).built;
|
|
Require(after[0] == before[0] && after[3] == before[3], "descriptor layout change rebuilt interface libraries");
|
|
Require(after[1] == before[1] + ((mask & (1u << set)) == 0u ? 1u : 0u), "pre-rasterization library reuse ignored descriptor set usage");
|
|
Require(after[2] == before[2] + ((mask & (1u << set)) != 0u ? 1u : 0u), "fragment library reuse ignored descriptor set usage");
|
|
before = after;
|
|
}
|
|
ShaderRecompiler::DescriptorBinding invalid{};
|
|
invalid.descriptorSet = 32;
|
|
fragment.bindings.push_back(invalid);
|
|
expectFailure(prebuild, "descriptor set exceeds two slots");
|
|
ClearPipelineLibraries(context.device);
|
|
Require(mock.live == 0, "shader library tests leaked Vulkan objects");
|
|
}
|
|
}
|
|
|
|
void barycentricEmulationTests() {
|
|
using namespace ShaderRecompiler;
|
|
using namespace AgcDriver::Graphics;
|
|
const std::array<FragmentParameter, 3> 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<std::pair<std::uint32_t, std::uint32_t>>{{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<FragmentParameter> crowded;
|
|
for (std::uint32_t location = 0; location < 11u; ++location) crowded.push_back({location, location, false, true});
|
|
expectFailure([&] { static_cast<void>(LayoutBarycentricEmulation(crowded, {true, false, false})); }, "exceed 32 locations");
|
|
|
|
const auto compile = [&](std::span<const std::uint32_t> 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<std::uint32_t> inputs;
|
|
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
|
if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpVariable && words[at + 3] == spv::StorageClassInput) inputs.insert(words[at + 2]);
|
|
}
|
|
std::vector<std::uint32_t> values;
|
|
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
|
if (static_cast<spv::Op>(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<spv::Op>(words[at] & 0xffffu) == spv::OpCapability && words[at + 1] == spv::CapabilityFragmentBarycentricKHR) return true;
|
|
}
|
|
return false;
|
|
};
|
|
static constexpr std::array<std::uint32_t, 3> readsIj{0xf800180fu, 0x01000100u, 0xbf810000u};
|
|
static constexpr std::array<std::uint32_t, 5> interpolates{0xc8080000u, 0xc8090001u, 0xf800180fu, 0x02020202u, 0xbf810000u};
|
|
static constexpr std::array<std::uint32_t, 4> readsVertex{0xc80a0000u, 0xf800180fu, 0x02020202u, 0xbf810000u};
|
|
static constexpr std::array<std::uint32_t, 6> 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<std::uint32_t>(spv::BuiltInBaryCoordKHR)) == builtins.end() && !barycentricCapability(fragment), "emulated barycentrics still need VK_KHR_fragment_shader_barycentric");
|
|
Require(decorations(fragment, spv::DecorationLocation) == std::vector<std::uint32_t>{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<std::uint32_t, 6> overwritesI{0xc8080000u, 0xc8090001u, 0x7e0002f2u, 0xf800180fu, 0x02020002u, 0xbf810000u};
|
|
static constexpr std::array<std::uint32_t, 5> accumulatesInI{0xc8000000u, 0xc8010001u, 0xf800180fu, 0x00000000u, 0xbf810000u};
|
|
for (const auto& code : {std::span<const std::uint32_t>(overwritesI), std::span<const std::uint32_t>(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<std::uint32_t>(spv::BuiltInBaryCoordKHR)) == reusedBuiltins.end(), "reusing the I/J VGPRs after v_interp_p1/p2 lost host interpolation");
|
|
}
|
|
static constexpr std::array<std::uint32_t, 7> 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<std::uint32_t, 6> 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<std::uint32_t>{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<std::uint32_t, 2> 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<CompiledShader, 3> 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<CompiledShader, 3> 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<void>(BuildBarycentricGeometryShader(vertex, plain, target, limits)); }, "does not emulate barycentrics");
|
|
expectFailure([&] { static_cast<void>(BuildBarycentricGeometryShader(vertex, fragment, target, std::nullopt)); }, "geometry shaders are unavailable");
|
|
expectFailure([&] { static_cast<void>(BuildBarycentricGeometryShader(vertex, fragment, target, GeometryStageLimits{64u, 12u, 256u, 1024u, 128u})); }, "exceeds device limits");
|
|
vertex.parameterExports.clear();
|
|
expectFailure([&] { static_cast<void>(BuildBarycentricGeometryShader(vertex, fragment, target, limits)); }, "not exported by the vertex shader");
|
|
}
|
|
|
|
void copiedWriterIndirectOverlapTests() {
|
|
using namespace ShaderRecompiler;
|
|
using namespace AgcDriver::Graphics;
|
|
|
|
constexpr std::size_t half = 65536;
|
|
|
|
void* directBlock = ::operator new(half, std::align_val_t{half});
|
|
std::memset(directBlock, 0xaa, half);
|
|
const auto directAddress = reinterpret_cast<std::uintptr_t>(directBlock);
|
|
|
|
void* copiedBlock = ::operator new(half, std::align_val_t{half});
|
|
std::memset(copiedBlock, 0xbb, half);
|
|
const auto copiedAddress = reinterpret_cast<std::uintptr_t>(copiedBlock);
|
|
|
|
{
|
|
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
|
GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, directBlock, half, true, true, true);
|
|
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
|
}
|
|
|
|
mock = MockVulkan{};
|
|
auto context = mockContext();
|
|
context.hostImportAlignment = half;
|
|
context.limits.minStorageBufferOffsetAlignment = 16;
|
|
context.limits.maxStorageBufferRange = 1u << 27;
|
|
|
|
{
|
|
GuestBufferMemory memory(context);
|
|
memory.AcquireRegistered();
|
|
Require(HostImportCovers(context, directAddress, half), "direct registered range must be covered by host import");
|
|
Require(!HostImportCovers(context, copiedAddress, half), "copied unregistered range must NOT be covered by host import");
|
|
|
|
memory.AddWritable(copiedAddress, 1024);
|
|
memory.AddWritable(directAddress, 1024);
|
|
memory.Upload(false);
|
|
|
|
Require(memory.HasCopiedWrites(), "writer with one copied region must report HasCopiedWrites() == true");
|
|
|
|
Require(memory.WritesOverlap(directAddress, 16), "WritesOverlap must return true for direct written region");
|
|
Require(memory.WritesOverlap(copiedAddress, 16), "WritesOverlap must return true for copied written region");
|
|
Require(!memory.WritesOverlap(directAddress + 2048, 16), "WritesOverlap must return false for unwritten range");
|
|
|
|
Require(!memory.CopiedWritesOverlap(directAddress, 16), "CopiedWritesOverlap must return false for direct written region");
|
|
Require(memory.CopiedWritesOverlap(copiedAddress, 16), "CopiedWritesOverlap must return true for copied written region");
|
|
Require(!memory.CopiedWritesOverlap(directAddress + 2048, 16), "CopiedWritesOverlap must return false for unwritten range");
|
|
|
|
memory.WriteBack();
|
|
}
|
|
|
|
{
|
|
RecompileResult shader;
|
|
shader.runtimeAbiVersion = RuntimeAbi::Version;
|
|
auto copiedBinding = makeBinding(Role::GuestBuffers, 0, 1, vsharp(copiedBlock, 1024));
|
|
copiedBinding.bufferWritten = {true};
|
|
auto directBinding = makeBinding(Role::GuestBuffers, 1, 1, vsharp(directBlock, 1024));
|
|
directBinding.bufferWritten = {true};
|
|
shader.bindings = {copiedBinding, directBinding};
|
|
CompiledShader compiled{ShaderStage::Compute, &shader, 0};
|
|
auto writer = std::make_shared<ShaderResources>(context, compiled);
|
|
|
|
Require(writer->HasCopiedWrites(), "ShaderResources with copied region must report HasCopiedWrites() == true");
|
|
Require(writer->WritesOverlap(directAddress, 16), "ShaderResources must report WritesOverlap for direct region");
|
|
Require(!writer->CopiedWritesOverlap(directAddress, 16), "ShaderResources must NOT report CopiedWritesOverlap for direct region");
|
|
Require(writer->CopiedWritesOverlap(copiedAddress, 16), "ShaderResources must report CopiedWritesOverlap for copied region");
|
|
|
|
auto copiedWriters = DrawCopiedWriters();
|
|
copiedWriters->push_back(writer);
|
|
|
|
const auto overlapsMain = [&](const auto& w) { return w->WritesOverlap(directAddress, 16); };
|
|
const auto overlapsPR = [&](const auto& w) { return w->CopiedWritesOverlap(directAddress, 16); };
|
|
|
|
const bool mainOverlaps = std::any_of(copiedWriters->begin(), copiedWriters->end(), overlapsMain);
|
|
const bool prOverlaps = std::any_of(copiedWriters->begin(), copiedWriters->end(), overlapsPR);
|
|
|
|
Require(mainOverlaps, "main's check (WritesOverlap) must flag an overlap over direct arguments");
|
|
Require(!prOverlaps, "PR's check (CopiedWritesOverlap) must disambiguate and NOT flag an overlap over direct arguments");
|
|
|
|
const auto copiedOverlapsPR = [&](const auto& w) { return w->CopiedWritesOverlap(copiedAddress, 16); };
|
|
Require(std::any_of(copiedWriters->begin(), copiedWriters->end(), copiedOverlapsPR), "PR's check must flag an overlap over copied arguments");
|
|
|
|
copiedWriters->erase(std::remove(copiedWriters->begin(), copiedWriters->end(), writer), copiedWriters->end());
|
|
writer->WriteBack();
|
|
}
|
|
|
|
{
|
|
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
|
GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, directBlock);
|
|
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
|
}
|
|
::operator delete(directBlock, std::align_val_t{half});
|
|
::operator delete(copiedBlock, std::align_val_t{half});
|
|
}
|
|
|
|
void registeredReadableCoverTests() {
|
|
using namespace AgcDriver::Graphics;
|
|
|
|
constexpr std::size_t block = 65536;
|
|
auto* memory = static_cast<std::byte*>(::operator new(4 * block, std::align_val_t{block}));
|
|
const auto base = reinterpret_cast<std::uint64_t>(memory);
|
|
const auto add = [&](std::size_t index, bool readable) {
|
|
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
|
GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, memory + index * block, block, readable, readable, true);
|
|
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
|
};
|
|
const auto remove = [&](std::size_t index) {
|
|
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
|
GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, memory + index * block);
|
|
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
|
};
|
|
|
|
add(0, true);
|
|
add(1, true);
|
|
Require(RegisteredReadableCovers(base, block), "a registered allocation must be covered");
|
|
Require(RegisteredReadableCovers(base + block / 2, block), "a range across two adjacent registered allocations must be covered");
|
|
Require(RegisteredReadableCovers(base, 2 * block), "two adjacent registered allocations must be covered together");
|
|
Require(!RegisteredReadableCovers(base + block, 2 * block), "a range past the last registered allocation must not be covered");
|
|
add(3, true);
|
|
Require(!RegisteredReadableCovers(base, 4 * block), "a range over a gap between registered allocations must not be covered");
|
|
add(2, false);
|
|
Require(!RegisteredReadableCovers(base + block, 2 * block), "a range over an unreadable registered allocation must not be covered");
|
|
Require(RegisteredReadableCovers(base + 3 * block, block), "the readable allocation after an unreadable one must be covered");
|
|
Require(!RegisteredReadableCovers(base, 0), "an empty range must not be covered");
|
|
for (std::size_t index = 0; index < 4; ++index) remove(index);
|
|
Require(!RegisteredReadableCovers(base, block), "a removed allocation must not be covered");
|
|
::operator delete(memory, std::align_val_t{block});
|
|
}
|
|
|
|
void rectListTests() {
|
|
using namespace ShaderRecompiler;
|
|
using namespace AgcDriver::Graphics;
|
|
RecompileResult vertex;
|
|
vertex.spirv = makeModule({});
|
|
RecompileResult fragment;
|
|
fragment.spirv = makeModule({.fragment = true});
|
|
const std::array<std::uint32_t, 2> 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<CompiledShader, 4> 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<CompiledShader, 4> 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<void>(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<void>(BuildRectListShaders(vertex, fragment, target)); }, "device limits");
|
|
target.tessellation.reset();
|
|
expectFailure([&] { static_cast<void>(BuildRectListShaders(vertex, fragment, target)); }, "unavailable");
|
|
auto queue = makeState();
|
|
queue.userConfig[0x242] = 17;
|
|
const auto state = DecodeState(queue);
|
|
const Context context{};
|
|
std::array<std::uint16_t, 8> indices{};
|
|
const auto drawFailure = [&](std::uint32_t count, bool indexed) {
|
|
const AgcDriver::Pm4::DrawParameters draw = indexed ? AgcDriver::Pm4::DrawParameters{reinterpret_cast<std::uint64_t>(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");
|
|
}
|
|
Require(drawFailure(0, true).empty(), "an indexed draw without indices was not skipped");
|
|
const AgcDriver::Pm4::DrawParameters noInstances{reinterpret_cast<std::uint64_t>(indices.data()), 3, 2, 0, 0, true};
|
|
Draw(context, state, noInstances, {});
|
|
}
|
|
|
|
ShaderRecompiler::RecompileResult recompilePixel(std::initializer_list<std::uint32_t> controls, std::span<const std::uint32_t> code) {
|
|
auto queue = makeState();
|
|
queue.context[0x1b3] = 0x2u;
|
|
queue.context[0x1b4] = 0x2u;
|
|
queue.context[0x1b6] = static_cast<std::uint32_t>(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<LocatedInput> locatedInputs(std::span<const std::uint32_t> words) {
|
|
std::map<std::uint32_t, LocatedInput> decorated;
|
|
std::set<std::uint32_t> inputs;
|
|
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
|
const auto op = static_cast<spv::Op>(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<LocatedInput> 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<std::uint32_t, 3> 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<std::uint32_t> depthModes(const ShaderRecompiler::RecompileRequest& request) {
|
|
const auto result = ShaderRecompiler::Recompile(request);
|
|
const auto& words = result.spirv.Words();
|
|
std::set<std::uint32_t> modes;
|
|
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
|
if (static_cast<spv::Op>(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<std::pair<std::uint32_t, std::set<std::uint32_t>>, 4> cases{{
|
|
{0x0021u, {spv::ExecutionModeDepthReplacing}},
|
|
{0x2021u, {spv::ExecutionModeDepthReplacing, spv::ExecutionModeDepthLess}},
|
|
{0x4021u, {spv::ExecutionModeDepthReplacing, spv::ExecutionModeDepthGreater}},
|
|
{0x2020u, {}}
|
|
}};
|
|
const ShaderRecompiler::RequestSerializer serializer;
|
|
std::set<std::vector<std::uint64_t>> 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<std::uint64_t> 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<void>(AgcDriver::Graphics::DecodeState(queue));
|
|
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DB_SHADER_CONTROL " + std::to_string(shaderControl));
|
|
}
|
|
expectFailure([] { static_cast<void>(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<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}};
|
|
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
|
for (const auto 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");
|
|
}
|
|
|
|
bool declaresEarlyFragmentTests(const ShaderRecompiler::RecompileRequest& request) {
|
|
const auto result = ShaderRecompiler::Recompile(request);
|
|
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<spv::Op>(words[at] & 0xffffu) == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeEarlyFragmentTests) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void earlyFragmentTestsTests() {
|
|
const std::array<std::pair<std::uint32_t, bool>, 9> cases{{
|
|
{0x0000u, false},
|
|
{0x0020u, false},
|
|
{0x0010u, true},
|
|
{0x0030u, true},
|
|
{0x0050u, false},
|
|
{0x0011u, false},
|
|
{0x1000u, true},
|
|
{0x1050u, true},
|
|
{0x1031u, true}
|
|
}};
|
|
const ShaderRecompiler::RequestSerializer serializer;
|
|
for (const auto& [shaderControl, expected] : cases) {
|
|
const auto request = depthExportRequest(shaderControl);
|
|
const auto replayed = serializer.Deserialize(serializer.Serialize(request));
|
|
Require(declaresEarlyFragmentTests(request) == expected && declaresEarlyFragmentTests(replayed.request) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + (expected ? " did not declare" : " declared") + " early fragment tests");
|
|
}
|
|
for (const auto shaderControl : {0x0010u, 0x1000u}) {
|
|
auto request = depthExportRequest(shaderControl);
|
|
request.context.pixel->alphaToCoverage = true;
|
|
const auto replayed = serializer.Deserialize(serializer.Serialize(request));
|
|
Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(replayed.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with alpha-to-coverage declared early fragment tests");
|
|
request = depthExportRequest(shaderControl);
|
|
request.context.pixel->quadPixelMask = 0x1u;
|
|
const auto masked = serializer.Deserialize(serializer.Serialize(request));
|
|
Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(masked.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with a quad pixel mask declared early fragment tests");
|
|
Require(AgcDriver::Graphics::DeclaresEarlyFragmentTests(ShaderRecompiler::Recompile(depthExportRequest(shaderControl))), "the driver did not read early fragment tests from a recompiled pixel shader");
|
|
}
|
|
}
|
|
|
|
constexpr std::array<std::uint32_t, 5> OrderedPixelCode{0x7e0e02f2u, 0xbf900007u, 0xf800180fu, 0x07070707u, 0xbf810000u};
|
|
constexpr std::array<std::uint32_t, 12> 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<const std::uint32_t> capabilities, std::span<const std::uint32_t> 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<const std::uint32_t> 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<spv::Op>(words[at] & 0xffffu);
|
|
if (op == spv::OpCapability && words[at + 1] == spv::CapabilityFragmentShaderPixelInterlockEXT) shape += "capability ";
|
|
if (op == spv::OpExtension && std::string_view(reinterpret_cast<const char*>(&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<const std::uint32_t> words, std::size_t& kills, std::size_t& terminates) {
|
|
struct Block {
|
|
std::vector<spv::Op> ops;
|
|
std::vector<std::uint32_t> next;
|
|
};
|
|
std::map<std::uint32_t, Block> 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<spv::Op>(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 || op == spv::OpTerminateInvocation) return "an interlock instruction, OpKill or OpTerminateInvocation 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<std::uint32_t, int> state{{first, 0}};
|
|
std::vector<std::uint32_t> 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;
|
|
if (op == spv::OpTerminateInvocation) ++terminates;
|
|
}
|
|
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<std::uint32_t, 2> interlock{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT};
|
|
const std::array<std::uint32_t, 1> plain{spv::CapabilityShader};
|
|
for (const auto shaderControl : {0x30640u, 0x30600u}) {
|
|
const auto queue = orderedPixelQueue(shaderControl);
|
|
static_cast<void>(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<void>(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<std::vector<std::uint64_t>> keys;
|
|
for (const auto& [shaderControl, expected] : std::array<std::pair<std::uint32_t, std::string_view>, 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<std::uint64_t> 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<AgcDriver::Graphics::CompiledShader, 2> 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<std::uint32_t, 5> bdaInterlock{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
|
|
const std::array<std::string_view, 2> 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, terminates = 0;
|
|
const auto exits = interlockExits(ShaderRecompiler::Recompile(faulting).spirv.Words(), kills, terminates);
|
|
Require(exits.empty() && kills == 6u && terminates == 0u, "a primitive-ordered pixel shader with BDA reads: " + (exits.empty() ? std::to_string(kills) + " OpKill and " + std::to_string(terminates) + " OpTerminateInvocation, not the valid-mask kill and 5 BDA fault kills" : exits));
|
|
const std::array<std::string_view, 3> terminateExtensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage", "SPV_KHR_terminate_invocation"};
|
|
auto terminating = faulting;
|
|
terminating.target.supportedExtensions = terminateExtensions;
|
|
const auto terminated = ShaderRecompiler::Recompile(terminating).spirv.Copy();
|
|
std::size_t terminatedKills = 0, terminatedStops = 0;
|
|
const auto terminatedExits = interlockExits(terminated, terminatedKills, terminatedStops);
|
|
Require(terminatedExits.empty() && terminatedKills == 1u && terminatedStops == 5u, "a primitive-ordered pixel shader with BDA reads and SPV_KHR_terminate_invocation: " + (terminatedExits.empty() ? std::to_string(terminatedKills) + " OpKill and " + std::to_string(terminatedStops) + " OpTerminateInvocation, not the valid-mask kill and 5 BDA fault terminations" : terminatedExits));
|
|
std::size_t terminateDeclarations = 0;
|
|
for (std::size_t at = 5; at < terminated.size() && (terminated[at] >> 16u) != 0; at += terminated[at] >> 16u) {
|
|
if ((terminated[at] & 0xffffu) == spv::OpExtension && std::string_view(reinterpret_cast<const char*>(&terminated[at + 1])) == "SPV_KHR_terminate_invocation") ++terminateDeclarations;
|
|
}
|
|
Require(terminateDeclarations == 1u, "the module declared SPV_KHR_terminate_invocation " + std::to_string(terminateDeclarations) + " times");
|
|
const auto countOps = [](std::span<const std::uint32_t> words) {
|
|
std::map<spv::Op, std::size_t> counts;
|
|
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) ++counts[static_cast<spv::Op>(words[at] & 0xffffu)];
|
|
return counts;
|
|
};
|
|
const std::array<std::uint32_t, 4> bdaCapabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
|
|
auto unordered = orderedPixelRequest(0x20600u, bdaCapabilities, OrderedBdaPixelCode);
|
|
unordered.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version;
|
|
unordered.target.supportedExtensions = terminateExtensions;
|
|
const auto unorderedOps = countOps(ShaderRecompiler::Recompile(unordered).spirv.Words());
|
|
Require(!unorderedOps.contains(spv::OpBeginInvocationInterlockEXT), "a pixel shader without PRIMITIVE_ORDERED_PIXEL_SHADER began the interlock");
|
|
Require(unorderedOps.at(spv::OpTerminateInvocation) == 5u && unorderedOps.at(spv::OpKill) == 1u, "a pixel shader with BDA reads and SPV_KHR_terminate_invocation did not terminate on a fault");
|
|
unordered.target.supportedExtensions = bdaExtensions;
|
|
const auto unorderedKills = countOps(ShaderRecompiler::Recompile(unordered).spirv.Words());
|
|
Require(!unorderedKills.contains(spv::OpTerminateInvocation) && unorderedKills.at(spv::OpKill) == 6u, "a pixel shader with BDA reads and no SPV_KHR_terminate_invocation did not kill on a fault");
|
|
static constexpr std::array<std::uint32_t, 12> vertexBdaCode{0x7e040f00u, 0x7e060280u, 0xdc208001u, 0x047d0002u, 0xdc308008u, 0x057d0002u, 0xbf8c3f70u, 0x4a080b04u, 0x7e0e0d04u, 0xf80008cfu, 0x07070707u, 0xbf810000u};
|
|
const std::array<std::uint32_t, 5> vertexCapabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance};
|
|
const auto vertexWords = [&](std::span<const std::string_view> extensions) {
|
|
ShaderRecompiler::RecompileRequest request{};
|
|
request.shader = {ShaderRecompiler::ShaderStage::Vertex, 0x10000u, vertexBdaCode, 0, {}};
|
|
request.context.waveSize = 64;
|
|
request.context.vertex = ShaderRecompiler::ShaderVertexStageInfo{};
|
|
request.target.vulkanVersion = 0x00401000u;
|
|
request.target.spirvVersion = 0x00010300u;
|
|
request.target.subgroupSize = 64;
|
|
request.target.supportedCapabilities = vertexCapabilities;
|
|
request.target.supportedExtensions = extensions;
|
|
request.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version;
|
|
request.layout.pushConstantSizeBytes = 128;
|
|
request.useCache = false;
|
|
return ShaderRecompiler::Recompile(request).spirv.Copy();
|
|
};
|
|
const auto vertexStops = vertexWords(bdaExtensions);
|
|
const auto vertexTerminate = vertexWords(terminateExtensions);
|
|
const auto vertexOps = countOps(vertexTerminate);
|
|
Require(std::ranges::equal(vertexStops, vertexTerminate) && !vertexOps.contains(spv::OpTerminateInvocation) && !vertexOps.contains(spv::OpKill) && vertexOps.at(spv::OpReturn) > 1u, "a vertex shader with BDA reads changed its fault exit when SPV_KHR_terminate_invocation was advertised");
|
|
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(orderedPixelRequest(0x30600u, plain))); }, "fragmentShaderPixelInterlock");
|
|
}
|
|
|
|
constexpr std::array<std::uint32_t, 4> StencilRefExportCode{0x7e0e02f2u, 0xf8001882u, 0x700u, 0xbf810000u};
|
|
constexpr std::array<std::uint32_t, 4> 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<const std::uint32_t> capabilities, std::span<const std::uint32_t> 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<const std::uint32_t> 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<spv::Op>(words[at] & 0xffffu);
|
|
if (op == spv::OpCapability && words[at + 1] == spv::CapabilityStencilExportEXT) shape += "capability ";
|
|
if (op == spv::OpExtension && std::string_view(reinterpret_cast<const char*>(&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<void>(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<void>(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<void>(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");
|
|
Require(AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x1012u, 2u).context, IdentityExports).earlyZ, "DEPTH_BEFORE_SHADER with a stencil reference export did not get early tests");
|
|
const std::array<std::uint32_t, 2> stencilExport{spv::CapabilityShader, spv::CapabilityStencilExportEXT};
|
|
const std::array<std::uint32_t, 1> plain{spv::CapabilityShader};
|
|
const ShaderRecompiler::RequestSerializer serializer;
|
|
std::set<std::vector<std::uint64_t>> keys;
|
|
const std::array<std::pair<std::uint32_t, std::string_view>, 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<std::uint64_t> 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<AgcDriver::Graphics::CompiledShader, 2> 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);
|
|
const auto emulated = ShaderRecompiler::Recompile(stencilRefExportRequest(0x52u, plain));
|
|
Require(emulated.stencilReferences == std::vector<std::uint32_t>{0u} && emulated.stencilReplaySafe && stencilRefShape(emulated.spirv.Words()).empty(), "a constant stencil reference was not lowered to replay");
|
|
Require(emulated.stencilReferenceDword == 0u && emulated.pushConstants.size() == sizeof(std::uint32_t), "a stencil-only shader did not reserve dynamic reference data");
|
|
auto replayRequest = stencilRefExportRequest(0x52u, plain);
|
|
const auto replayHandle = ShaderRecompiler::PrepareShader(replayRequest);
|
|
const auto replayCapture = ShaderRecompiler::CaptureResources(replayRequest, ShaderRecompiler::SrtRuntime{}, *replayHandle);
|
|
const auto firstReplay = ShaderRecompiler::MaterializeShader(replayRequest, *replayCapture, *replayHandle);
|
|
replayRequest.layout.pushConstantOffsetBytes = 128u;
|
|
Require(ShaderRecompiler::MatchesPreparedShader(replayRequest, *replayHandle), "moving the stencil replay stage slot invalidated the prepared artifact");
|
|
const auto movedReplay = ShaderRecompiler::MaterializeShader(replayRequest, *replayCapture, *replayHandle);
|
|
Require(firstReplay->spirv != movedReplay->spirv && firstReplay->variantId == movedReplay->variantId && firstReplay->pushConstants == movedReplay->pushConstants, "prepared stencil replay did not move only the dynamic reference offset");
|
|
replayRequest.layout.pushConstantSizeBytes = 0u;
|
|
Require(!ShaderRecompiler::MatchesPreparedShader(replayRequest, *replayHandle), "prepared stencil replay accepted an exhausted push range");
|
|
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(replayRequest)); }, "unused push constant dword");
|
|
const std::array<std::uint32_t, 5> pushedDepthStencil{0x7e000200u, 0x7e0e02f2u, 0xf8001883u, 0x700u, 0xbf810000u};
|
|
const std::array<std::uint32_t, 1> pushedDepth{0x3f000000u};
|
|
auto pushedRequest = stencilRefExportRequest(0x53u, plain, pushedDepthStencil);
|
|
pushedRequest.context.userData = pushedDepth;
|
|
pushedRequest.layout = {0u, 0u, 96u, 32u};
|
|
const auto pushedHandle = ShaderRecompiler::PrepareShader(pushedRequest);
|
|
ShaderRecompiler::SrtRuntime pushedRuntime{};
|
|
pushedRuntime.userData = pushedDepth;
|
|
const auto pushedCapture = ShaderRecompiler::CaptureResources(pushedRequest, pushedRuntime, *pushedHandle);
|
|
const auto pushedResult = ShaderRecompiler::MaterializeShader(pushedRequest, *pushedCapture, *pushedHandle);
|
|
Require(pushedResult->stencilReferenceDword == 1u && pushedResult->pushConstants.size() == 8u, "a nonaligned replay stage did not reserve its reference after existing push data");
|
|
for (const auto [offset, bytes] : {std::pair{128u, 128u}, std::pair{120u, 8u}}) {
|
|
pushedRequest.layout.pushConstantOffsetBytes = offset;
|
|
pushedRequest.layout.pushConstantSizeBytes = bytes;
|
|
Require(ShaderRecompiler::MatchesPreparedShader(pushedRequest, *pushedHandle), "a replay payload could not move within a valid stage range");
|
|
const auto moved = ShaderRecompiler::MaterializeShader(pushedRequest, *pushedCapture, *pushedHandle);
|
|
Require(moved->pushConstants == pushedResult->pushConstants && moved->spirv != pushedResult->spirv, "moving replay push data changed its payload or retained its old offset");
|
|
}
|
|
pushedRequest.layout = {0u, 0u, 124u, 4u};
|
|
Require(!ShaderRecompiler::MatchesPreparedShader(pushedRequest, *pushedHandle), "a full push budget reused a replay artifact");
|
|
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(pushedRequest)); }, "unused push constant dword");
|
|
pushedRequest.layout.pushConstantSizeBytes = 8u;
|
|
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(pushedRequest)); }, "crosses a stage slot");
|
|
const std::array<std::uint32_t, 4> noStencilExport{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u};
|
|
auto noStencilRequest = stencilRefExportRequest(0x52u, plain, noStencilExport);
|
|
noStencilRequest.layout.pushConstantSizeBytes = 0u;
|
|
const auto noStencil = ShaderRecompiler::Recompile(noStencilRequest);
|
|
Require(noStencil.stencilReferences.empty() && noStencil.stencilReferenceDword == 0xffffffffu && noStencil.pushConstants.empty(), "a stencil export flag without a remaining stencil output reserved replay state");
|
|
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<void>(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<void>(AgcDriver::Graphics::DecodeState(queue)); }, "stencil clear with a pixel shader that exports the stencil test value");
|
|
queue.context[0x203] = 0x50u;
|
|
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
|
|
}
|
|
|
|
bool stencilComparison(VkCompareOp op, std::uint32_t reference, std::uint32_t stencil) {
|
|
switch (op) {
|
|
case VK_COMPARE_OP_NEVER: return false;
|
|
case VK_COMPARE_OP_LESS: return reference < stencil;
|
|
case VK_COMPARE_OP_EQUAL: return reference == stencil;
|
|
case VK_COMPARE_OP_LESS_OR_EQUAL: return reference <= stencil;
|
|
case VK_COMPARE_OP_GREATER: return reference > stencil;
|
|
case VK_COMPARE_OP_NOT_EQUAL: return reference != stencil;
|
|
case VK_COMPARE_OP_GREATER_OR_EQUAL: return reference >= stencil;
|
|
case VK_COMPARE_OP_ALWAYS: return true;
|
|
default: throw std::runtime_error("invalid stencil comparison in the reference oracle");
|
|
}
|
|
}
|
|
|
|
std::pair<std::uint32_t, bool> stencilReferenceOutcome(const AgcDriver::Graphics::State& state, const VkStencilOpState& face, std::uint32_t reference, std::uint32_t stencil, bool depthPass, bool boundsPass) {
|
|
if (state.depthBoundsTest && !boundsPass) return {stencil, false};
|
|
const bool stencilPass = stencilComparison(face.compareOp, reference & face.compareMask & 0xffu, stencil & face.compareMask & 0xffu);
|
|
if (!state.depthTest || state.depthCompare == VK_COMPARE_OP_ALWAYS) depthPass = true;
|
|
if (state.depthTest && state.depthCompare == VK_COMPARE_OP_NEVER) depthPass = false;
|
|
const auto op = !stencilPass ? face.failOp : !depthPass ? face.depthFailOp : face.passOp;
|
|
auto updated = stencil;
|
|
switch (op) {
|
|
case VK_STENCIL_OP_KEEP: break;
|
|
case VK_STENCIL_OP_ZERO: updated = 0; break;
|
|
case VK_STENCIL_OP_REPLACE: updated = reference; break;
|
|
case VK_STENCIL_OP_INCREMENT_AND_CLAMP: updated = std::min(stencil + 1u, 255u); break;
|
|
case VK_STENCIL_OP_DECREMENT_AND_CLAMP: updated = stencil == 0 ? 0 : stencil - 1u; break;
|
|
case VK_STENCIL_OP_INVERT: updated = ~stencil; break;
|
|
case VK_STENCIL_OP_INCREMENT_AND_WRAP: updated = stencil + 1u; break;
|
|
case VK_STENCIL_OP_DECREMENT_AND_WRAP: updated = stencil - 1u; break;
|
|
default: throw std::runtime_error("invalid stencil operation in the reference oracle");
|
|
}
|
|
const auto writeMask = face.writeMask & 0xffu;
|
|
return {((stencil & ~writeMask) | (updated & writeMask)) & 0xffu, stencilPass && depthPass};
|
|
}
|
|
|
|
void stencilReferenceUnusedTests() {
|
|
using AgcDriver::Graphics::StencilReferenceUnused;
|
|
AgcDriver::Graphics::State state{};
|
|
state.depth.emplace();
|
|
state.depth->format = VK_FORMAT_D32_SFLOAT_S8_UINT;
|
|
state.stencilTest = true;
|
|
state.stencilFront = {VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 37u};
|
|
state.stencilBack = state.stencilFront;
|
|
std::size_t accepted = 0;
|
|
std::size_t countermodels = 0;
|
|
const auto verify = [&](const auto& candidate, bool unused, bool witness, std::string_view name) {
|
|
Require(StencilReferenceUnused(candidate) == unused, "wrong unused stencil reference decision for " + std::string(name));
|
|
bool changed = false;
|
|
for (const auto& face : {candidate.stencilFront, candidate.stencilBack}) {
|
|
for (std::uint32_t stencil = 0; stencil < 256u; ++stencil) {
|
|
for (const bool depthPass : {false, true}) {
|
|
for (const bool boundsPass : {false, true}) {
|
|
const auto fixed = stencilReferenceOutcome(candidate, face, face.reference, stencil, depthPass, boundsPass);
|
|
for (std::uint32_t reference = 0; reference < 256u; ++reference) {
|
|
changed |= stencilReferenceOutcome(candidate, face, reference, stencil, depthPass, boundsPass) != fixed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Require(!unused || !changed, "dropping the stencil reference changes a sample outcome for " + std::string(name));
|
|
Require(!witness || changed, "the dependent stencil reference has no countermodel for " + std::string(name));
|
|
accepted += unused;
|
|
countermodels += witness;
|
|
};
|
|
verify(state, true, false, "ALWAYS with KEEP");
|
|
for (const auto op : {VK_STENCIL_OP_ZERO, VK_STENCIL_OP_INCREMENT_AND_CLAMP, VK_STENCIL_OP_DECREMENT_AND_CLAMP, VK_STENCIL_OP_INVERT, VK_STENCIL_OP_INCREMENT_AND_WRAP, VK_STENCIL_OP_DECREMENT_AND_WRAP}) {
|
|
auto independent = state;
|
|
independent.stencilFront.passOp = op;
|
|
independent.stencilBack = independent.stencilFront;
|
|
verify(independent, true, false, "reference-independent update");
|
|
}
|
|
for (std::uint32_t comparison = 0; comparison < 8u; ++comparison) {
|
|
auto masked = state;
|
|
masked.stencilFront.compareOp = static_cast<VkCompareOp>(comparison);
|
|
masked.stencilFront.compareMask = 0x100u;
|
|
masked.stencilBack = masked.stencilFront;
|
|
verify(masked, true, false, "effective zero compare mask");
|
|
if (comparison != VK_COMPARE_OP_ALWAYS && comparison != VK_COMPARE_OP_NEVER) {
|
|
auto dependent = masked;
|
|
dependent.stencilBack.compareMask = 0x80u;
|
|
dependent.stencilBack.writeMask = 0;
|
|
verify(dependent, false, true, "dependent back-face comparison with no writes");
|
|
}
|
|
}
|
|
auto replace = state;
|
|
replace.stencilFront.passOp = VK_STENCIL_OP_REPLACE;
|
|
verify(replace, false, true, "reachable front pass REPLACE");
|
|
replace.stencilFront.passOp = VK_STENCIL_OP_KEEP;
|
|
replace.stencilBack.failOp = VK_STENCIL_OP_REPLACE;
|
|
verify(replace, true, false, "unreachable back fail REPLACE");
|
|
replace.stencilBack.compareOp = VK_COMPARE_OP_NEVER;
|
|
verify(replace, false, true, "reachable back fail REPLACE");
|
|
replace.stencilBack.failOp = VK_STENCIL_OP_KEEP;
|
|
replace.stencilBack.passOp = VK_STENCIL_OP_REPLACE;
|
|
replace.stencilBack.depthFailOp = VK_STENCIL_OP_REPLACE;
|
|
verify(replace, true, false, "NEVER with unreachable depth operations");
|
|
replace = state;
|
|
replace.stencilBack.passOp = VK_STENCIL_OP_REPLACE;
|
|
replace.stencilBack.writeMask = 0x100u;
|
|
verify(replace, true, false, "effective zero write mask");
|
|
replace.stencilBack.writeMask = 0x80u;
|
|
verify(replace, false, true, "partially writable REPLACE");
|
|
replace = state;
|
|
replace.stencilBack.depthFailOp = VK_STENCIL_OP_REPLACE;
|
|
verify(replace, true, false, "disabled depth test");
|
|
replace.depthTest = true;
|
|
replace.depthCompare = VK_COMPARE_OP_ALWAYS;
|
|
verify(replace, true, false, "always passing depth test");
|
|
replace.depthCompare = VK_COMPARE_OP_LESS;
|
|
verify(replace, false, true, "possibly failing depth test");
|
|
replace.depthCompare = VK_COMPARE_OP_NEVER;
|
|
verify(replace, false, true, "always failing depth test");
|
|
replace.stencilBack.depthFailOp = VK_STENCIL_OP_KEEP;
|
|
replace.stencilBack.passOp = VK_STENCIL_OP_REPLACE;
|
|
verify(replace, true, false, "unreachable depth pass REPLACE");
|
|
replace = state;
|
|
replace.depthBoundsTest = true;
|
|
verify(replace, true, false, "bounds with reference-independent operations");
|
|
replace.stencilBack.depthFailOp = VK_STENCIL_OP_REPLACE;
|
|
Require(!StencilReferenceUnused(replace), "depth bounds admitted an unchecked REPLACE operation");
|
|
auto clear = state;
|
|
clear.stencilFront = {VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_REPLACE, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 7u};
|
|
clear.stencilBack = clear.stencilFront;
|
|
verify(clear, false, true, "clear replacement state");
|
|
for (const bool front : {false, true}) {
|
|
auto invalid = state;
|
|
(front ? invalid.stencilFront : invalid.stencilBack).compareOp = static_cast<VkCompareOp>(8u);
|
|
Require(!StencilReferenceUnused(invalid), "an unknown stencil comparison lost its export");
|
|
invalid = state;
|
|
(front ? invalid.stencilFront : invalid.stencilBack).passOp = static_cast<VkStencilOp>(8u);
|
|
Require(!StencilReferenceUnused(invalid), "an unknown stencil operation lost its export");
|
|
}
|
|
auto invalidDepth = state;
|
|
invalidDepth.depthTest = true;
|
|
invalidDepth.depthCompare = static_cast<VkCompareOp>(8u);
|
|
Require(!StencilReferenceUnused(invalidDepth), "an unknown depth comparison lost its stencil export");
|
|
auto inactive = clear;
|
|
inactive.stencilTest = false;
|
|
Require(StencilReferenceUnused(inactive), "a disabled stencil test retained the export");
|
|
inactive.stencilTest = true;
|
|
inactive.depth.reset();
|
|
Require(StencilReferenceUnused(inactive), "an absent attachment retained the export");
|
|
std::cout << "Unused stencil reference sample equivalence passed: " << accepted << " accepted states, " << countermodels << " dependent countermodels\n";
|
|
}
|
|
|
|
std::set<std::string> bufferViews(std::span<const std::uint32_t> words) {
|
|
std::set<std::string> views;
|
|
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
|
if (static_cast<spv::Op>(words[at] & 0xffffu) != spv::OpName) continue;
|
|
const std::string name(reinterpret_cast<const char*>(&words[at + 2]));
|
|
if (name.starts_with("buffers")) views.insert(name);
|
|
}
|
|
return views;
|
|
}
|
|
|
|
void aliasedBufferViewTests() {
|
|
constexpr std::array<std::uint32_t, 5> capabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityInt64Atomics};
|
|
constexpr std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
|
|
constexpr std::array<std::uint32_t, 12> userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu, 0x20000000u, 0u, 0x1000u, 0x31038facu};
|
|
const auto code = [](std::uint32_t store, std::uint32_t resource) {
|
|
return std::array<std::uint32_t, 9>{0xe0000000u, 0x80010a00u, 0xbf8c3f70u, store, resource, 0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u};
|
|
};
|
|
const std::array<std::pair<std::array<std::uint32_t, 9>, std::set<std::string>>, 2> cases{{
|
|
{code(0xe0104000u, 0x80010a00u), {"buffers", "buffers_coherent"}},
|
|
{code(0xe1480000u, 0x80010a00u), {"buffers", "buffers_u64"}},
|
|
}};
|
|
AgcDriver::Graphics::State state{};
|
|
state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex;
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({});
|
|
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
|
for (const auto& [words, views] : cases) {
|
|
auto request = orderedPixelRequest(0x20600u, capabilities, words);
|
|
request.target.supportedExtensions = extensions;
|
|
request.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version;
|
|
request.context.userDataBaseRegister = 0;
|
|
request.context.userData = userData;
|
|
auto pixel = ShaderRecompiler::Recompile(request);
|
|
const auto declared = bufferViews(pixel.spirv.Words());
|
|
std::string names;
|
|
for (const auto& name : declared) names += " " + name;
|
|
Require(declared == views, "the pixel shader declared the guest buffers as" + names + ", not as the expected " + std::to_string(views.size()) + " aliased variables");
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}};
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true);
|
|
auto unaliased = pixel.spirv.Copy();
|
|
for (std::size_t at = 5; at < unaliased.size() && (unaliased[at] >> 16u) != 0; at += unaliased[at] >> 16u) {
|
|
if (static_cast<spv::Op>(unaliased[at] & 0xffffu) == spv::OpDecorate && unaliased[at + 2] == spv::DecorationAliased) unaliased[at + 2] = spv::DecorationRestrict;
|
|
}
|
|
pixel.spirv = std::move(unaliased);
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true); }, "duplicate SPIR-V resource binding");
|
|
}
|
|
}
|
|
|
|
void LineStripTests() {
|
|
using namespace AgcDriver::Graphics;
|
|
auto queue = makeState();
|
|
queue.context[0x1b3] = 2;
|
|
queue.context[0x1b4] = 2;
|
|
queue.userConfig[0x242] = 3;
|
|
auto state = DecodeState(queue);
|
|
Require(state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && !state.rectList && !state.primitiveRestart, "primitive type 3 did not decode as a line strip");
|
|
Require(RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Line, "a line strip draw did not get the line primitive class");
|
|
queue.userConfig[0x24b] = 1;
|
|
queue.context[0x103] = 0xffffffffu;
|
|
state = DecodeState(queue);
|
|
Require(state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && state.primitiveRestart, "primitive restart was lost on a line strip");
|
|
const auto rejection = DrawRejection(queue, true);
|
|
Require(rejection.empty(), "the draw precheck rejected an indexed line strip with primitive restart: " + rejection);
|
|
}
|
|
|
|
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, 3u, 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<std::uint32_t, 7> 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<CompiledShader, 1> flatShaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatPixel, 0}}};
|
|
ValidateProvokingVertex(supported, last, flatShaders);
|
|
const std::array<std::uint32_t, 7> 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<CompiledShader, 1> 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");
|
|
auto lastStrip = last;
|
|
lastStrip.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
|
|
auto lastList = last;
|
|
lastList.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
|
const std::array<std::uint32_t, 11> smoothColor{0xc8100000u, 0xc8110001u, 0xc8140100u, 0xc8150101u, 0xc8180200u, 0xc8190201u, 0xc81c0300u, 0xc81d0301u, 0xf800180fu, 0x07060504u, 0xbf810000u};
|
|
const auto smoothPixel = recompilePixel({0x0u}, smoothColor);
|
|
Require(smoothPixel.fragmentParameters.size() == 1 && smoothPixel.fragmentParameters[0].perVertex && !smoothPixel.fragmentParameters[0].vertexOrder, "v_interp_p1/p2 of a smooth input was recorded as reading a vertex slot");
|
|
const std::array<CompiledShader, 1> smoothShaders{{{ShaderRecompiler::ShaderStage::Fragment, &smoothPixel, 0}}};
|
|
ValidateProvokingVertex(supported, lastStrip, smoothShaders);
|
|
const auto slotPixel = recompilePixel({0x0u}, flatP10Color);
|
|
Require(slotPixel.fragmentParameters.size() == 1 && slotPixel.fragmentParameters[0].perVertex && slotPixel.fragmentParameters[0].vertexOrder, "v_interp_mov P10 of a smooth input was not recorded as reading a vertex slot");
|
|
const std::array<CompiledShader, 1> slotShaders{{{ShaderRecompiler::ShaderStage::Fragment, &slotPixel, 0}}};
|
|
expectFailure([&] { ValidateProvokingVertex(supported, lastStrip, slotShaders); }, "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported");
|
|
ValidateProvokingVertex(supported, lastList, slotShaders);
|
|
ShaderRecompiler::RecompileResult fragment{};
|
|
const std::array<CompiledShader, 1> 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}) {
|
|
for (const auto vertexOrder : {false, true}) {
|
|
if (vertexOrder && !perVertex) continue;
|
|
fragment.fragmentParameters = {{0, 0, flat, perVertex, false, false, vertexOrder}};
|
|
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 (vertexOrder && 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<VkImageView, 1> views{reinterpret_cast<VkImageView>(std::uintptr_t{1})};
|
|
const std::array<VkProvokingVertexModeEXT, 5> 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<VkImageView, 1> differentViews{reinterpret_cast<VkImageView>(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<std::pair<std::uint32_t, std::string_view>, 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<std::pair<std::uint32_t, std::string_view>, 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<std::pair<std::uint32_t, std::string_view>, 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<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}};
|
|
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
|
for (const auto 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<std::uint32_t, 8> 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<CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}};
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, true);
|
|
const std::array<std::uint32_t, 7> 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<std::uint32_t, 7> vertices{0xc8120002u, 0xc8160000u, 0xc81a0001u, 0xc81e0302u, 0xf800180fu, 0x07060504u, 0xbf810000u};
|
|
const auto subtracts = [](std::span<const std::uint32_t> 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<CompiledShader, 2> 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<const std::uint32_t> 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<std::uint32_t, 3> 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<std::uint64_t> key;
|
|
std::vector<std::uint64_t> 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 == unpacked.spirv), "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<AgcDriver::Graphics::CompiledShader, 2> 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<std::uint32_t>{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<void>(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<std::uint32_t, std::uint32_t> locations;
|
|
std::map<std::uint32_t, std::uint32_t> indices;
|
|
};
|
|
|
|
OutputDecorations outputDecorations(std::span<const std::uint32_t> words) {
|
|
OutputDecorations result;
|
|
std::set<std::uint32_t> outputs;
|
|
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
|
const auto op = static_cast<spv::Op>(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<std::pair<std::uint32_t, VkBlendFactor>, 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<std::byte, 1024> 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<std::uint32_t>(reinterpret_cast<std::uintptr_t>(slotOneMemory.data()) >> 8u);
|
|
changed.context[0x390 + 1u] = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(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<std::uint32_t, 5> 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<std::uint64_t> key;
|
|
std::vector<std::uint64_t> 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<std::uint32_t> singleLocations;
|
|
for (const auto& entry : singleOutputs.locations) singleLocations.insert(entry.second);
|
|
Require(singleOutputs.indices.empty() && singleLocations == std::set<std::uint32_t>{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<AgcDriver::Graphics::CompiledShader, 2> 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<std::uint32_t>{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 ReversedBlendShaderTests() {
|
|
const auto queue = reversedBlendQueue(10u, 0u, 2u, 0x65010504u);
|
|
const auto state = AgcDriver::Graphics::DecodeState(queue);
|
|
Require(state.reversedBlend == 0x570001u && !state.dualSourceBlend && state.blends.size() == 1, "the reversed-order source-alpha blend did not decode to a pixel shader blend code");
|
|
constexpr std::array<std::uint32_t, 3> code{0xf800180fu, 0x07060504u, 0xbf810000u};
|
|
auto request = packedExportRequest(queue, state, code);
|
|
request.context.pixel->reversedBlend = state.reversedBlend;
|
|
auto plain = request;
|
|
plain.context.pixel->reversedBlend = 0u;
|
|
const ShaderRecompiler::RequestSerializer serializer;
|
|
Require(serializer.Deserialize(serializer.Serialize(request)).request.context.pixel->reversedBlend == 0x570001u, "the reversed-order blend code was lost in serialization");
|
|
std::vector<std::uint64_t> key;
|
|
std::vector<std::uint64_t> plainKey;
|
|
ShaderRecompiler::BuildPreparedShaderKey(request, key);
|
|
ShaderRecompiler::BuildPreparedShaderKey(plain, plainKey);
|
|
Require(key == plainKey, "the reversed-order blend code entered the static shader ABI");
|
|
const auto prepared = ShaderRecompiler::PrepareShader(plain);
|
|
Require(ShaderRecompiler::MatchesPreparedShader(request, *prepared), "a reversed-order blended draw did not match the registered pixel shader");
|
|
const auto reversed = ShaderRecompiler::Recompile(request);
|
|
const auto single = ShaderRecompiler::Recompile(plain);
|
|
Require(reversed.variantId == single.variantId && reversed.variantId == ShaderRecompiler::GetPreparedArtifact(*prepared).variantId, "the reversed-order blended draw did not reuse the registered artifact");
|
|
Require(reversed.PipelineVariantId() != single.PipelineVariantId() && ShaderRecompiler::Recompile(request).PipelineVariantId() == reversed.PipelineVariantId(), "the reversed-order blend code did not key its own specialization");
|
|
const auto arithmetic = [](std::span<const std::uint32_t> words) {
|
|
std::size_t count = 0;
|
|
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
|
const auto op = static_cast<spv::Op>(words[at] & 0xffffu);
|
|
if (op == spv::OpFAdd || op == spv::OpFSub || op == spv::OpFMul || op == spv::OpFDiv || op == spv::OpFNegate || op == spv::OpExtInst || op == spv::OpSelect) ++count;
|
|
}
|
|
return count;
|
|
};
|
|
for (const auto shaderCode : {0x570001u, 0x540001u, 0x560001u, 0x020001u, 0x560301u, 0xffff01u}) {
|
|
auto changed = request;
|
|
changed.context.pixel->reversedBlend = shaderCode;
|
|
const auto specialized = ShaderRecompiler::Recompile(changed);
|
|
const auto words = specialized.spirv.Words();
|
|
const auto outputs = outputDecorations(words);
|
|
Require(outputs.locations.size() == 2 && std::all_of(outputs.locations.begin(), outputs.locations.end(), [](const auto& entry) { return entry.second == 0u; }) && outputs.indices.size() == 1 && outputs.indices.begin()->second == 1u, "blend code " + std::to_string(shaderCode) + " did not add a second color at location 0 index 1");
|
|
Require(arithmetic(words) == arithmetic(single.spirv.Words()), "blend code " + std::to_string(shaderCode) + " computed in the pixel shader instead of copying the export");
|
|
}
|
|
const auto singleOutputs = outputDecorations(single.spirv.Words());
|
|
Require(singleOutputs.locations.size() == 1 && singleOutputs.locations.begin()->second == 0u && singleOutputs.indices.empty(), "a pixel shader without a reversed-order blend code changed its outputs");
|
|
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<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}};
|
|
return AgcDriver::Graphics::ValidateShaders(shaders, target, subgroup, false);
|
|
};
|
|
Require(validate(reversed, state) == std::set<std::uint32_t>{0u}, "the reversed-order blended shader did not validate against its draw");
|
|
expectFailure([&] { validate(single, state); }, "needs a float4 second color at location 0 index 1");
|
|
auto plainState = state;
|
|
plainState.reversedBlend = 0u;
|
|
expectFailure([&] { validate(reversed, plainState); }, "Index decoration");
|
|
for (const auto invalid : {0x2u, 0x80u, 0x100u, 0x10000u, 0x1000001u, 0x80000001u}) {
|
|
auto changed = request;
|
|
changed.context.pixel->reversedBlend = invalid;
|
|
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(changed)); }, "invalid prepared reversed-order blend");
|
|
}
|
|
const auto twoQueue = dualSourceQueue();
|
|
const auto twoState = AgcDriver::Graphics::DecodeState(twoQueue);
|
|
constexpr std::array<std::uint32_t, 5> twoExports{0xf800000fu, 0x07060504u, 0xf800181fu, 0x0b0a0908u, 0xbf810000u};
|
|
auto both = packedExportRequest(twoQueue, twoState, twoExports);
|
|
both.context.pixel->reversedBlend = 0x570001u;
|
|
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(both)); }, "reversed-order blending needs MRT0 to be the only color output");
|
|
both.context.pixel->dualSourceBlend = true;
|
|
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(both)); }, "reversed-order blending cannot be combined with dual-source blending");
|
|
}
|
|
|
|
void validationTests() {
|
|
AgcDriver::Graphics::State state{};
|
|
state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex;
|
|
ShaderRecompiler::RecompileResult fragment;
|
|
fragment.spirv = makeModule({.fragment = true});
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({.parameterOutput = true});
|
|
ShaderRecompiler::RecompileResult pixel;
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}};
|
|
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
|
for (const auto noPerspective : {false, true}) {
|
|
pixel.spirv = makeModule({.fragment = true, .barycentric = true, .barycentricNoPerspective = noPerspective, .perVertex = true});
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, true);
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "fragmentShaderBarycentric");
|
|
}
|
|
pixel.spirv = makeModule({.fragment = true, .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");
|
|
pixel.spirv = makeModule({.fragment = true});
|
|
vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 4, .cullDistanceLength = 4});
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, true);
|
|
state.paClVsOutCntl = 0x0040000fu;
|
|
vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 4});
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, true); }, "PA_CL_VS_OUT_CNTL=0x0040000f");
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, false);
|
|
vertex.spirv = makeModule({.parameterOutput = true, .cullDistanceLength = 4});
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, false); }, "PA_CL_VS_OUT_CNTL=0x0040000f");
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, true);
|
|
state.paClVsOutCntl = 0;
|
|
vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 9});
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, true); }, "clip or cull distance");
|
|
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");
|
|
const auto validateTerminate = [&](bool enabled) {
|
|
return AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, false, false, false, enabled);
|
|
};
|
|
vertex.spirv = makeModule({});
|
|
pixel.spirv = makeModule({.fragment = true, .terminateInvocation = true});
|
|
validateTerminate(true);
|
|
expectFailure([&] { validateTerminate(false); }, "shaderTerminateInvocation");
|
|
vertex.spirv = makeModule({.terminateInvocation = true});
|
|
pixel.spirv = makeModule({.fragment = true});
|
|
expectFailure([&] { validateTerminate(true); }, "fragment shader");
|
|
vertex.spirv = makeModule({});
|
|
pixel.spirv = makeModule({.fragment = true, .terminateInvocation = true, .omitTerminateExtension = true});
|
|
expectFailure([&] { validateTerminate(true); }, "OpTerminateInvocation requires SPV_KHR_terminate_invocation");
|
|
expectFailure([&] { validateTerminate(false); }, "OpTerminateInvocation requires enabled shaderTerminateInvocation");
|
|
vertex.spirv = makeModule({.terminateInvocation = true, .omitTerminateExtension = true});
|
|
pixel.spirv = makeModule({.fragment = true});
|
|
expectFailure([&] { validateTerminate(true); }, "OpTerminateInvocation requires enabled shaderTerminateInvocation in a fragment shader");
|
|
vertex.spirv = makeModule({});
|
|
pixel.spirv = ShaderRecompiler::SpecializeSpirv(makeModule({.fragment = true, .terminateInvocation = true}));
|
|
std::size_t terminates = 0;
|
|
std::size_t declarations = 0;
|
|
for (std::size_t at = 5; at < pixel.spirv.size(); at += pixel.spirv[at] >> 16u) {
|
|
const auto op = pixel.spirv[at] & 0xffffu;
|
|
if (op == spv::OpTerminateInvocation) ++terminates;
|
|
if (op == spv::OpExtension && std::string_view(reinterpret_cast<const char*>(&pixel.spirv[at + 1])) == "SPV_KHR_terminate_invocation") ++declarations;
|
|
}
|
|
Require(terminates == 1u && declarations == 1u, "specialization dropped OpTerminateInvocation or its extension declaration");
|
|
validateTerminate(true);
|
|
}
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({});
|
|
ShaderRecompiler::RecompileResult pixel;
|
|
pixel.spirv = makeModule({.fragment = true, .secondTarget = true});
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}};
|
|
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
|
Require(state.colors.empty() && AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false) == std::set<std::uint32_t>{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<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}};
|
|
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "missing attribute metadata");
|
|
vertex.vertexAttributes.push_back(attribute);
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false);
|
|
vertex.vertexAttributes[0].components = 2;
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "metadata disagrees");
|
|
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 80, "incorrect strided vertex range");
|
|
Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 2, 7) == 64, "vertex range includes records before the first vertex");
|
|
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<std::uint32_t, 4> indices{0xffffffffu, 2u, 0x12345u, 0xffffffffu};
|
|
Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(indices)), 4, true, 9u) == 9u && indices == std::array<std::uint32_t, 4>{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<std::uint32_t, 4>{9u, 2u, 9u, 9u}, "clamping without restart kept a 0xffffffff index");
|
|
std::array<std::uint16_t, 2> 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::VertexBufferReadOffset(attribute, 2, 7) == 224, "instance range includes records before the first instance");
|
|
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");
|
|
Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 100, 7) == 0, "zero-stride input skipped its repeated 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<ShaderRecompiler::IrBufferFormat>(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<std::uint32_t>(capability)});
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}};
|
|
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "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<std::uint32_t>(spv::CapabilityInt64Atomics)});
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}};
|
|
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability 12");
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true);
|
|
}
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({.layerOutput = true});
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}};
|
|
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "VK_EXT_shader_viewport_index_layer");
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, 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<std::uint32_t>(capability)});
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}};
|
|
VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
|
subgroup.supportedStages = VK_SHADER_STAGE_VERTEX_BIT;
|
|
subgroup.supportedOperations = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_BALLOT_BIT | VK_SUBGROUP_FEATURE_SHUFFLE_BIT;
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false);
|
|
subgroup.supportedStages = VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported for shader stage");
|
|
subgroup.supportedStages = VK_SHADER_STAGE_VERTEX_BIT;
|
|
subgroup.supportedOperations = capability == spv::CapabilityGroupNonUniform ? 0u : VK_SUBGROUP_FEATURE_BASIC_BIT;
|
|
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "device lacks operations");
|
|
}
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({});
|
|
vertex.spirv.insert(vertex.spirv.begin() + 5, {(2u << 16u) | spv::OpCapability, static_cast<std::uint32_t>(spv::CapabilityGroupNonUniformArithmetic)});
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}};
|
|
VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
|
subgroup.supportedStages = VK_SHADER_STAGE_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<std::uint32_t> words(8, 0);
|
|
const auto validate = [&](const ShaderRecompiler::RecompileResult& vertex, std::uint32_t fragmentOffset) {
|
|
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, fragmentOffset}}};
|
|
AgcDriver::Graphics::ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false);
|
|
};
|
|
const auto pushed = [&](const ModuleShape& shape) {
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule(shape);
|
|
vertex.pushConstants.assign(8, std::byte{1});
|
|
vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, words));
|
|
return vertex;
|
|
};
|
|
validate(pushed({.push = true, .bufferArray = 2}), 8);
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({.shaderData = true});
|
|
vertex.bindings.push_back(makeBinding(Role::ShaderData, 5, 1, {1, 2}));
|
|
validate(vertex, 0);
|
|
}
|
|
{
|
|
ShaderRecompiler::RecompileResult vertex;
|
|
vertex.spirv = makeModule({.bufferArray = 1, .shaderData = true});
|
|
vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, {1, 2, 3, 4}));
|
|
vertex.bindings.push_back(makeBinding(Role::ShaderData, 5, 1, {1, 2}));
|
|
validate(vertex, 0);
|
|
}
|
|
expectFailure([&] { validate(pushed({.push = true, .pushLength = 16, .bufferArray = 2}), 8); }, "32 elements");
|
|
expectFailure([&] { validate(pushed({.push = true, .pushStride = 8, .bufferArray = 2}), 8); }, "ArrayStride of 4");
|
|
expectFailure([&] { validate(pushed({.push = false, .bufferArray = 2}), 8); }, "push constant metadata disagrees with SPIR-V");
|
|
{
|
|
auto vertex = pushed({.push = true, .bufferArray = 2});
|
|
vertex.pushConstants.clear();
|
|
expectFailure([&] { validate(vertex, 8); }, "invalid push constant interface");
|
|
}
|
|
{
|
|
auto vertex = pushed({.push = true, .bufferArray = 3});
|
|
expectFailure([&] { validate(vertex, 8); }, "descriptor array length disagrees");
|
|
}
|
|
{
|
|
auto vertex = pushed({.push = true, .plainBuffer = true});
|
|
expectFailure([&] { validate(vertex, 8); }, "must be declared as a descriptor array");
|
|
}
|
|
{
|
|
auto vertex = pushed({.push = true, .bufferArray = 2});
|
|
vertex.bindings.front().readOnly = true;
|
|
expectFailure([&] { validate(vertex, 8); }, "read-only descriptor metadata is unsupported");
|
|
}
|
|
{
|
|
auto vertex = pushed({.push = true, .bufferArray = 2});
|
|
vertex.bindings.front().descriptorSet = 1;
|
|
expectFailure([&] { validate(vertex, 8); }, "descriptor set other than 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<std::uint32_t, 4> 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<void>(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");
|
|
memory.memoryHeapCount = 2;
|
|
memory.memoryHeaps[1].size = VkDeviceSize{16} << 30u;
|
|
memory.memoryTypeCount = 2;
|
|
memory.memoryTypes[0] = {VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, 0};
|
|
memory.memoryTypes[1] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, 1};
|
|
Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{256} << 20u, "the staging pool budget on a 16 GiB heap of system memory is not a sixty-fourth of it");
|
|
memory.memoryHeaps[1].size = VkDeviceSize{4} << 30u;
|
|
Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{128} << 20u, "the staging pool budget on system memory fell below 128 MiB");
|
|
memory.memoryTypes[0] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 1};
|
|
Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{512} << 20u, "a heap with a device-only memory type was taken for system memory");
|
|
}
|
|
|
|
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<std::uint16_t, 5> 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<std::uint32_t, 4> 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<std::uint16_t, 4> 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 storeAtLabelTests() {
|
|
using AgcDriver::Graphics::StorageTexture;
|
|
Require(!StorageTexture::StoreAtLabelRequested(nullptr), "an unset APS5_STORE_AT_LABEL stored at each label");
|
|
Require(StorageTexture::StoreAtLabelRequested("1"), "APS5_STORE_AT_LABEL=1 did not store at each label");
|
|
for (const char* value : {"0", "", "true", "11"}) expectFailure([&] { StorageTexture::StoreAtLabelRequested(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<VertexFetch, 3> 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<std::size_t>{0, 0, 0} && plan.offsets == std::vector<std::uint64_t>{0x18, 0, 0xc}, "interleaved attribute offsets are wrong");
|
|
Require(plan.alignments == std::vector<std::uint32_t>{4}, "the union's alignment is not its attributes'");
|
|
}
|
|
{
|
|
const std::array<VertexFetch, 2> 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<std::uint32_t>{4}, "a union did not take its strictest attribute alignment");
|
|
}
|
|
{
|
|
const std::array<VertexFetch, 6> 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<VertexFetch, 2> 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<VertexFetch, 1> 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<std::size_t>(stride) * records;
|
|
std::vector<std::byte> guest(extent);
|
|
std::fill(guest.begin(), guest.begin() + static_cast<std::ptrdiff_t>(valid), std::byte{0x7a});
|
|
std::fill(guest.begin() + static_cast<std::ptrdiff_t>(valid), guest.end(), std::byte{0x55});
|
|
const auto copy = CopyZeroPaddedDrawInput(mockContext(), reinterpret_cast<std::uint64_t>(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<std::ptrdiff_t>(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<std::ptrdiff_t>(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<VertexFetch> 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<std::byte> first(block * count + 100, std::byte{7});
|
|
auto second = first;
|
|
const std::array<std::size_t, 4> 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<CompareSpan> 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<std::uint8_t> 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<std::uint8_t> 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<std::uint8_t> none;
|
|
CompareSpansFrom({}, none, 0);
|
|
}
|
|
|
|
void ResourceContentKeyTests(bool benchmark) {
|
|
using AgcDriver::Graphics::ShaderResources;
|
|
using ShaderRecompiler::DescriptorKind;
|
|
using ShaderRecompiler::DescriptorRole;
|
|
for (const auto count : {0u, 1u, 31u, 32u, 33u, 16384u}) {
|
|
ShaderRecompiler::RecompileResult program;
|
|
ShaderRecompiler::DescriptorBinding images{};
|
|
images.kind = DescriptorKind::SampledImage;
|
|
images.role = DescriptorRole::GuestImages;
|
|
images.count = count;
|
|
images.guestDescriptor.resize(static_cast<std::size_t>(count) * 8u);
|
|
for (std::size_t i = 0; i < images.guestDescriptor.size(); ++i) images.guestDescriptor[i] = static_cast<std::uint32_t>(i * 1664525u + 1013904223u);
|
|
for (auto* bits : {&images.imageWritten, &images.samplerDepthCompare, &images.imageDepthCompare, &images.imageAtomic, &images.samplerUnnormalized, &images.samplerIntegerBorder, &images.imageUnnormalized, &images.imageUnnormalizedBaseLevel, &images.bufferWritten}) {
|
|
bits->resize(count);
|
|
for (std::size_t i = 0; i < bits->size(); ++i) (*bits)[i] = i % 3u == 0;
|
|
}
|
|
images.imageSamplers.resize(count, 7u);
|
|
images.imageShapes.resize(count, ShaderRecompiler::DescriptorImageShape::Image2D);
|
|
program.bindings.push_back(std::move(images));
|
|
ShaderRecompiler::DescriptorBinding data{};
|
|
data.kind = DescriptorKind::StorageBuffer;
|
|
data.role = DescriptorRole::ShaderData;
|
|
data.count = 1;
|
|
data.guestDescriptor.resize(257u, 0x12345678u);
|
|
program.bindings.push_back(std::move(data));
|
|
ShaderRecompiler::DescriptorBinding buffers{};
|
|
buffers.kind = DescriptorKind::StorageBuffer;
|
|
buffers.role = DescriptorRole::GuestBuffers;
|
|
buffers.count = 1;
|
|
buffers.guestDescriptor = {0x1000u, 0x10000u, 4u, 0x1234u};
|
|
buffers.bufferWritten = {false};
|
|
program.bindings.push_back(std::move(buffers));
|
|
const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0};
|
|
for (const bool dataWords : {false, true}) for (const bool movable : {false, true}) {
|
|
const auto original = ShaderResources::ContentKey(shader, dataWords, movable);
|
|
program.bindings[1].guestDescriptor.back() ^= 1u;
|
|
Require((ShaderResources::ContentKey(shader, dataWords, movable) != original) == (dataWords && !movable), "content key changed its data-word policy");
|
|
program.bindings[1].guestDescriptor.back() ^= 1u;
|
|
program.bindings[2].guestDescriptor[0] += 0x1000u;
|
|
Require((ShaderResources::ContentKey(shader, dataWords, movable) != original) == !movable, "content key changed its movable-buffer policy");
|
|
program.bindings[2].guestDescriptor[0] -= 0x1000u;
|
|
program.bindings[2].bufferWritten[0] = true;
|
|
Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the written-buffer mask");
|
|
program.bindings[2].bufferWritten[0] = false;
|
|
if (count != 0) {
|
|
for (auto* bits : {&program.bindings[0].imageWritten, &program.bindings[0].samplerDepthCompare, &program.bindings[0].imageDepthCompare, &program.bindings[0].imageAtomic, &program.bindings[0].samplerUnnormalized, &program.bindings[0].samplerIntegerBorder, &program.bindings[0].imageUnnormalized, &program.bindings[0].imageUnnormalizedBaseLevel}) {
|
|
bits->back().flip();
|
|
Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the last packed flag");
|
|
bits->back().flip();
|
|
}
|
|
++program.bindings[0].imageSamplers.back();
|
|
Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the last sampler association");
|
|
--program.bindings[0].imageSamplers.back();
|
|
program.bindings[0].imageShapes.back() = ShaderRecompiler::DescriptorImageShape::Image3D;
|
|
Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the last image shape");
|
|
program.bindings[0].imageShapes.back() = ShaderRecompiler::DescriptorImageShape::Image2D;
|
|
}
|
|
Require(original.capacity() == original.size(), "content key reserved a different word count than it serialized");
|
|
std::uint64_t hash = 14695981039346656037ull;
|
|
for (const auto word : original) hash = (hash ^ word) * 1099511628211ull;
|
|
std::cout << "content-key count=" << count << " data=" << dataWords << " movable=" << movable << " words=" << original.size() << " capacity=" << original.capacity() << " hash=" << hash;
|
|
if (benchmark) {
|
|
constexpr std::size_t repeats = 256;
|
|
std::uint64_t checksum = 0;
|
|
const auto begin = std::chrono::steady_clock::now();
|
|
for (std::size_t i = 0; i < repeats; ++i) {
|
|
const auto key = ShaderResources::ContentKey(shader, dataWords, movable);
|
|
checksum += key.size() + key.back();
|
|
}
|
|
const auto elapsed = std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - begin).count();
|
|
std::cout << " us/key=" << elapsed / repeats << " checksum=" << checksum;
|
|
}
|
|
std::cout << '\n';
|
|
}
|
|
}
|
|
}
|
|
|
|
// The pipeline journal without a GPU: the recipe codec and the store key agree with each other and
|
|
// with the canonical state, identities follow the module and the specialization, the journal
|
|
// records each pipeline once, and a replay into the recording context (the lavapipe test replays
|
|
// into a second device) rebuilds the recorded graphics pipeline so the same configuration hits the
|
|
// store, hands the compute result to its hook, recovers a tail torn inside a record's padding and
|
|
// stops when cancelled.
|
|
void pipelineJournalTests() {
|
|
using namespace AgcDriver::Graphics;
|
|
using Stage = ShaderRecompiler::ShaderStage;
|
|
const auto directory = std::filesystem::temp_directory_path() / ("aps5-pipeline-journal-mock-" + std::to_string(std::random_device{}()));
|
|
std::filesystem::create_directories(directory);
|
|
struct Cleanup {
|
|
std::filesystem::path directory;
|
|
~Cleanup() {
|
|
std::error_code error;
|
|
std::filesystem::remove_all(directory, error);
|
|
}
|
|
} cleanup{directory};
|
|
#ifdef _WIN32
|
|
_putenv_s("ANYPS5_NO_SHADER_CACHE", "0");
|
|
_putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str());
|
|
_putenv_s("APS5_PIPELINE_JOURNAL_THREADS", "2");
|
|
#else
|
|
setenv("ANYPS5_NO_SHADER_CACHE", "0", 1);
|
|
setenv("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str(), 1);
|
|
setenv("APS5_PIPELINE_JOURNAL_THREADS", "2", 1);
|
|
#endif
|
|
Require(PipelineJournalEnabled(), "the journal is disabled with a shader cache directory set");
|
|
const auto module = [](std::uint32_t salt) {
|
|
ShaderRecompiler::RecompileResult result;
|
|
result.spirv = std::vector<std::uint32_t>{0x07230203u, 0x00010000u, 0u, 8u, 0u, 0x00020011u, 1u, 0x00020011u, 2u + salt};
|
|
result.runtimeAbiVersion = ShaderRecompiler::RuntimeAbi::Version;
|
|
return result;
|
|
};
|
|
{
|
|
auto first = module(1);
|
|
const auto& constFirst = first;
|
|
auto copy = first;
|
|
const auto& constCopy = copy;
|
|
Require(!constFirst.Identity().Empty() && constFirst.Identity() == constCopy.Identity() && constFirst.spirv.data() == constCopy.spirv.data(), "a copied result did not share its module and identity");
|
|
copy.spirv[8] = 99;
|
|
Require(!(constCopy.Identity() == constFirst.Identity()) && constCopy.spirv.data() != constFirst.spirv.data() && constFirst.Identity() == module(1).Identity(), "writing a copied module did not give it an identity of its own");
|
|
auto specialized = module(1);
|
|
specialized.specialization.push_back({3, 4});
|
|
Require(!(specialized.Identity() == constFirst.Identity()), "specialization constants did not enter the identity");
|
|
auto subgroup = module(1);
|
|
subgroup.hostSubgroupSize = 64;
|
|
Require(!(subgroup.Identity() == constFirst.Identity()), "the host subgroup size did not enter the identity");
|
|
Require(ShaderRecompiler::RecompileResult{}.Identity().Empty(), "a result without a module has an identity");
|
|
}
|
|
mock = MockVulkan{};
|
|
auto context = mockContext();
|
|
context.device = makeHandle<VkDevice>();
|
|
context.graphicsPipelineLibrary = true;
|
|
context.tessellationShader = true;
|
|
context.limits.maxTessellationPatchSize = 32;
|
|
context.limits.maxColorAttachments = 8;
|
|
context.limits.maxPushConstantsSize = 256;
|
|
context.limits.maxVertexInputBindings = 16;
|
|
context.limits.maxVertexInputAttributes = 16;
|
|
context.limits.maxFramebufferWidth = 4096;
|
|
context.limits.maxFramebufferHeight = 4096;
|
|
context.limits.maxFramebufferLayers = 1;
|
|
std::uint64_t replayedComputeIdentityLow = 0;
|
|
{
|
|
DescriptorCache cache(context);
|
|
context.descriptorCache = &cache;
|
|
auto vertex = module(1);
|
|
vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, std::vector<std::uint32_t>(4)));
|
|
auto fragment = module(2);
|
|
fragment.pushConstants.resize(8);
|
|
fragment.bindings.push_back(makeBinding(Role::ShaderData, 0, 1, std::vector<std::uint32_t>(4)));
|
|
fragment.bindings.back().descriptorSet = ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet;
|
|
const std::array<CompiledShader, 2> shaders{{{Stage::Vertex, &vertex, 0}, {Stage::Fragment, &fragment, PipelinePushSlotBytes}}};
|
|
State state{};
|
|
state.stages = {ShaderPath::Vertex, 7u, 64u, 32u, std::nullopt, std::nullopt};
|
|
state.stages.tessellation = ShaderRecompiler::TessellationConfiguration{3, 3, 2, 1, 4, 128, 16, true};
|
|
ColorTarget color{};
|
|
color.address = 0x1000;
|
|
color.extent = {64, 32};
|
|
color.format = VK_FORMAT_R8G8B8A8_UNORM;
|
|
color.bytes = 64 * 32 * 4;
|
|
color.componentMapping = 0xec;
|
|
state.colors = {color, color};
|
|
state.colors[1].format = VK_FORMAT_B8G8R8A8_UNORM;
|
|
state.colors[1].exportIndex = 1;
|
|
state.color = color;
|
|
state.hasColorTarget = true;
|
|
VkPipelineColorBlendAttachmentState blend{};
|
|
blend.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
|
|
blend.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
|
|
blend.colorWriteMask = 0xf;
|
|
state.blends = {blend, blend};
|
|
state.blend = blend;
|
|
state.blendConstants = {0.25f, 0.5f, 0.75f, 1.0f};
|
|
DepthTarget depth{};
|
|
depth.address = 0x2000;
|
|
depth.format = VK_FORMAT_D32_SFLOAT;
|
|
depth.samples = 1;
|
|
state.depth = depth;
|
|
state.depthTest = true;
|
|
state.depthWrite = true;
|
|
state.depthCompare = VK_COMPARE_OP_LESS_OR_EQUAL;
|
|
state.stencilTest = true;
|
|
state.stencilFront = {VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xff, 0xff, 1};
|
|
state.stencilBack = state.stencilFront;
|
|
state.renderExtent = {64, 32};
|
|
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
|
|
state.viewport = {0, 32, 64, -32, 0, 1};
|
|
state.scissor = {{0, 0}, {64, 32}};
|
|
state.cullMode = VK_CULL_MODE_BACK_BIT;
|
|
state.frontFace = VK_FRONT_FACE_CLOCKWISE;
|
|
state.samples = 1;
|
|
const VertexInputLayout input{{{0, 16, VK_VERTEX_INPUT_RATE_VERTEX}}, {{0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}}};
|
|
const auto layouts = DescribeSetLayouts(context, shaders);
|
|
Require(layouts.layouts.split, "a fragment stage with the fragment set did not split the set layouts");
|
|
const auto recipe = MakeGraphicsPipelineRecipe(state, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
|
Require(recipe.stages.size() == 2 && recipe.stages[0].identity == vertex.Identity() && recipe.stages[1].identity == fragment.Identity() && !recipe.stages[0].generated && recipe.stages[1].pushConstantOffset == PipelinePushSlotBytes && recipe.splitSets, "the recipe does not name its stages");
|
|
Require(recipe.state.colors.size() == 2 && recipe.state.colors[0].address == 0 && recipe.state.colors[0].componentMapping == 0xec && recipe.state.depth && recipe.state.depth->address == 0 && recipe.state.viewport.width == 0 && recipe.state.stages.registerValue == 0 && recipe.state.stages.tessellation && recipe.state.stages.tessellation->breakAtInstance, "the canonical state kept or dropped the wrong fields");
|
|
const auto bytes = EncodeGraphicsPipelineRecipe(recipe);
|
|
GraphicsPipelineRecipe decoded;
|
|
Require(DecodeGraphicsPipelineRecipe(bytes, decoded), "the recipe does not decode");
|
|
Require(EncodeGraphicsPipelineRecipe(decoded) == bytes, "the recipe does not round-trip byte for byte");
|
|
for (std::size_t length = 0; length < bytes.size(); length += 7) {
|
|
GraphicsPipelineRecipe scratch;
|
|
Require(!DecodeGraphicsPipelineRecipe(std::span(bytes).first(length), scratch), "a truncated recipe decoded");
|
|
}
|
|
std::vector<std::byte> original, canonical, replayed, moved, mapped;
|
|
BuildPipelineStoreKey(original, context, state, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
|
BuildPipelineStoreKey(canonical, context, recipe.state, recipe.vertexInput, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
|
const PipelineSetLayouts decodedLayouts{layouts.layouts.main, layouts.layouts.fragment, decoded.splitSets, decoded.mainLayoutKey, decoded.fragmentLayoutKey};
|
|
BuildPipelineStoreKey(replayed, context, decoded.state, decoded.vertexInput, decodedLayouts, shaders, decoded.attachmentLayout);
|
|
Require(!original.empty() && original == canonical && original == replayed, "the canonical or the decoded state keys another pipeline");
|
|
auto gridState = state;
|
|
gridState.samples = 2;
|
|
gridState.customSampleLocations = true;
|
|
gridState.sampleLocationGrid = true;
|
|
gridState.perSampleShading = true;
|
|
gridState.sampleLocations[31] = 0x5a;
|
|
GraphicsPipelineRecipe gridDecoded;
|
|
Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(gridState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), gridDecoded), "the sample grid recipe does not decode");
|
|
Require(gridDecoded.state.sampleLocationGrid && gridDecoded.state.perSampleShading && gridDecoded.state.sampleLocations[31] == 0x5a, "the recipe dropped the sample grid, per-sample shading or the last sample location");
|
|
std::vector<std::byte> grid, gridReplayed;
|
|
BuildPipelineStoreKey(grid, context, gridState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
|
BuildPipelineStoreKey(gridReplayed, context, gridDecoded.state, gridDecoded.vertexInput, layouts.View(), shaders, gridDecoded.attachmentLayout);
|
|
Require(grid != original && grid == gridReplayed, "the decoded sample grid state keys another pipeline");
|
|
auto lineState = state;
|
|
lineState.stages.mesh = ShaderRecompiler::MeshConfiguration{1, 2, 2, 4, 4, 32, 64, 1, 4, 2};
|
|
GraphicsPipelineRecipe lineDecoded;
|
|
Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(lineState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), lineDecoded), "the line strip mesh recipe does not decode");
|
|
Require(lineDecoded.state.stages.mesh && lineDecoded.state.stages.mesh->outputPrimitiveSize == 2u && lineDecoded.state.stages.mesh->esgsItemSize == 4u, "the recipe dropped the mesh output primitive size");
|
|
auto fourSampleState = state;
|
|
fourSampleState.samples = 4;
|
|
auto maskState = fourSampleState;
|
|
maskState.quadSampleMask = {0x3u, 0x3u, 0x3u, 0x3u};
|
|
maskState.stencilReplayReference = 7u;
|
|
maskState.reversedBlend = 1u;
|
|
GraphicsPipelineRecipe maskDecoded;
|
|
Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(maskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), maskDecoded), "the sample mask recipe does not decode");
|
|
Require(maskDecoded.state.quadSampleMask == maskState.quadSampleMask && maskDecoded.state.stencilReplayReference.has_value() && maskDecoded.state.reversedBlend == 1u, "the recipe dropped the quad sample masks, the replayed stencil reference or the reversed blend");
|
|
std::vector<std::byte> fourSamples, masked, maskReplayed;
|
|
BuildPipelineStoreKey(fourSamples, context, fourSampleState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
|
BuildPipelineStoreKey(masked, context, maskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
|
BuildPipelineStoreKey(maskReplayed, context, maskDecoded.state, maskDecoded.vertexInput, layouts.View(), shaders, maskDecoded.attachmentLayout);
|
|
Require(masked != fourSamples && masked == maskReplayed, "the decoded sample mask state keys another pipeline");
|
|
auto movedState = state;
|
|
movedState.viewport.x = 5;
|
|
movedState.colors[0].address = 0x9000;
|
|
movedState.renderLayers = 4;
|
|
BuildPipelineStoreKey(moved, context, movedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
|
Require(moved == original, "a field the pipeline does not read changed the key");
|
|
auto mappedState = state;
|
|
mappedState.colors[0].componentMapping = 0x24;
|
|
BuildPipelineStoreKey(mapped, context, mappedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
|
Require(mapped != original, "the component mapping did not enter the key");
|
|
std::vector<std::byte> unclamped, clamped, biased;
|
|
auto clampedState = state;
|
|
clampedState.depthBiasClamp = 1.0f;
|
|
BuildPipelineStoreKey(clamped, context, clampedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
|
Require(clamped == original, "a depth bias clamp without depth bias changed the key");
|
|
clampedState.depthBias = true;
|
|
BuildPipelineStoreKey(clamped, context, clampedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
|
clampedState.depthBiasClamp = 0.0f;
|
|
BuildPipelineStoreKey(unclamped, context, clampedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
|
Require(unclamped != original && clamped != unclamped && CanonicalPipelineState(clampedState).depthBias && CanonicalPipelineState(mappedState).depthBiasClamp == 0.0f, "the depth bias clamp did not enter the key");
|
|
clampedState.depthBiasClamp = 2.0f;
|
|
BuildPipelineStoreKey(biased, context, clampedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
|
Require(biased == clamped, "the depth bias clamp's value entered the key");
|
|
auto compute = module(3);
|
|
compute.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, std::vector<std::uint32_t>(8)));
|
|
compute.pushConstants.resize(16);
|
|
compute.hostSubgroupSize = 32;
|
|
constexpr std::uint64_t fingerprint = 0x5eed'0000'0000'0001ull;
|
|
{
|
|
auto bound = StartPipelineJournalReplay(context, fingerprint, {});
|
|
Require(bound != nullptr, "the first replay did not start");
|
|
bound->Wait();
|
|
Require(bound->Done() && PipelineJournalTotals().loadedRecipes == 0, "an absent journal loaded pipelines");
|
|
}
|
|
const auto before = PipelineJournalTotals();
|
|
RecordGraphicsPipeline(recipe, shaders);
|
|
RecordComputePipeline(compute, true);
|
|
FlushPipelineJournal();
|
|
const auto recorded = PipelineJournalTotals();
|
|
Require(recorded.recordedGraphics == before.recordedGraphics + 1 && recorded.recordedCompute == before.recordedCompute + 1 && recorded.recordedModules == before.recordedModules + 3 && recorded.writeFailures == 0, "the journal did not record one graphics and one compute pipeline with three modules");
|
|
RecordGraphicsPipeline(recipe, shaders);
|
|
RecordComputePipeline(compute, true);
|
|
FlushPipelineJournal();
|
|
const auto repeated = PipelineJournalTotals();
|
|
Require(repeated.recordedGraphics == recorded.recordedGraphics && repeated.recordedCompute == recorded.recordedCompute && repeated.recordedModules == recorded.recordedModules, "a repeated pipeline was recorded again");
|
|
Require(std::filesystem::file_size(PipelineJournalPath()) > 0, "the journal file is empty");
|
|
const auto storeBefore = PipelineStoreTotals();
|
|
std::vector<std::byte> receivedWords;
|
|
ShaderRecompiler::RecompileResult received;
|
|
bool receivedPush = false;
|
|
std::uint32_t computeCalls = 0;
|
|
PipelineJournalHooks hooks{[&](const ShaderRecompiler::RecompileResult& shader, bool pushConstants) {
|
|
++computeCalls;
|
|
received = shader;
|
|
receivedPush = pushConstants;
|
|
return true;
|
|
}};
|
|
auto replay = StartPipelineJournalReplay(context, fingerprint, hooks);
|
|
Require(replay != nullptr, "the replay did not start");
|
|
replay->Wait();
|
|
Require(replay->Done(), "the replay did not finish");
|
|
const auto totals = PipelineJournalTotals();
|
|
Require(totals.loadedRecipes == 2 && totals.loadedModules == 3 && totals.droppedRecords == 0, "the replay did not load the two recorded pipelines and three modules");
|
|
Require(totals.replayedGraphics == 1 && totals.replayedCompute == 1 && totals.replayRejected == 0 && totals.replayFailed == 0, "the replay did not build one graphics and one compute pipeline");
|
|
Require(computeCalls == 1 && receivedPush && received.Identity() == compute.Identity() && received.spirv == compute.spirv && received.bindings.size() == 1 && received.bindings[0].count == 2 && received.pushConstants.size() == 16 && received.hostSubgroupSize == 32, "the compute hook did not receive the recorded result");
|
|
replayedComputeIdentityLow = received.Identity().low;
|
|
const auto storeReplayed = PipelineStoreTotals();
|
|
Require(storeReplayed.misses == storeBefore.misses + 1 && storeReplayed.cached == storeBefore.cached + 1, "the replayed graphics pipeline did not enter the store");
|
|
const auto created = mock.pipelineCreateCount;
|
|
bool built = true;
|
|
const auto pipeline = CachedPipeline(context, state, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL, &built);
|
|
const auto storeHit = PipelineStoreTotals();
|
|
Require(pipeline != nullptr && !built && mock.pipelineCreateCount == created && storeHit.hits == storeReplayed.hits + 1 && storeHit.misses == storeReplayed.misses, "the draw's configuration did not find the replayed pipeline");
|
|
{
|
|
const auto journalPath = PipelineJournalPath();
|
|
const auto whole = std::filesystem::file_size(journalPath);
|
|
Require(whole % 8 == 0 && whole > 3, "the journal file is not record aligned");
|
|
std::filesystem::resize_file(journalPath, whole - 3);
|
|
auto later = module(4);
|
|
later.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, std::vector<std::uint32_t>(4)));
|
|
RecordComputePipeline(later, false);
|
|
FlushPipelineJournal();
|
|
Require(std::filesystem::file_size(journalPath) % 8 == 0 && PipelineJournalTotals().writeFailures == 0, "an append after a tail torn inside a record's padding left the file unaligned");
|
|
auto torn = StartPipelineJournalReplay(context, fingerprint, hooks);
|
|
Require(torn != nullptr, "the replay after the torn tail did not start");
|
|
torn->Wait();
|
|
const auto reloaded = PipelineJournalTotals();
|
|
Require(reloaded.droppedRecords == 0 && reloaded.loadedModules == 4 && reloaded.loadedRecipes == 2 && reloaded.replayFailed == 0, "the journal did not recover from a tail torn inside a record's padding");
|
|
RecordComputePipeline(compute, true);
|
|
FlushPipelineJournal();
|
|
Require(PipelineJournalTotals().recordedCompute == reloaded.recordedCompute + 1 && PipelineJournalTotals().recordedModules == reloaded.recordedModules, "the pipeline whose record was torn was not recorded again, or its module was");
|
|
}
|
|
{
|
|
const auto before = PipelineJournalTotals();
|
|
auto cancelled = StartPipelineJournalReplay(context, fingerprint, hooks);
|
|
Require(cancelled != nullptr, "the third replay did not start");
|
|
cancelled->Cancel();
|
|
cancelled->Wait();
|
|
const auto after = PipelineJournalTotals();
|
|
Require(cancelled->Done() && after.replayFailed == 0 && after.replayRejected == 0 && after.replayedGraphics + after.replayPresent <= before.replayedGraphics + before.replayPresent + 1, "a cancelled replay did not stop cleanly");
|
|
}
|
|
{
|
|
const std::uint32_t meshWithoutOutputSize = 2u;
|
|
std::fstream file(PipelineJournalPath(), std::ios::in | std::ios::out | std::ios::binary);
|
|
file.seekp(4);
|
|
file.write(reinterpret_cast<const char*>(&meshWithoutOutputSize), sizeof(meshWithoutOutputSize));
|
|
file.close();
|
|
Require(file.good(), "the journal header could not be rewritten");
|
|
auto stale = StartPipelineJournalReplay(context, fingerprint, hooks);
|
|
Require(stale != nullptr, "the replay of a journal in the previous format did not start");
|
|
stale->Wait();
|
|
const auto discarded = PipelineJournalTotals();
|
|
Require(discarded.loadedRecipes == 0 && discarded.loadedModules == 0, "a journal in format 2, whose mesh records lack the output primitive size, was loaded");
|
|
}
|
|
ClearCachedPipelines(context.device);
|
|
}
|
|
Require(replayedComputeIdentityLow != 0, "the replayed compute identity is empty");
|
|
Require(mock.live == 0, "the pipeline journal test leaked Vulkan objects");
|
|
}
|
|
|
|
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]) == "--unused-stencil-reference") {
|
|
stencilReferenceUnusedTests();
|
|
return 0;
|
|
}
|
|
if (argc == 2 && std::string_view(argv[1]) == "--pipeline-journal") {
|
|
pipelineJournalTests();
|
|
std::cout << "pipeline journal recipe, record and replay tests passed\n";
|
|
return 0;
|
|
}
|
|
if (argc == 2 && std::string_view(argv[1]) == "--sparse-buffer-holes") {
|
|
bool passed = false;
|
|
{
|
|
auto context = mockContext();
|
|
context.limits.maxStorageBufferRange = 1u << 27;
|
|
passed = RunSparseBufferHoleTests(context, {
|
|
[](VkBuffer buffer) -> std::span<std::byte> { return mockBufferMemory(buffer); }, {}, {}, {}, {}
|
|
});
|
|
}
|
|
Require(mock.live == 0, "sparse buffer hole tests leaked Vulkan objects");
|
|
std::cout << "sparse buffer hole tests " << (passed ? "passed" : "failed") << '\n';
|
|
return passed ? 0 : 1;
|
|
}
|
|
if (argc == 2 && std::string_view(argv[1]) == "--address-space-teardown") {
|
|
addressSpaceTeardownTests();
|
|
std::cout << "address-space device teardown releases cached guest allocations\n";
|
|
return 0;
|
|
}
|
|
if (argc == 2 && std::string_view(argv[1]) == "--buffer-budget-placement") {
|
|
bufferBudgetPlacementTests();
|
|
return 0;
|
|
}
|
|
{
|
|
AgcDriver::Graphics::PipelineCacheSynchronization synchronization;
|
|
auto pipelineUse = synchronization.LockForPipelineUse();
|
|
bool snapshotAcquired = false;
|
|
std::thread snapshot([&] { snapshotAcquired = synchronization.TryLockForSnapshot().owns_lock(); });
|
|
snapshot.join();
|
|
Require(!snapshotAcquired, "pipeline cache snapshot overlapped pipeline creation");
|
|
pipelineUse.unlock();
|
|
Require(synchronization.TryLockForSnapshot().owns_lock(), "pipeline cache snapshot stayed blocked after pipeline creation finished");
|
|
}
|
|
if (argc == 2 && std::string_view(argv[1]) == "--undithered-alpha-to-coverage-state") {
|
|
UnditheredAlphaToCoverageTests();
|
|
std::cout << "Existing undithered alpha-to-coverage states passed\n";
|
|
return 0;
|
|
}
|
|
if (argc == 2 && (std::string_view(argv[1]) == "--resource-content-key" || std::string_view(argv[1]) == "--benchmark-resource-content-key")) {
|
|
ResourceContentKeyTests(std::string_view(argv[1]) == "--benchmark-resource-content-key");
|
|
return 0;
|
|
}
|
|
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]) == "--alpha-to-coverage-state") {
|
|
MsaaEnableSingleSampleTests();
|
|
AlphaToCoverageTests();
|
|
std::cout << "Alpha-to-coverage state and refusal tests passed\n";
|
|
return 0;
|
|
}
|
|
if (argc == 2 && std::string_view(argv[1]) == "--image-table-allocation") {
|
|
imageTableAllocationTests();
|
|
return 0;
|
|
}
|
|
if (argc == 2 && std::string_view(argv[1]) == "--image-table-snapshot") {
|
|
imageTableSnapshotAllocationTests();
|
|
return 0;
|
|
}
|
|
const std::array<std::string_view, 3> samplerCases{"--sampler-table-lod-bias", "--sampler-table-anisotropy", "--sampler-table-reduction-compare"};
|
|
for (std::uint32_t index = 0; index < samplerCases.size(); ++index) {
|
|
if (argc == 2 && std::string_view(argv[1]) == samplerCases[index]) {
|
|
samplerTableDeviceLimitTests(index);
|
|
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;
|
|
}
|
|
if (argc == 2 && std::string_view(argv[1]) == "--covered-centroid-state") {
|
|
multisampleTests();
|
|
std::cout << "Covered-centroid and multisample state checks passed\n";
|
|
return 0;
|
|
}
|
|
if (argc == 2 && std::string_view(argv[1]) == "--multisample-dcc-state") {
|
|
multisampleDccStateTests();
|
|
std::cout << "Multisampled DCC state checks passed\n";
|
|
return 0;
|
|
}
|
|
{
|
|
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();
|
|
ResourceContentKeyTests(false);
|
|
stateTests();
|
|
retainedPixelStageTests();
|
|
pipelineLibrarySetTests();
|
|
hdrComponentSwapTests();
|
|
depthMaintenanceTests();
|
|
hardwareScreenOffsetTests();
|
|
srgb8TargetTests();
|
|
DepthClipTests();
|
|
DepthStencilTests();
|
|
OneDimensionalColorTests();
|
|
htileDepthClearTests();
|
|
memoryValueFillTests();
|
|
ZExportTests();
|
|
unorm10_11_11TargetTests();
|
|
DepthBoundsBiasTests();
|
|
conservativeZExportTests();
|
|
earlyFragmentTestsTests();
|
|
orderedPixelShaderTests();
|
|
stencilRefExportTests();
|
|
stencilReferenceUnusedTests();
|
|
aliasedBufferViewTests();
|
|
LineStripTests();
|
|
ProvokingVertexTests();
|
|
ProvokingVertexRenderPassTests();
|
|
ConservativeRasterizationTests();
|
|
DisabledColorTests();
|
|
OpaqueDestinationAlphaTests();
|
|
Color1555TargetTests();
|
|
BlendBypassTests();
|
|
CompactedExportTests();
|
|
ReversedComponentOrderTests();
|
|
ReversedComponentOrderBlendTests();
|
|
ReversedBlendRoutingTests();
|
|
ShadersWriteMemoryTests();
|
|
DualSourceBlendTests();
|
|
ReversedBlendShaderTests();
|
|
metadataPassTests();
|
|
cmaskTests();
|
|
uint16ExportTests();
|
|
uint8x4TargetTests();
|
|
multisampleTests();
|
|
MsaaEnableSingleSampleTests();
|
|
clipDistanceTests();
|
|
framebufferLayerTests();
|
|
UnwrittenSampleLocationTests();
|
|
quadPixelMaskTests();
|
|
ShaderStageTests();
|
|
NggVertexWorkgroupTests();
|
|
inactiveVertexPointSizeTests();
|
|
TuningFieldTests();
|
|
PixelInputLayoutTests();
|
|
UnexportedPixelInputTests();
|
|
ColorPipeBankXorTests();
|
|
HeaderlessVertexStageTests();
|
|
HiddenUserWordVertexTablesTests();
|
|
ComputeScratchTests();
|
|
shaderUserDataTailPaddingTests();
|
|
InitialContextTests();
|
|
AlphaToCoverageTests();
|
|
descriptorPoolTests();
|
|
pushConstantTests();
|
|
resourceTests();
|
|
oversizedDescriptorTests();
|
|
descriptorSnapshotTests();
|
|
dispatchMoveTests();
|
|
descriptorCacheTests();
|
|
bufferBudgetPlacementTests();
|
|
imageTableAllocationTests();
|
|
imageTableSnapshotAllocationTests();
|
|
samplerTableNonSeamlessCubeTests();
|
|
samplerTableDeviceLimitTests();
|
|
misalignedShaderDataTests();
|
|
debugBranchTests();
|
|
textureCacheBudgetTests();
|
|
sampledTextureBudgetTests();
|
|
storageTextureBudgetTests();
|
|
sampledBudgetReportTests();
|
|
meshArgumentTests();
|
|
meshIndexBufferTests();
|
|
stagingPoolBudgetTests();
|
|
validationTests();
|
|
vertexCopyTests();
|
|
highestDrawIndexTests();
|
|
storeAtFlipTests();
|
|
storeAtLabelTests();
|
|
nullVertexDescriptorTests();
|
|
vertexZeroFillTests();
|
|
vertexZeroFillMergeTests();
|
|
parallelCompareTests();
|
|
pixelParameterSlotTests();
|
|
rectListTests();
|
|
floatControlsModeTests();
|
|
barycentricEmulationTests();
|
|
copiedWriterIndirectOverlapTests();
|
|
registeredReadableCoverTests();
|
|
mock = MockVulkan{};
|
|
auto bdaContext = mockContext();
|
|
bdaContext.bufferDeviceAddress = true;
|
|
bdaContext.limits.maxStorageBufferRange = 1u << 27;
|
|
RunBdaResourceTests(bdaContext, {
|
|
[](VkBuffer buffer) -> std::span<std::byte> { 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<VkBuffer>(offset / 0x10000);
|
|
return mockBufferMemory(buffer).subspan(offset % 0x10000);
|
|
},
|
|
[](std::optional<VkDeviceSize> headroom) {
|
|
mock.memoryLimit = headroom.has_value() ? std::optional<VkDeviceSize>(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<std::byte>& { return mock.memories.at(mock.bufferMemory.at(buffer)); },
|
|
[] {
|
|
Require(mock.writes.size() >= 2, "expected texture detiling descriptor writes");
|
|
const auto& destinationWrite = mock.writes.back();
|
|
const auto& sourceWrite = mock.writes[mock.writes.size() - 2];
|
|
DetilerCapture capture{};
|
|
capture.groupsX = mock.lastDispatchGroups.x;
|
|
capture.groupsY = mock.lastDispatchGroups.y;
|
|
capture.groupsZ = mock.lastDispatchGroups.z;
|
|
capture.pushConstants = mock.lastPushConstants;
|
|
capture.sourceBuffer = sourceWrite.buffers.at(0).buffer;
|
|
capture.sourceOffset = sourceWrite.buffers.at(0).offset;
|
|
capture.sourceRange = sourceWrite.buffers.at(0).range;
|
|
capture.destinationBuffer = destinationWrite.buffers.at(0).buffer;
|
|
capture.destinationOffset = destinationWrite.buffers.at(0).offset;
|
|
capture.destinationRange = destinationWrite.buffers.at(0).range;
|
|
return capture;
|
|
},
|
|
[] { return mock.pipelineCreateCount; },
|
|
[] { return mock.pipelineSpecializations.back(); },
|
|
[] { return mock.descriptorPools.size(); },
|
|
[] {
|
|
std::uint32_t resets = 0;
|
|
for (const auto& [handle, pool] : mock.descriptorPools) resets += pool.resets;
|
|
return resets;
|
|
}
|
|
});
|
|
std::cout << "Graphics validation tests passed\n";
|
|
return 0;
|
|
} catch (const std::exception& error) {
|
|
std::cerr << error.what() << '\n';
|
|
return 1;
|
|
}
|
|
}
|