fix(shader): packed pixel ancillary as SampleId and Layer, gl_Layer via Geometry, Z export formats

Bit-field extracts of the packed ancillary VGPR become gl_SampleID and gl_Layer reads. A pixel
shader reading gl_Layer needs the Geometry capability instead of ShaderLayer and SPIR-V 1.5, and
the Geometry capability is available whenever the geometryShader feature is. 32-bit
SPI_SHADER_Z_FORMATs and 32_ABGR are accepted with the depth and sample-mask export enables.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michon 2026-09-29 19:08:10 +02:00
parent af92452ff6
commit 5a6e2b8eb5
4 changed files with 97 additions and 5 deletions

View File

@ -852,6 +852,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
if (enabled.shaderImageGatherExtended) state->capabilities.push_back(spv::CapabilityImageGatherExtended);
enabled.shaderResourceMinLod = available.shaderResourceMinLod;
if (enabled.shaderResourceMinLod) state->capabilities.push_back(spv::CapabilityMinLod);
enabled.sampleRateShading = available.sampleRateShading;
enabled.geometryShader = available.geometryShader;
if (enabled.geometryShader) state->capabilities.push_back(spv::CapabilityGeometry);
enabled.shaderClipDistance = available.shaderClipDistance;
if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat);
if (enabled.shaderStorageImageReadWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageReadWithoutFormat);
// Bindless image tables index an image array with a wave-uniform runtime slot.

View File

