feat(agc): implement primitive-ordered pixel shaders with fragment shader interlock
DecodeState and the draw precheck rejected DB_SHADER_CONTROL bit 16, PRIMITIVE_ORDERED_PIXEL_SHADER (POPS), which runs the ordered section of overlapping pixels' shaders in primitive order. The bit now reaches the recompiler as a pixel stage field, and such a pixel shader declares PixelInterlockOrderedEXT and runs its whole body between OpBeginInvocationInterlockEXT and OpEndInvocationInterlockEXT; an invocation that a BDA read fault kills ends the interlock first. The POPS collision SGPRs read 0, so a title skips its POPS wait and runs its per-collision loop once. An image_load still reads a sampled binding, which misses an earlier overlapping primitive's image_store in the same draw. The device enables VK_EXT_fragment_shader_interlock when it has fragmentShaderPixelInterlock; without it the recompile throws, as does a nonzero POPS_OVERLAP_NUM_SAMPLES. The field is part of the recompile cache key, the draw key and the serialized request (version 10).
This commit is contained in:
parent
3e32797cca
commit
e1d4e699be
|
|
@ -63,6 +63,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
|
|||
mix(pixel.inputAddr);
|
||||
for (const bool flag : {pixel.wave32, pixel.hasPerspectiveCenterVgpr, pixel.perspectiveCentroid, pixel.posX, pixel.posY, pixel.posZ, pixel.posW, pixel.frontFace, pixel.ancillary, pixel.sampleShading, pixel.noPerspective, pixel.linearCentroid, pixel.pixelKillEnable, pixel.depthExportEnable, pixel.sampleMaskExportEnable, pixel.earlyZ, pixel.executeOnNoop}) mix(flag);
|
||||
mix(static_cast<std::uint64_t>(pixel.conservativeZExport));
|
||||
mix(pixel.orderedPixelShader);
|
||||
for (const auto value : pixel.targetOutputMode) mix(value);
|
||||
for (const auto value : pixel.targetExportMapping) mix(value);
|
||||
std::lock_guard cacheLock(drawCacheMutex);
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
|
|||
if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false;
|
||||
const auto& p = a.pixel;
|
||||
const auto& q = b.pixel;
|
||||
if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false;
|
||||
if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false;
|
||||
if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false;
|
||||
for (std::size_t i = 0; i < a.programs.size(); ++i) {
|
||||
const auto& x = a.programs[i];
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@ struct VulkanDevice::State {
|
|||
bool samplerAnisotropy = false;
|
||||
bool textureCompressionBC = false;
|
||||
bool samplerFilterMinmax = false;
|
||||
bool fragmentShaderPixelInterlock = false;
|
||||
// VK_KHR_timeline_semaphore enabled: the recorder's unlocked waits are available.
|
||||
bool timelineSemaphores = false;
|
||||
bool computeWave32 = false;
|
||||
|
|
@ -750,6 +751,14 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
|
||||
state->fragmentShaderBarycentric = barycentricFeatures.fragmentShaderBarycentric == VK_TRUE;
|
||||
}
|
||||
VkPhysicalDeviceFragmentShaderInterlockFeaturesEXT interlockFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT};
|
||||
if (hasExtension(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME)) {
|
||||
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &interlockFeatures};
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
|
||||
state->fragmentShaderPixelInterlock = interlockFeatures.fragmentShaderPixelInterlock == VK_TRUE;
|
||||
}
|
||||
interlockFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT};
|
||||
interlockFeatures.fragmentShaderPixelInterlock = VK_TRUE;
|
||||
VkPhysicalDeviceShaderClockFeaturesKHR clockFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CLOCK_FEATURES_KHR};
|
||||
if (hasExtension(VK_KHR_SHADER_CLOCK_EXTENSION_NAME)) {
|
||||
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &clockFeatures};
|
||||
|
|
@ -763,6 +772,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
state->capabilities.push_back(spv::CapabilityFragmentBarycentricKHR);
|
||||
state->spirvExtensions.push_back("SPV_KHR_fragment_shader_barycentric");
|
||||
}
|
||||
if (state->fragmentShaderPixelInterlock) {
|
||||
deviceExtensions.push_back(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME);
|
||||
state->capabilities.push_back(spv::CapabilityFragmentShaderPixelInterlockEXT);
|
||||
state->spirvExtensions.push_back("SPV_EXT_fragment_shader_interlock");
|
||||
}
|
||||
if (state->shaderClock) {
|
||||
deviceExtensions.push_back(VK_KHR_SHADER_CLOCK_EXTENSION_NAME);
|
||||
state->capabilities.push_back(spv::CapabilityShaderClockKHR);
|
||||
|
|
@ -990,6 +1004,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
barycentricFeatures.pNext = byteFeatures.pNext;
|
||||
byteFeatures.pNext = &barycentricFeatures;
|
||||
}
|
||||
if (state->fragmentShaderPixelInterlock) {
|
||||
interlockFeatures.pNext = byteFeatures.pNext;
|
||||
byteFeatures.pNext = &interlockFeatures;
|
||||
}
|
||||
if (state->shaderClock) {
|
||||
clockFeatures.pNext = byteFeatures.pNext;
|
||||
byteFeatures.pNext = &clockFeatures;
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
|
|||
.earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable,
|
||||
.executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0,
|
||||
.conservativeZExport = static_cast<ShaderRecompiler::ConservativeZExport>(conservativeZExport),
|
||||
.orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0,
|
||||
.targetOutputMode = targetOutputMode,
|
||||
.targetExportMapping = exportMappings
|
||||
};
|
||||
|
|
|
|||
|
|
@ -207,6 +207,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
|||
mesh &&
|
||||
capability == spv::CapabilityMeshShadingEXT;
|
||||
|
||||
const bool isInterlockCapability =
|
||||
fragment &&
|
||||
capability == spv::CapabilityFragmentShaderPixelInterlockEXT;
|
||||
|
||||
// Enabled unconditionally or by the device setup in VulkanDevice.
|
||||
const bool isFeatureCapability =
|
||||
capability == spv::CapabilitySampled1D ||
|
||||
|
|
@ -241,6 +245,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
|||
isBdaCapability ||
|
||||
isTessellationCapability ||
|
||||
isMeshCapability ||
|
||||
isInterlockCapability ||
|
||||
isFeatureCapability ||
|
||||
isDescriptorIndexingCapability ||
|
||||
(imageInt64Atomics && (capability == spv::CapabilityInt64Atomics || capability == spv::CapabilityInt64ImageEXT)),
|
||||
|
|
@ -256,6 +261,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
|||
const auto end = std::find(text, text + bytes.size(), '\0');
|
||||
Require(end != text + bytes.size(), "unterminated SPIR-V extension");
|
||||
const std::string_view extension(text, static_cast<std::size_t>(end - text));
|
||||
if (extension == "SPV_EXT_fragment_shader_interlock") {
|
||||
Require(fragment, "SPV_EXT_fragment_shader_interlock requires a fragment shader");
|
||||
break;
|
||||
}
|
||||
if (extension == "SPV_KHR_fragment_shader_barycentric") {
|
||||
Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader");
|
||||
break;
|
||||
|
|
@ -376,7 +385,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
|||
mode(spv::ExecutionModeVertexOrderCw, {});
|
||||
Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode");
|
||||
} else if (fragment) {
|
||||
for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater), "unsupported fragment execution mode");
|
||||
for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater || name == spv::ExecutionModePixelInterlockOrderedEXT), "unsupported fragment execution mode");
|
||||
} else Require(module.modes.empty(), "unsupported vertex execution mode");
|
||||
std::set<std::pair<std::uint32_t, std::uint32_t>> descriptors;
|
||||
bool push = false;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ constexpr std::uint32_t LayerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) |
|
|||
constexpr std::uint32_t DepthControlMask = ~0x007007f0u;
|
||||
// EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader
|
||||
// to run, which it always does here.
|
||||
constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u);
|
||||
constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u | 0x00010000u);
|
||||
constexpr std::uint32_t PixelStageRunsMask = 0x00020747u;
|
||||
constexpr std::uint32_t AlphaToCoverageMask = ~0x0001ff00u;
|
||||
constexpr std::uint32_t ScanModeMask = ~2u;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "SceShaders.hpp"
|
||||
#include "ControlFlow/RequestSerializer.hpp"
|
||||
#include "CacheKey.hpp"
|
||||
#include "BdaAbi.hpp"
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
|
@ -1935,6 +1936,141 @@ void conservativeZExportTests() {
|
|||
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment execution mode");
|
||||
}
|
||||
|
||||
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) {
|
||||
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) return "an interlock instruction or OpKill outside the entry point";
|
||||
continue;
|
||||
}
|
||||
if (label == 0) continue;
|
||||
auto& block = blocks[label];
|
||||
block.ops.push_back(op);
|
||||
if (op == spv::OpBranch) block.next = {words[at + 1]};
|
||||
if (op == spv::OpBranchConditional) block.next = {words[at + 2], words[at + 3]};
|
||||
if (op == spv::OpSwitch) {
|
||||
block.next = {words[at + 2]};
|
||||
for (auto target = at + 4; target < end; target += 2) block.next.push_back(words[target]);
|
||||
}
|
||||
}
|
||||
std::map<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;
|
||||
}
|
||||
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;
|
||||
const auto exits = interlockExits(ShaderRecompiler::Recompile(faulting).spirv.Words(), kills);
|
||||
Require(exits.empty() && kills == 6u, "a primitive-ordered pixel shader with BDA reads: " + (exits.empty() ? std::to_string(kills) + " OpKill, not the valid-mask kill and 5 BDA fault kills" : exits));
|
||||
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(orderedPixelRequest(0x30600u, plain))); }, "fragmentShaderPixelInterlock");
|
||||
}
|
||||
|
||||
void pixelParameterSlotTests() {
|
||||
using AgcDriver::Graphics::CompiledShader;
|
||||
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
||||
|
|
@ -2325,6 +2461,7 @@ int main() {
|
|||
ZExportTests();
|
||||
DepthBoundsBiasTests();
|
||||
conservativeZExportTests();
|
||||
orderedPixelShaderTests();
|
||||
DisabledColorTests();
|
||||
CompactedExportTests();
|
||||
metadataPassTests();
|
||||
|
|
|
|||
|
|
@ -678,7 +678,7 @@ void verifyPixelRequestSerialization() {
|
|||
value.hasPerspectiveCenterVgpr, value.perspectiveCentroid, value.posX, value.posY,
|
||||
value.posZ, value.posW, value.frontFace, value.ancillary, value.sampleShading,
|
||||
value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable,
|
||||
value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport,
|
||||
value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, value.orderedPixelShader,
|
||||
value.targetOutputMode, value.targetExportMapping);
|
||||
};
|
||||
const std::array<std::uint32_t, 1> code{0xbf810000u};
|
||||
|
|
@ -718,6 +718,7 @@ void verifyPixelRequestSerialization() {
|
|||
.earlyZ = true,
|
||||
.executeOnNoop = true,
|
||||
.conservativeZExport = ConservativeZExport::GreaterThanZ,
|
||||
.orderedPixelShader = true,
|
||||
.targetOutputMode = {1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u}
|
||||
};
|
||||
for (std::uint32_t i = 0; i < pixel.interpolatorSettings.size(); ++i) pixel.interpolatorSettings[i] = 0x10101010u + i;
|
||||
|
|
@ -756,19 +757,19 @@ void verifyPixelRequestSerialization() {
|
|||
minimal.context.waveSize = 64;
|
||||
minimal.context.pixel = ShaderPixelStageInfo{};
|
||||
const auto encoded = serializer.Serialize(minimal);
|
||||
require(requestPrefix(encoded, 8u) == "NVNQQQoAAAA=", "new requests did not use serialization version 10");
|
||||
constexpr std::size_t mappingOffset = 8u + 37u + 18u + 162u;
|
||||
require(requestPrefix(encoded, 8u) == "NVNQQQsAAAA=", "new requests did not use serialization version 11");
|
||||
constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u;
|
||||
for (std::size_t bytes = 0; bytes < 8u; ++bytes) {
|
||||
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated version-8 pixel mapping was accepted");
|
||||
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated version-11 pixel mapping was accepted");
|
||||
}
|
||||
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQQsAAAA="}) {
|
||||
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQQwAAAA="}) {
|
||||
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
|
||||
}
|
||||
}
|
||||
|
||||
void verifyLegacyPixelRequests() {
|
||||
using namespace ShaderRecompiler;
|
||||
static constexpr std::array<std::string_view, 8> legacyPixelRequests{
|
||||
static constexpr std::array<std::string_view, 10> legacyPixelRequests{
|
||||
"NVNQQQEAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAA"
|
||||
|
|
@ -809,6 +810,16 @@ void verifyLegacyPixelRequests() {
|
|||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgkAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAsAAAAAAAAAgAAAAAAAAQ==",
|
||||
"NVNQQQkAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgkAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAsAAAAAAAAAgAAAAAAAAQAAAAA=",
|
||||
"NVNQQQoAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgkAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAsAAAAAAAAAgAAAAAAAAQAAAAA=",
|
||||
};
|
||||
const RequestSerializer serializer;
|
||||
for (std::size_t index = 0; index < legacyPixelRequests.size(); ++index) {
|
||||
|
|
@ -820,6 +831,7 @@ void verifyLegacyPixelRequests() {
|
|||
require(pixel.targetExportMapping == std::array<std::uint8_t, 8>{}, "legacy pixel mapping no longer defaults to zero");
|
||||
require(pixel.inputAddr == 2u && pixel.hasPerspectiveCenterVgpr && pixel.targetOutputMode[0] == 9u, "legacy pixel layout was misread");
|
||||
require(pixel.conservativeZExport == (version >= 7u ? ConservativeZExport::GreaterThanZ : ConservativeZExport::AnyZ), "legacy conservative Z layout was misread");
|
||||
require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader");
|
||||
require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned");
|
||||
require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned");
|
||||
require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u, "legacy request trailer was misread");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,239 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Width = 64;
|
||||
constexpr std::uint32_t Height = 32;
|
||||
constexpr std::uint32_t Layers = 64;
|
||||
constexpr std::uint32_t PixelInterlockCapability = 5378;
|
||||
constexpr std::size_t GuestBytes = 65536;
|
||||
constexpr std::uint32_t FaultColumns = 8;
|
||||
alignas(256) std::array<std::byte, Width * Height * 4> Pixels{};
|
||||
alignas(256) std::array<std::uint32_t, Width * Height> Counters{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 17> PixelCode{
|
||||
0x7e040f00, 0x7e060f01, 0x34060686, 0x4a040702, 0x34040482,
|
||||
0xe030d000, 0x80000402, 0xbf8c3f70, 0x4a080881, 0xe0705000, 0x80000402,
|
||||
0xbbfd0000, 0xbf900007, 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 30> FaultPixelCode{
|
||||
0x7e040f00, 0x7e060f01, 0x36100487, 0x34060686, 0x4a040702, 0x34040482, 0x7e0a02ff, 0x00008000,
|
||||
0x7e0c02ff, 0x00010000, 0x7d841080, 0x02160d05, 0x4a161702, 0xdc319000, 0x0a04000b, 0xe030d000,
|
||||
0x80000402, 0xbf8c3f70, 0x4a080881, 0x4a08090a, 0xe0705000, 0x80000402, 0xdce08000, 0x00040402,
|
||||
0xbbfd0000, 0xbf900007, 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000,
|
||||
};
|
||||
|
||||
class GuestCounters {
|
||||
public:
|
||||
GuestCounters() {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, 2 * GuestBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
#else
|
||||
block = static_cast<std::uint8_t*>(std::aligned_alloc(GuestBytes, 2 * GuestBytes));
|
||||
#endif
|
||||
Require(block != nullptr, "pixel interlock: cannot allocate the guest block");
|
||||
GuestAllocations::Mutation().Add(block, GuestBytes, true, true);
|
||||
}
|
||||
|
||||
~GuestCounters() {
|
||||
GuestAllocations::Mutation().Remove(block);
|
||||
#ifdef _WIN32
|
||||
VirtualFree(block, 0, MEM_RELEASE);
|
||||
#else
|
||||
std::free(block);
|
||||
#endif
|
||||
}
|
||||
|
||||
GuestCounters(const GuestCounters&) = delete;
|
||||
GuestCounters& operator=(const GuestCounters&) = delete;
|
||||
|
||||
void Clear() { std::memset(block, 0, 2 * GuestBytes); }
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(block); }
|
||||
std::uint32_t Word(std::size_t index) const {
|
||||
std::uint32_t value = 0;
|
||||
std::memcpy(&value, block + index * 4u, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
std::uint8_t* block = nullptr;
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
|
||||
}
|
||||
|
||||
const std::vector<std::array<float, 4>>& Triangles() {
|
||||
static const auto triangles = [] {
|
||||
std::vector<std::array<float, 4>> vertices;
|
||||
for (std::uint32_t layer = 0; layer < Layers; ++layer) {
|
||||
vertices.push_back({-1.0f, -1.0f, 0.5f, 1.0f});
|
||||
vertices.push_back({3.0f, -1.0f, 0.5f, 1.0f});
|
||||
vertices.push_back({-1.0f, 3.0f, 0.5f, 1.0f});
|
||||
}
|
||||
return vertices;
|
||||
}();
|
||||
return triangles;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::span<const std::uint32_t> pixelCode, std::span<const std::uint32_t> pixelUserData) {
|
||||
Pixels.fill(std::byte{0});
|
||||
Counters.fill(0);
|
||||
const auto target = device.Target();
|
||||
const auto& triangles = Triangles();
|
||||
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = BufferDescriptor(triangles.data(), 16u, static_cast<std::uint32_t>(triangles.size()));
|
||||
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
|
||||
ShaderRecompiler::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
target,
|
||||
{0, 0, 0, 64}
|
||||
};
|
||||
vertex.useCache = false;
|
||||
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
|
||||
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
|
||||
|
||||
ShaderRecompiler::ShaderPixelStageInfo pixel{};
|
||||
pixel.wave32 = waveSize == 32u;
|
||||
pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY);
|
||||
pixel.posX = true;
|
||||
pixel.posY = true;
|
||||
pixel.executeOnNoop = true;
|
||||
pixel.orderedPixelShader = true;
|
||||
pixel.targetOutputMode[0] = 9;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
|
||||
{waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
|
||||
target,
|
||||
{0, 0, vertexPush, 128 - vertexPush}
|
||||
};
|
||||
fragment.useCache = false;
|
||||
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
|
||||
{ShaderStage::Vertex, &vertexResult, 0},
|
||||
{ShaderStage::Fragment, &pixelResult, vertexPush}
|
||||
}};
|
||||
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u};
|
||||
state.colors = {state.color};
|
||||
state.hasColorTarget = true;
|
||||
state.renderExtent = {Width, Height};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
|
||||
state.negativeOneToOne = false;
|
||||
state.scissor = {{0, 0}, {Width, Height}};
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blend.colorWriteMask = 15;
|
||||
state.blends = {state.blend};
|
||||
state.blendConstants = {};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(triangles.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> CounterUserData() {
|
||||
std::vector<std::uint32_t> userData(4, 0u);
|
||||
const auto counters = BufferDescriptor(Counters.data(), 0u, static_cast<std::uint32_t>(sizeof(Counters)));
|
||||
std::copy(counters.begin(), counters.end(), userData.begin());
|
||||
return userData;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) {
|
||||
Draw(device, waveSize, PixelCode, CounterUserData());
|
||||
}
|
||||
|
||||
void DrawFault(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, GuestCounters& guest) {
|
||||
const auto what = "pixel interlock wave" + std::to_string(waveSize) + " BDA fault";
|
||||
guest.Clear();
|
||||
auto pixelUserData = CounterUserData();
|
||||
pixelUserData.push_back(static_cast<std::uint32_t>(guest.Address()));
|
||||
pixelUserData.push_back(static_cast<std::uint32_t>(guest.Address() >> 32u));
|
||||
std::string failure;
|
||||
try {
|
||||
Draw(device, waveSize, FaultPixelCode, pixelUserData);
|
||||
} catch (const std::exception& error) {
|
||||
failure = error.what();
|
||||
}
|
||||
device.WaitIdle();
|
||||
Require(failure.find("BDA access failed") != std::string::npos && failure.find("reason=1") != std::string::npos, what + ": the draw reported \"" + failure + "\", not an unmapped read");
|
||||
for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) {
|
||||
const auto expected = pixel % FaultColumns == 0 ? 0u : Layers;
|
||||
Require(guest.Word(pixel) == expected, what + ": pixel " + std::to_string(pixel) + " counted " + std::to_string(guest.Word(pixel)) + " of " + std::to_string(expected) + " overlapping read-modify-writes");
|
||||
Require(guest.Word(GuestBytes / 8u + pixel) == 0u && guest.Word(GuestBytes / 4u + pixel) == 0u, what + ": pixel " + std::to_string(pixel) + " stored into its read range");
|
||||
}
|
||||
}
|
||||
|
||||
void Check(std::uint32_t waveSize, std::uint32_t pass) {
|
||||
const auto what = "pixel interlock wave" + std::to_string(waveSize) + " pass " + std::to_string(pass);
|
||||
for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) {
|
||||
for (std::uint32_t channel = 0; channel < 4; ++channel) {
|
||||
Require(Pixels[pixel * 4u + channel] == std::byte{255}, what + ": missing color at pixel " + std::to_string(pixel));
|
||||
}
|
||||
Require(Counters[pixel] == Layers, what + ": pixel " + std::to_string(pixel) + " counted " + std::to_string(Counters[pixel]) + " of " + std::to_string(Layers) + " overlapping read-modify-writes");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
const auto capabilities = device->Target().supportedCapabilities;
|
||||
if (std::find(capabilities.begin(), capabilities.end(), PixelInterlockCapability) == capabilities.end()) {
|
||||
std::puts("skipped, the device lacks fragmentShaderPixelInterlock");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
GuestCounters guest;
|
||||
for (const auto waveSize : {64u, 32u}) {
|
||||
for (std::uint32_t pass = 0; pass < 3; ++pass) {
|
||||
Draw(*device, waveSize);
|
||||
Check(waveSize, pass);
|
||||
}
|
||||
DrawFault(*device, waveSize, guest);
|
||||
Draw(*device, waveSize);
|
||||
Check(waveSize, 3);
|
||||
}
|
||||
std::printf("pixel interlock tests passed: 10 draws of %u overlapping triangles over %u pixels, 2 of them with a killed BDA fault in every %uth column\n", Layers, Width * Height, FaultColumns);
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -142,6 +142,7 @@ private:
|
|||
append(key, value.earlyZ);
|
||||
append(key, value.executeOnNoop);
|
||||
append(key, value.conservativeZExport);
|
||||
append(key, value.orderedPixelShader);
|
||||
append(key, value.targetOutputMode);
|
||||
append(key, value.targetExportMapping);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) {
|
|||
writer.WriteBool(info.earlyZ);
|
||||
writer.WriteBool(info.executeOnNoop);
|
||||
writer.WriteU8(static_cast<std::uint8_t>(info.conservativeZExport));
|
||||
writer.WriteBool(info.orderedPixelShader);
|
||||
for (const std::uint8_t value : info.targetOutputMode) {
|
||||
writer.WriteU8(value);
|
||||
}
|
||||
|
|
@ -341,6 +342,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) {
|
|||
info.earlyZ = reader.ReadBool();
|
||||
info.executeOnNoop = reader.ReadBool();
|
||||
if (version >= 7u) info.conservativeZExport = readConservativeZExport(reader);
|
||||
if (version >= 11u) info.orderedPixelShader = reader.ReadBool();
|
||||
for (std::uint8_t& value : info.targetOutputMode) {
|
||||
value = reader.ReadU8();
|
||||
}
|
||||
|
|
@ -697,7 +699,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
|
|||
std::string buffer;
|
||||
Writer writer(buffer);
|
||||
writer.WriteU32(0x41505335u);
|
||||
writer.WriteU32(10u);
|
||||
writer.WriteU32(11u);
|
||||
writeShaderBinary(writer, request.shader);
|
||||
writeGuestContext(writer, request.context);
|
||||
writeSpirvTarget(writer, request.target);
|
||||
|
|
@ -723,7 +725,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const
|
|||
Reader reader(decoded);
|
||||
if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature");
|
||||
const auto version = reader.ReadU32();
|
||||
if (version < 1u || version > 10u) throw std::runtime_error("unsupported recompile request serialization version");
|
||||
if (version < 1u || version > 11u) throw std::runtime_error("unsupported recompile request serialization version");
|
||||
DeserializedRequest result{};
|
||||
result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader);
|
||||
result.request.context = readGuestContext(reader, result, version);
|
||||
|
|
|
|||
|
|
@ -185,6 +185,7 @@ struct ShaderPixelInputInfo {
|
|||
bool psEarlyZ = false;
|
||||
bool psExecuteOnNoop = false;
|
||||
ConservativeZExport psConservativeZExport = ConservativeZExport::AnyZ;
|
||||
bool psOrderedPixelShader = false;
|
||||
ShaderStageRuntime stage;
|
||||
|
||||
bool HasPositionInput() const {
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ struct ShaderPixelStageInfo {
|
|||
bool earlyZ;
|
||||
bool executeOnNoop;
|
||||
ConservativeZExport conservativeZExport;
|
||||
bool orderedPixelShader;
|
||||
std::array<std::uint8_t, 8> targetOutputMode;
|
||||
std::array<std::uint8_t, 8> targetExportMapping;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ DppTargetLane EmitDppRowRotateRightTargetLane(SpirvEmitterState& state, std::uin
|
|||
DppTargetLane EmitDppMirrorTargetLane(SpirvEmitterState& state, std::uint32_t subid, bool halfRow);
|
||||
DppTargetLane EmitDppTargetLane(SpirvEmitterState& state, std::uint32_t control);
|
||||
std::uint32_t InputVariableForKind(const SpirvEmitterState& state, StageInputKind kind);
|
||||
bool OrderedPixelShader(const SpirvEmitterState& state);
|
||||
const SpirvInputBinding* SpirvInputBindingForParameter(const SpirvEmitterState& state, std::uint32_t location);
|
||||
std::uint32_t EmitVertexParameterComponentU32(SpirvEmitterState& state, const SpirvInputBinding& input, std::uint32_t component);
|
||||
std::uint32_t EmitInputComponentU32(SpirvEmitterState& state, StageInputKind kind, std::uint32_t component);
|
||||
|
|
|
|||
|
|
@ -92,6 +92,9 @@ void StopBdaInvocationIf(SpirvEmitterState& state, std::uint32_t condition) {
|
|||
if (state.bdaStopValue != 0) {
|
||||
state.module.AddFunction(spv::OpReturnValue, state.bdaStopValue);
|
||||
} else {
|
||||
if (OrderedPixelShader(state)) {
|
||||
state.module.AddFunction(spv::OpEndInvocationInterlockEXT);
|
||||
}
|
||||
state.module.AddFunction(state.program.Resources().stage == IrShaderStage::Pixel ? spv::OpKill : spv::OpReturn);
|
||||
}
|
||||
EmitLabel(state, next);
|
||||
|
|
|
|||
|
|
@ -88,6 +88,9 @@ void EmitReturnTerminator(SpirvValueEmitContext& ctx) {
|
|||
RecordBdaFault(state, BdaConstant(state, state.program.Resources().shaderHash), ConstantU32(state, state.loopGuardLimit), EmitBinaryU32(state, spv::OpISub, pc, ConstantU32(state, 1u)), BdaAbi::FaultReason::LoopLimit);
|
||||
});
|
||||
}
|
||||
if (OrderedPixelShader(state)) {
|
||||
state.module.AddFunction(spv::OpEndInvocationInterlockEXT);
|
||||
}
|
||||
EmitKillIfPixelValidMaskInactive(state);
|
||||
state.module.AddFunction(spv::OpReturn);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,6 +151,10 @@ namespace ShaderRecompiler
|
|||
return 0;
|
||||
}
|
||||
|
||||
bool OrderedPixelShader(const SpirvEmitterState& state) {
|
||||
return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psOrderedPixelShader;
|
||||
}
|
||||
|
||||
const SpirvInputBinding* SpirvInputBindingForParameter(const SpirvEmitterState& state, std::uint32_t location) {
|
||||
for (const auto& input : state.inputs) {
|
||||
if (input.kind == StageInputKind::Parameter && input.location == location) {
|
||||
|
|
|
|||
|
|
@ -179,6 +179,14 @@ void DefineModule(SpirvEmitterState& state) {
|
|||
if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable) {
|
||||
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests);
|
||||
}
|
||||
if (pixel.psOrderedPixelShader) {
|
||||
if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast<std::uint32_t>(spv::CapabilityFragmentShaderPixelInterlockEXT)) == state.supportedCapabilities.end()) {
|
||||
throw std::runtime_error("a primitive-ordered pixel shader needs the fragmentShaderPixelInterlock feature, which the device lacks");
|
||||
}
|
||||
state.module.EmitCapability(spv::CapabilityFragmentShaderPixelInterlockEXT);
|
||||
state.module.EmitExtension("SPV_EXT_fragment_shader_interlock");
|
||||
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModePixelInterlockOrderedEXT);
|
||||
}
|
||||
}
|
||||
if (state.requirements.computeDerivatives && StageOf(state) == IrShaderStage::Compute) {
|
||||
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDerivativeGroupQuadsKHR);
|
||||
|
|
|
|||
|
|
@ -927,8 +927,14 @@ void EmitProgram(SpirvEmitterState& state) {
|
|||
if (state.pixelValidMaskVariable != 0u) {
|
||||
state.module.AddFunction(spv::OpStore, state.pixelValidMaskVariable, ConstantU32(state, 1u));
|
||||
}
|
||||
if (OrderedPixelShader(state)) {
|
||||
state.module.AddFunction(spv::OpBeginInvocationInterlockEXT);
|
||||
}
|
||||
EmitMemoryOffsets(state);
|
||||
if (program.BlockOrder().empty()) {
|
||||
if (OrderedPixelShader(state)) {
|
||||
state.module.AddFunction(spv::OpEndInvocationInterlockEXT);
|
||||
}
|
||||
if (state.pixelValidMaskVariable != 0u) {
|
||||
const auto maskValue = state.module.AllocateId();
|
||||
const auto active = state.module.AllocateId();
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues
|
|||
pixelStorage.psEarlyZ = pixel.earlyZ;
|
||||
pixelStorage.psExecuteOnNoop = pixel.executeOnNoop;
|
||||
pixelStorage.psConservativeZExport = pixel.conservativeZExport;
|
||||
pixelStorage.psOrderedPixelShader = pixel.orderedPixelShader;
|
||||
ShaderStageInputInfo result;
|
||||
result.pixel = &pixelStorage;
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -274,6 +274,9 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. SHA-1 and SHA-256 instructions with a RIP-relative operand fail the relink.
|
||||
- The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust VEX/0F38/0F3A RIP-relative operands, data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length.
|
||||
- [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured.
|
||||
- [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE.
|
||||
- [POPS collision SGPRs](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once.
|
||||
- [Image loads](../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw.
|
||||
- The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test.
|
||||
- The libc [SSE4a trap emulation](../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it.
|
||||
- [Descriptor pool chain](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page
|
||||
|
|
|
|||
Loading…
Reference in New Issue