feat(agc): single-sample sample masks as a per-quad-pixel discard

With one sample, PA_SC_AA_MASK leaves one coverage bit per pixel of the 2x2 quad,
so a draw can cover only some pixels of every quad (GTA V renders a target one
quarter of the pixels at a time this way). The pixel stage info carries that 4-bit
mask; a fragment shader whose mask is not full starts by killing the invocation
unless its gl_FragCoord quad position is covered, and runs without early fragment
tests. The mask is part of the recompile cache key and the serialized request
(version 24). Sample masks of multisampled draws stay refused.
This commit is contained in:
Michon authored and Dean Galvin committed 2026-10-15 12:59:39 +13:00
1 parent ab9a476840
commit 72518f64fb
13 files changed
+198 -13

No files matched your search

@@ -146,6 +146,9 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0;
const bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0;
const auto zOrder = (shaderControl >> 4u) & 0x3u;
const auto aaMask = read(context, 0x30e, RegisterBank::Context);
const auto aaMaskBottom = read(context, 0x30f, RegisterBank::Context);
const auto quadPixelMask = static_cast<std::uint8_t>((aaMask & 1u) | (((aaMask >> 16u) & 1u) << 1u) | ((aaMaskBottom & 1u) << 2u) | (((aaMaskBottom >> 16u) & 1u) << 3u));
const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; };
return ShaderRecompiler::ShaderPixelStageInfo{
.interpolatorCount = inputNum,
@@ -173,7 +176,8 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
.orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0,
.targetOutputMode = targetOutputMode,
.targetExportMapping = nullProgram ? std::array<std::uint8_t, 8>{} : exportMappings,
.stencilRefExportEnable = stencilRefExportEnable
.stencilRefExportEnable = stencilRefExportEnable,
.quadPixelMask = quadPixelMask
};
}
@@ -816,7 +816,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
Require(read(cx, 0x83) == 0xffffu, "clip rectangles are unsupported");
Require((read(cx, 0x8c) & 0xfu) == 0xau, "nonstandard triangle edge rules are unsupported");
Require(read(cx, 0x2f9) == 0x2du, "nonstandard pixel center or vertex quantization is unsupported");
Require(read(cx, 0x30e) == 0xffffffffu && read(cx, 0x30f) == 0xffffffffu, "sample masks are unsupported");
Require((read(cx, 0x2f8) & 7u) == 0 || (read(cx, 0x30e) == 0xffffffffu && read(cx, 0x30f) == 0xffffffffu), "sample masks of multisampled draws are unsupported");
const auto viewportControl = read(cx, 0x206);
if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl));
zero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags");
@@ -1391,7 +1391,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
if (value(cx, 0x2f9, word) && word != 0x2du) return require(false, "nonstandard pixel center or vertex quantization is unsupported");
if (value(cx, 0x313, word) && (word & ~0x001f6000u) != 0 && word != 0x6001u) return conservativeMessage(word);
std::uint32_t other = 0;
if (value(cx, 0x30e, word) && value(cx, 0x30f, other) && (word != 0xffffffffu || other != 0xffffffffu)) return require(false, "sample masks are unsupported");
if (value(cx, 0x2f8, word) && (word & 7u) != 0 && value(cx, 0x30e, word) && value(cx, 0x30f, other) && (word != 0xffffffffu || other != 0xffffffffu)) return require(false, "sample masks of multisampled draws are unsupported");
if (value(cx, 0x206, word) && word != 0x43fu) return vteMessage(word);
if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason;
std::uint32_t targetMask = 0, shaderMask = 0;
@@ -2780,6 +2780,21 @@ void framebufferLayerTests() {
}
}
void quadPixelMaskTests() {
auto queue = makeState();
queue.context[0x30e] = 0xffff0000u;
queue.context[0x30f] = 0u;
queue.context[0x1b3] = 2;
queue.context[0x1b4] = 2;
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a single-sample quad pixel mask was refused by the precheck");
Require(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, {}).quadPixelMask == 0x2u, "PA_SC_AA_MASK did not decode to the covered quad pixel");
queue = makeState();
queue.context[0x1b3] = 2;
queue.context[0x1b4] = 2;
Require(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, {}).quadPixelMask == 0xfu, "full sample masks did not cover the whole quad");
}
void DepthClipTests() {
auto queue = makeState();
const auto direct = AgcDriver::Graphics::DecodeState(queue);
@@ -6723,6 +6738,7 @@ int main(int argc, char** argv) {
clipDistanceTests();
framebufferLayerTests();
UnwrittenSampleLocationTests();
quadPixelMaskTests();
ShaderStageTests();
NggVertexWorkgroupTests();
inactiveVertexPointSizeTests();
@@ -2590,6 +2590,22 @@ void verifyMeshConfiguration() {
RecompileCacheKey::Build(vertex, unclipped);
require(clipped != unclipped, "PA_CL_VS_OUT_CNTL is not part of the recompile cache key");
}
{
RecompileRequest fragment{};
fragment.shader = {ShaderStage::Fragment, 0x10000u, code, 0, {}};
fragment.context.waveSize = 64;
ShaderPixelStageInfo info{};
info.quadPixelMask = 0x2u;
fragment.context.pixel = info;
const auto back = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(fragment));
require(back.request.context.pixel.has_value() && back.request.context.pixel->quadPixelMask == 0x2u, "the quad pixel mask was lost in serialization");
std::vector<std::uint64_t> masked;
RecompileCacheKey::Build(fragment, masked);
fragment.context.pixel->quadPixelMask = 0xfu;
std::vector<std::uint64_t> full;
RecompileCacheKey::Build(fragment, full);
require(masked != full, "the quad pixel mask is not part of the recompile cache key");
}
std::vector<std::uint64_t> key;
RecompileCacheKey::Build(request, key);
const auto first = key;
@@ -2970,7 +2986,7 @@ void verifyPixelRequestSerialization() {
minimal.context.pixel->layerExported = true;
minimal.context.pixel->ditheredAlphaToCoverage = true;
const auto encoded = serializer.Serialize(minimal);
require(requestPrefix(encoded, 8u) == "NVNQQRsAAAA=", "new requests did not use serialization version 27");
require(requestPrefix(encoded, 8u) == "NVNQQRwAAAA=", "new requests did not use serialization version 28");
const std::array<std::uint32_t, 2> fallbacks{0x40u, 0x1ac4u};
auto withFallbacks = minimal;
withFallbacks.context.linkedCallFallbacks = fallbacks;
@@ -3085,10 +3101,10 @@ void verifyPixelRequestSerialization() {
require(RecompileCacheKey::ContextHash(legacy19.request) == RecompileCacheKey::ContextHash(upgraded19.request), "upgrading version19 changed shader identity");
}
constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u;
for (std::size_t bytes = 0; bytes < 24u; ++bytes) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag was accepted");
for (std::size_t bytes = 0; bytes < 25u; ++bytes) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, or quad pixel mask was accepted");
}
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRwAAAA="}) {
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQR0AAAA="}) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
}
static constexpr std::string_view version18Request{
@@ -3213,12 +3229,23 @@ void verifyVertexInfoVersion19() {
std::size_t allBitsOffset = 0;
while (allBitsOffset < version26Pixel.size() && version26Pixel[allBitsOffset] == allBitsBytes[allBitsOffset]) ++allBitsOffset;
require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag");
version26Pixel.erase(version26Pixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset));
version26Pixel.erase(version26Pixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset), version26Pixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset + 2u));
version26Pixel[4] = 26u;
const auto replay26 = serializer.Deserialize(encodeBase64(version26Pixel));
require(replay26.request.context.pixel.has_value() && replay26.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replay26.request.context.pixel->frontFaceAllBits, "a version 26 request lost its pixel primitive class or read a front-face all-bits flag");
require(serializer.Serialize(replay26.request) == encodedPixel, "a version 26 request misread the fields after its dithered coverage flag");
legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(flagOffset), legacyPixel.begin() + static_cast<std::ptrdiff_t>(flagOffset + 3u));
auto maskedPixel = pixel;
maskedPixel.context.pixel->quadPixelMask = 0x2u;
const auto maskedBytes = decodeBase64(serializer.Serialize(maskedPixel));
std::size_t maskOffset = 0;
while (maskOffset < legacyPixel.size() && legacyPixel[maskOffset] == maskedBytes[maskOffset]) ++maskOffset;
require(maskOffset == flagOffset + 3u && maskOffset < legacyPixel.size(), "the quad pixel mask is not the byte after the front-face all-bits flag");
auto version27 = maskedBytes;
version27.erase(version27.begin() + static_cast<std::ptrdiff_t>(maskOffset));
version27[4] = 27u;
const auto replay27 = serializer.Deserialize(encodeBase64(version27));
require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or misread the fields after it");
legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(flagOffset), legacyPixel.begin() + static_cast<std::ptrdiff_t>(flagOffset + 4u));
legacyPixel.resize(legacyPixel.size() - sizeof(std::uint64_t));
for (const auto version : {19u, 20u, 21u, 22u}) {
auto versionPixel = legacyPixel;
@@ -0,0 +1,115 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <array>
#include <cstdio>
#include <iostream>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Width = 64;
constexpr std::uint32_t Height = 32;
alignas(256) std::array<std::byte, Width * Height * 4> Pixels{};
alignas(256) std::array<std::array<float, 4>, 3> Triangle{{{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}}};
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 4> PixelCode{
0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000,
};
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};
}
void Draw(AgcDriver::VulkanDevice& device, std::uint8_t quadPixelMask) {
Pixels.fill(std::byte{0});
const auto target = device.Target();
std::vector<std::uint32_t> vertexUserData(4, 0u);
const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast<std::uint32_t>(Triangle.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, {}},
{64, 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.targetOutputMode[0] = 9;
pixel.targetExportMapping.fill(0xe4u);
pixel.quadPixelMask = quadPixelMask;
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(PixelCode.data()), PixelCode, 0, {}},
{64, 0, {}, std::nullopt, pixel, std::nullopt, pixelMemory},
target,
PixelPushLayout(vertexPush, target)
};
fragment.useCache = false;
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &vertexResult, 0},
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
}};
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, 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>(Triangle.size()), 0, 1, 0, false};
device.Draw(state, draw, shaders);
device.WaitIdle();
}
void Check(std::uint8_t quadPixelMask) {
for (std::uint32_t y = 0; y < Height; ++y) {
for (std::uint32_t x = 0; x < Width; ++x) {
const bool expected = ((quadPixelMask >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0;
const bool written = std::to_integer<std::uint8_t>(Pixels[(y * Width + x) * 4u]) == 255u;
Require(written == expected, "quad pixel mask 0x" + std::to_string(quadPixelMask) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + (written ? "written" : "not written"));
}
}
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
for (const std::uint8_t mask : {std::uint8_t{0xf}, std::uint8_t{0x2}, std::uint8_t{0x9}}) {
Draw(*device, mask);
Check(mask);
}
std::puts("quad pixel mask tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
+1
View File
@@ -191,6 +191,7 @@ private:
append(key, value.alphaToCoverage);
append(key, value.stencilRefExportEnable);
append(key, value.ditheredAlphaToCoverage);
append(key, value.quadPixelMask);
append(key, value.targetOutputMode);
}
@@ -340,6 +340,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) {
writer.WriteBool(info.layerExported);
writer.WriteBool(info.ditheredAlphaToCoverage);
writer.WriteBool(info.frontFaceAllBits);
writer.WriteU8(info.quadPixelMask);
}
ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) {
@@ -393,6 +394,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) {
if (version >= 23u) info.layerExported = reader.ReadBool();
if (version >= 25u) info.ditheredAlphaToCoverage = reader.ReadBool();
if (version >= 27u) info.frontFaceAllBits = reader.ReadBool();
if (version >= 28u) info.quadPixelMask = reader.ReadU8();
if (version < 5u) {
const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; };
info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) |
@@ -764,7 +766,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
std::string buffer;
Writer writer(buffer);
writer.WriteU32(0x41505335u);
writer.WriteU32(27u);
writer.WriteU32(28u);
writeShaderBinary(writer, request.shader);
writeGuestContext(writer, request.context);
writeSpirvTarget(writer, request.target);
@@ -797,7 +799,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 > 27u) throw std::runtime_error("unsupported recompile request serialization version");
if (version < 1u || version > 28u) 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);
@@ -181,6 +181,7 @@ struct ShaderPixelInputInfo {
bool psOrderedPixelShader = false;
bool psAlphaToCoverage = false;
bool psDitheredAlphaToCoverage = false;
std::uint8_t quadPixelMask = 0xf;
ShaderStageRuntime stage;
bool HasPositionInput() const {
@@ -237,7 +237,7 @@ bool IsPixelParameterFlat(const ShaderPixelInputInfo& pixel, std::uint32_t input
}
void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pixel, ShaderInfo& info) {
if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage) {
if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage || pixel->quadPixelMask != 0xf) {
AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord");
}
if (pixel->psFrontFace) {
+1
View File
@@ -154,6 +154,7 @@ struct ShaderPixelStageInfo {
RasterPrimitive rasterPrimitive = RasterPrimitive::Triangle;
bool layerExported = false;
bool ditheredAlphaToCoverage = false;
std::uint8_t quadPixelMask = 0xf;
};
struct ShaderVertexBufferResource {
@@ -196,7 +196,7 @@ void DefineModule(SpirvEmitterState& state) {
state.module.EmitExtension("SPV_EXT_shader_stencil_export");
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT);
}
if (pixel.psEarlyZ && !pixel.psAlphaToCoverage) {
if (pixel.psEarlyZ && !pixel.psAlphaToCoverage && pixel.quadPixelMask == 0xf) {
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests);
}
if (pixel.psOrderedPixelShader) {
@@ -1180,6 +1180,23 @@ void EmitProgram(SpirvEmitterState& state) {
state.module.AddFunction(spv::OpBeginInvocationInterlockEXT);
}
EmitMemoryOffsets(state);
if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->quadPixelMask != 0xfu) {
const auto coordinate = [&](std::uint32_t component) {
const auto value = Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component));
return EmitBinaryU32(state, spv::OpBitwiseAnd, Unary(state, spv::OpConvertFToU, TypeU32(state), value), ConstantU32(state, 1u));
};
const auto quad = EmitBinaryU32(state, spv::OpBitwiseOr, coordinate(0u), EmitBinaryU32(state, spv::OpShiftLeftLogical, coordinate(1u), ConstantU32(state, 1u)));
const auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, ConstantU32(state, state.inputInfo.pixel->quadPixelMask), quad), ConstantU32(state, 1u));
const auto uncovered = state.module.AllocateId();
state.module.AddFunction(spv::OpIEqual, TypeBool(state), uncovered, bit, ConstantU32(state, 0u));
const auto killLabel = state.module.AllocateId();
const auto mergeLabel = state.module.AllocateId();
state.module.AddFunction(spv::OpSelectionMerge, mergeLabel, spv::SelectionControlMaskNone);
state.module.AddFunction(spv::OpBranchConditional, uncovered, killLabel, mergeLabel);
EmitLabel(state, killLabel);
state.module.AddFunction(spv::OpKill);
EmitLabel(state, mergeLabel);
}
if (program.BlockOrder().empty()) {
if (OrderedPixelShader(state)) {
state.module.AddFunction(spv::OpEndInvocationInterlockEXT);
@@ -148,6 +148,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues
if (pixel.ditheredAlphaToCoverage && !pixel.alphaToCoverage) throw std::runtime_error("dithered alpha-to-coverage requires enabled coverage");
pixelStorage.psAlphaToCoverage = pixel.alphaToCoverage;
pixelStorage.psDitheredAlphaToCoverage = pixel.ditheredAlphaToCoverage;
pixelStorage.quadPixelMask = pixel.quadPixelMask;
ShaderStageInputInfo result;
result.pixel = &pixelStorage;
return result;