@ -72,6 +72,19 @@ constexpr std::uint32_t ScreenOffsetMask = ~0x01ff01ffu;
// Bits 26/27 (ZCLIP_NEAR/FAR_DISABLE) become depth clamping; bit 19 selects the [0, 1] clip space.
constexpr std::uint32_t ClipControlMask = ~(0x80000u | 0x0c000000u);
bool zFormatSupported(std::uint32_t format) {
return format == 0u || format == 1u || format == 2u || format == 3u || format == 9u;
}
std::uint32_t shaderControlMask(std::uint32_t zFormat) {
constexpr std::uint32_t zExportEnable = 0x1u;
constexpr std::uint32_t maskExportEnable = 0x100u;
auto mask = ShaderControlMask;
if (zFormat != 0u && zFormatSupported(zFormat)) mask &= ~zExportEnable;
if (zFormat == 9u) mask &= ~maskExportEnable;
return mask;
}
// Debug aid: APS5_IGNORE_DEPTH_TEST=1 renders depth- and stencil-tested draws without a depth
// target as if their tests always passed (wrong occlusion, but the draws run), so stages that
// depth-test everything can exercise the draw paths before depth targets exist.
@ -469,7 +482,7 @@ State DecodeState(const QueueState& queue) {
Require((depthControl & 8u) == 0 || result.depth.has_value(), "depth bounds without a depth surface");
Require((depthControl & 0xc0000000u) == 0, "depth-conditional color writes are unsupported");
}
zero(cx, 0x203, ShaderControlMask, "depth export, shader coverage or ordered fragment execution");
zero(cx, 0x203, shaderControlMask(read(cx, 0x1c4)), "depth export, shader coverage or ordered fragment execution");
zero(cx, 0x2dc, AlphaToCoverageMask, "alpha-to-coverage");
zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion");
zero(cx, 0x292, ScanModeMask, "scan conversion mode");
@ -515,7 +528,7 @@ State DecodeState(const QueueState& queue) {
// CB_COLOR_CONTROL mode 0 disables color writes, which only matters when a target is written.
if (const auto colorControl = read(cx, 0x202); !colorControlSupported(colorControl, result.hasColorTarget)) throw std::runtime_error(colorControlMessage(colorControl));
zero(cx, 0x1c4, ~0u, "depth or sample-mask export");
zero(cx, 0x1c4, zFormatSupported(read(cx, 0x1c4)) ? 0u : ~0u, "depth or sample-mask export");
const auto exportFormat = result.hasColorTarget ? read(cx, 0x1c5) : 0u;
APS5_LOG_OUT_DEBUG("Export format=%u", exportFormat);
// SPI_SHADER_POS_FORMAT: POS0 must be a 4-component position; later vectors carry the misc/clip
@ -758,7 +771,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
if (auto reason = require((word & 8u) == 0 || surface, "depth bounds without a depth surface"); !reason.empty()) return reason;
if (auto reason = require((word & 0xc0000000u) == 0, "depth-conditional color writes are unsupported"); !reason.empty()) return reason;
}
if (auto reason = nonzero(cx, 0x203, ShaderControlMask, "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason;
std::uint32_t zFormat = 0;
static_cast<void>(value(cx, 0x1c4, zFormat));
if (auto reason = nonzero(cx, 0x203, shaderControlMask(zFormat), "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "alpha-to-coverage"); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x292, ScanModeMask, "scan conversion mode"); !reason.empty()) return reason;
@ -775,7 +790,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason;
std::uint32_t targetMask = 0, shaderMask = 0;
if (value(cx, 0x8e, targetMask) && value(cx, 0x8f, shaderMask) && value(cx, 0x202, word) && !colorControlSupported(word, (targetMask & shaderMask) != 0)) return colorControlMessage(word);
if (auto reason = nonzero(cx, 0x1c4, ~0u, "depth or sample-mask export"); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x1c4, zFormatSupported(read(cx, 0x1c4)) ? 0u : ~0u, "depth or sample-mask export"); !reason.empty()) return reason;
if (PixelProgramUnset(queue)) return NullPixelProgramRejection(queue);
for (const auto offset : {0x1b3u, 0x1b4u, 0x1c5u}) {
if (find(cx, offset) != cx.end()) continue;

View File

@ -2,8 +2,12 @@
#include "IntermediateRepresentation/IrBuilder.hpp"
#include <algorithm>
#include <array>
#include <bit>
#include <cstdint>
#include <initializer_list>
#include <utility>
#include <vector>
namespace ShaderRecompiler {
namespace {
@ -181,12 +185,78 @@ bool foldCompositeExtract(IrBuilder& builder, IrValue& inst, IrOpcode construct,
return false;
}
void collectUses(IrValue& value, std::vector<IrUse>& uses) {
for (const IrUse& use : value.OperandUses()) {
if (use.user->Opcode() == IrOpcode::Identity) {
collectUses(*use.user, uses);
} else {
uses.push_back(use);
}
}
}
bool lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancillary) {
auto& kind = resolveArg(ancillary, 0);
if (!isImmediate(kind, IrType::U32) || static_cast<StageInputKind>(kind.ImmediateU32()) != StageInputKind::PackedAncillary) {
return false;
}
std::vector<IrUse> uses;
collectUses(ancillary, uses);
if (uses.empty()) {
return false;
}
IrBlock& block = *ancillary.Parent();
const auto emit = [&](IrOpcode opcode, std::initializer_list<IrValue*> arguments) -> IrValue& {
IrValue& created = program.CreateValue(opcode, IrType::U32);
for (IrValue* argument : arguments) {
created.AddArgument(argument);
}
block.InsertInstructionBefore(&ancillary, &created);
return created;
};
std::array<IrValue*, 2> fields {};
const auto field = [&](std::size_t index) -> IrValue& {
if (fields[index] == nullptr) {
const auto builtin = index == 0u ? StageInputKind::SampleId : StageInputKind::Layer;
fields[index] = &emit(IrOpcode::GetBuiltin, {&builder.Constant(static_cast<std::uint32_t>(builtin)), &builder.Constant(0u)});
}
return *fields[index];
};
constexpr std::array<std::pair<std::uint32_t, std::uint32_t>, 2> ranges {{{8u, 4u}, {16u, 13u}}};
IrValue* packed = nullptr;
for (const IrUse& use : uses) {
IrValue& user = *use.user;
if ((user.Opcode() == IrOpcode::BitFieldUExtract || user.Opcode() == IrOpcode::BitFieldSExtract) && use.operand == 0u) {
auto& offset = resolveArg(user, 1);
auto& count = resolveArg(user, 2);
if (isImmediate(offset, IrType::U32) && isImmediate(count, IrType::U32) && count.ImmediateU32() != 0u) {
const auto range = std::ranges::find_if(ranges, [&](const auto& candidate) {
return offset.ImmediateU32() >= candidate.first && count.ImmediateU32() <= candidate.first + candidate.second - offset.ImmediateU32();
});
if (range != ranges.end()) {
user.ReplaceArgument(0, &field(static_cast<std::size_t>(range - ranges.begin())));
user.ReplaceArgument(1, &builder.Constant(offset.ImmediateU32() - range->first));
continue;
}
}
}
if (packed == nullptr) {
IrValue& sample = emit(IrOpcode::ShiftLeftLogical32, {&field(0u), &builder.Constant(ranges[0].first)});
IrValue& layer = emit(IrOpcode::ShiftLeftLogical32, {&field(1u), &builder.Constant(ranges[1].first)});
packed = &emit(IrOpcode::BitwiseOr32, {&sample, &layer});
}
user.ReplaceArgument(use.operand, packed);
}
return true;
}
} // namespace
bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const {
IrBuilder builder(program);
switch (value.Opcode()) {
case IrOpcode::Phi: return foldPhi(value);
case IrOpcode::GetBuiltin: return lowerPackedAncillary(program, builder, value);
case IrOpcode::SelectU1:
case IrOpcode::SelectF32:
case IrOpcode::SelectU32: return foldSelect(value);

View File

@ -132,10 +132,13 @@ void DefineModule(SpirvEmitterState& state) {
if (state.cullDistanceVariable != 0) {
state.module.EmitCapability(spv::CapabilityCullDistance);
}
if (state.layerVariable != 0 || InputVariableForKind(state, StageInputKind::Layer) != 0) {
if (state.layerVariable != 0) {
state.module.RequireVersion(0x00010500u);
state.module.EmitCapability(spv::CapabilityShaderLayer);
}
if (InputVariableForKind(state, StageInputKind::Layer) != 0) {
state.module.EmitCapability(spv::CapabilityGeometry);
}
if (state.viewportIndexVariable != 0) {
state.module.RequireVersion(0x00010500u);
state.module.EmitCapability(spv::CapabilityShaderViewportIndex);