mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-30 12:25:49 +00:00
Merge pull request #11199 from QRMOE/fix/shader-pixel-helper-lane-masks
fix(shader): keep pixel helper lanes out of guest lane masks
This commit is contained in:
8 files changed
+246
-11
No files matched your search
@@ -0,0 +1,201 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Sentinel = 0xdeadbeefu;
|
||||
constexpr std::uint32_t Width = 192;
|
||||
constexpr std::uint32_t Height = 128;
|
||||
constexpr std::uint32_t Size = 16;
|
||||
constexpr std::uint32_t Levels = 5;
|
||||
constexpr std::uint32_t PassLimit = 80;
|
||||
alignas(256) std::array<std::byte, Width * Height * 4> Pixels{};
|
||||
alignas(256) std::array<std::uint32_t, Width * Height * 2> Results{};
|
||||
alignas(256) std::array<std::uint8_t, 16384> Texels{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 42> PixelCode{
|
||||
0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, 0x000000c0, 0xbec4047e, 0x7e120506, 0xbefe0a7e,
|
||||
0x101400ff, 0x3baaaaab, 0x101602ff, 0x3c000000, 0xf0800f08, 0x01060c0a, 0xbe8a047e, 0xbe8c0380,
|
||||
0xbe8d0380, 0xbe940480, 0xbe82140a, 0xd7600003, 0x00000506, 0x890d030d, 0xd4c20014, 0x00020c03,
|
||||
0x8a8a140a, 0x800c810c, 0xbf09ff0c, 0x00000050, 0xbf850002, 0xbf13800a, 0xbf85fff3, 0xbefe0444,
|
||||
0x8f0c900c, 0x880d0c0d, 0x7e100209, 0x7e12020d, 0xe0742000, 0x80010806, 0xbf8c3f70, 0xf800180f,
|
||||
0x0f0e0d0c, 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};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor() {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(Texels.data());
|
||||
return {static_cast<std::uint32_t>(address >> 8), static_cast<std::uint32_t>((address >> 40) & 0xffu) | (56u << 20) | (3u << 30),
|
||||
((Size - 1u) >> 2) | ((Size - 1u) << 14), 0xfacu | ((Levels - 1u) << 16) | (9u << 28), 0u, (Levels - 1u) << 4, 0u, 0u};
|
||||
}
|
||||
|
||||
void FillTexture() {
|
||||
const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor()));
|
||||
Require(geometry.guestBytes <= Texels.size(), "helper waterfall texture does not fit");
|
||||
Texels.fill(0xffu);
|
||||
}
|
||||
|
||||
std::vector<std::array<float, 4>> Triangles() {
|
||||
std::vector<std::array<float, 4>> vertices;
|
||||
constexpr std::uint32_t columns = 6;
|
||||
constexpr std::uint32_t rows = 4;
|
||||
for (std::uint32_t cell = 0; cell < columns * rows; ++cell) {
|
||||
const float width = 2.0f / columns;
|
||||
const float height = 2.0f / rows;
|
||||
const float x = -1.0f + width * static_cast<float>(cell % columns);
|
||||
const float y = -1.0f + height * static_cast<float>(cell / columns);
|
||||
const float skew = 0.07f * static_cast<float>(cell % 5u);
|
||||
vertices.push_back({x + (0.05f + skew) * width, y + 0.05f * height, 0.5f, 1.0f});
|
||||
vertices.push_back({x + 0.93f * width, y + (0.11f + skew) * height, 0.5f, 1.0f});
|
||||
vertices.push_back({x + (0.37f - skew * 0.5f) * width, y + 0.91f * height, 0.5f, 1.0f});
|
||||
}
|
||||
return vertices;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target) {
|
||||
static const auto triangles = Triangles();
|
||||
Pixels.fill(std::byte{0});
|
||||
Results.fill(Sentinel);
|
||||
|
||||
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.targetOutputMode[0] = 9;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
std::vector<std::uint32_t> pixelUserData(36, 0u);
|
||||
const auto results = BufferDescriptor(Results.data(), 8u, static_cast<std::uint32_t>(Results.size() / 2u));
|
||||
std::copy(results.begin(), results.end(), pixelUserData.begin() + 4);
|
||||
const auto texture = TextureDescriptor();
|
||||
std::copy(texture.begin(), texture.end(), pixelUserData.begin() + 24);
|
||||
const std::array<std::uint32_t, 4> sampler{0u, 0xfffu << 12, 1u << 26, 0u};
|
||||
std::copy(sampler.begin(), sampler.end(), pixelUserData.begin() + 32);
|
||||
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, {}},
|
||||
{waveSize, 0, pixelUserData, 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, 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();
|
||||
}
|
||||
|
||||
void Check(std::uint32_t waveSize) {
|
||||
const auto what = "pixel helper waterfall wave" + std::to_string(waveSize);
|
||||
std::map<std::uint32_t, std::vector<std::uint32_t>> waves;
|
||||
std::uint32_t partialQuads = 0;
|
||||
for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) {
|
||||
if (Pixels[pixel * 4u + 3u] == std::byte{0}) {
|
||||
Require(Results[pixel * 2u] == Sentinel, what + ": uncovered pixel " + std::to_string(pixel) + " stored a result");
|
||||
continue;
|
||||
}
|
||||
Require(Results[pixel * 2u] != Sentinel, what + ": covered pixel " + std::to_string(pixel) + " stored nothing");
|
||||
waves[Results[pixel * 2u]].push_back(pixel);
|
||||
const auto quad = (pixel / Width & ~1u) * Width + (pixel % Width & ~1u);
|
||||
if (quad == pixel) {
|
||||
for (const auto offset : {1u, Width, Width + 1u}) {
|
||||
if (Pixels[(quad + offset) * 4u + 3u] == std::byte{0}) {
|
||||
++partialQuads;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Require(waves.size() > 4u && partialQuads > 16u, what + ": the triangles made only " + std::to_string(waves.size()) + " waves and " + std::to_string(partialQuads) + " partly covered quads");
|
||||
for (const auto& [first, live] : waves) {
|
||||
std::uint32_t keys = 0;
|
||||
for (const auto pixel : live) keys ^= pixel;
|
||||
const auto passes = static_cast<std::uint32_t>(live.size());
|
||||
Require(passes < PassLimit, what + ": the wave whose first lane is pixel " + std::to_string(first) + " has more live lanes than the pass limit");
|
||||
for (const auto pixel : live) {
|
||||
const auto stored = Results[pixel * 2u + 1u];
|
||||
Require((stored >> 16u) == passes, what + ": pixel " + std::to_string(pixel) + " of the wave whose first lane is pixel " + std::to_string(first) + " made " + std::to_string(stored >> 16u) + " passes over " + std::to_string(passes) + " live lanes");
|
||||
Require((stored & 0xffffu) == keys, what + ": pixel " + std::to_string(pixel) + " of the wave whose first lane is pixel " + std::to_string(first) + " read keys " + std::to_string(stored & 0xffffu) + ", expected " + std::to_string(keys));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (device->Target().subgroupSize < 32u) {
|
||||
std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize);
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
FillTexture();
|
||||
for (const auto waveSize : {64u, 32u}) {
|
||||
for (std::uint32_t pass = 0; pass < 4; ++pass) {
|
||||
Draw(*device, waveSize, device->Target());
|
||||
Check(waveSize);
|
||||
}
|
||||
}
|
||||
std::puts("pixel helper waterfall tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,13 @@ namespace ShaderRecompiler
|
||||
state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), result, own, zero, zero, zero);
|
||||
return;
|
||||
}
|
||||
if (state.program.Resources().stage == IrShaderStage::Pixel) {
|
||||
if (const auto helperVariable = InputVariableForKind(state, StageInputKind::HelperInvocation); helperVariable != 0u) {
|
||||
const auto helper = state.module.AllocateId();
|
||||
state.module.AddFunction(spv::OpLoad, TypeBool(state), helper, helperVariable);
|
||||
predicate = Binary(state, spv::OpLogicalAnd, TypeBool(state), predicate, Unary(state, spv::OpLogicalNot, TypeBool(state), helper));
|
||||
}
|
||||
}
|
||||
state.module.AddSubgroupBallot(TypeU32Vector(state, 4u), result, ConstantU32(state, spv::ScopeSubgroup), predicate);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ public:
|
||||
}
|
||||
void SetEntryRounding(const RoundingState& state) { setRounding(state); }
|
||||
void SetNativeFloat16(bool enabled) { nativeFloat16 = enabled; }
|
||||
void SetWholeQuad(bool entered) { wholeQuad = entered; }
|
||||
void SetFloat32DenormPreserve(bool enabled) { float32DenormPreserve = enabled; }
|
||||
void SetFloat32FlushToZero(bool enabled) { float32FlushToZero = enabled; }
|
||||
void SetFloat32FlushDeclared(bool enabled) { float32FlushDeclared = enabled; }
|
||||
@@ -58,6 +59,7 @@ private:
|
||||
IrU32 applyBitSourceModifiers(const RdnaOperand& operand, IrU32 value);
|
||||
IrValue* readOperand(const RdnaOperand& operand, IrType type);
|
||||
IrU1 threadBit(const std::array<IrU32, 2>& mask);
|
||||
IrU1 execLanes(const std::array<IrU32, 2>& mask);
|
||||
void writeRawU32(const RdnaOperand& operand, IrU32 value);
|
||||
IrF32 applyF32ResultModifiers(const RdnaOperand& operand, IrF32 value);
|
||||
IrF32 scaleF32Result(std::uint32_t omod, IrF32 value);
|
||||
@@ -383,6 +385,7 @@ private:
|
||||
RoundingState rounding;
|
||||
bool ieeeMode = false;
|
||||
bool nativeFloat16 = false;
|
||||
bool wholeQuad = false;
|
||||
IrBuilder ir;
|
||||
IrBlock& block;
|
||||
IrU1 instructionBranchCondition;
|
||||
|
||||
@@ -45,7 +45,7 @@ void TranslationContext::sSubvectorLoop(const RdnaInstruction& inst, bool begin)
|
||||
writeRawU32(inst.destination, newCounter);
|
||||
ir.SetExecLo(newExecLo.Value());
|
||||
ir.SetExecHi(newExecHi.Value());
|
||||
ir.SetExec(threadBit({newExecLo, newExecHi}).Value());
|
||||
ir.SetExec(execLanes({newExecLo, newExecHi}).Value());
|
||||
}
|
||||
|
||||
// s_*_saveexec reads its source before it writes the old exec into the destination, so
|
||||
@@ -84,7 +84,7 @@ void TranslationContext::sSaveexec(const RdnaInstruction& inst, IrOpcode operati
|
||||
if (writeResult) writeU32Pair(inst.destination, {hostExecWord(0u), hostExecWord(1u)});
|
||||
const IrU1 nonZero(ir.LogicalOr(ir.INotEqual(newExecLo.Value(), ir.Constant(0u)), ir.INotEqual(newExecHi.Value(), ir.Constant(0u))));
|
||||
ir.SetScc(nonZero.Value());
|
||||
ir.SetExec(threadBit({newExecLo, newExecHi}).Value());
|
||||
ir.SetExec(execLanes({newExecLo, newExecHi}).Value());
|
||||
return;
|
||||
}
|
||||
const IrU32 oldExec(ir.GetExecLo());
|
||||
@@ -116,7 +116,7 @@ void TranslationContext::sSaveexec(const RdnaInstruction& inst, IrOpcode operati
|
||||
IrValue& combinedLane = ir.Emit(laneOperation, IrType::U1, {&execLane, &sourceLaneOperand});
|
||||
newLane = IrU1(negateResult ? ir.LogicalNot(combinedLane) : combinedLane);
|
||||
} else {
|
||||
newLane = threadBit({newExec, IrU32(ir.GetExecHi())});
|
||||
newLane = execLanes({newExec, IrU32(ir.GetExecHi())});
|
||||
}
|
||||
if (writeResult) writeSaved(hostExecWord(0u), newLane);
|
||||
const IrU1 nonZero(ir.INotEqual(newExec.Value(), ir.Constant(0u)));
|
||||
|
||||
@@ -513,6 +513,7 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con
|
||||
program.SetWaveSize(options.waveSize);
|
||||
std::uint32_t windowBase = NumVectorRegs;
|
||||
bool indexedWrites = false;
|
||||
bool wholeQuad = false;
|
||||
for (const auto& cfgBlock : cfg.blocks) {
|
||||
for (std::uint32_t index = cfgBlock.instructionBegin; index < cfgBlock.instructionEnd; index++) {
|
||||
const auto& instruction = decoded.instructions[index];
|
||||
@@ -531,6 +532,10 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con
|
||||
windowBase = std::min({windowBase, instruction.source0.reg, instruction.destination.reg});
|
||||
indexedWrites = true;
|
||||
break;
|
||||
case RdnaOpcode::SWqmB32:
|
||||
case RdnaOpcode::SWqmB64:
|
||||
wholeQuad = wholeQuad || instruction.destination.kind == RdnaOperandKind::ExecLo || instruction.destination.kind == RdnaOperandKind::ExecHi;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -619,6 +624,7 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con
|
||||
context.SetPixelInput(options.inputInfo.pixel, options.fragmentShaderBarycentricEnabled);
|
||||
context.SetFloatMode(options.floatMode);
|
||||
context.SetNativeFloat16(options.nativeFloat16);
|
||||
context.SetWholeQuad(wholeQuad);
|
||||
context.SetFloat32DenormPreserve(options.float32DenormPreserve);
|
||||
context.SetFloat32FlushToZero(options.float32FlushToZero);
|
||||
context.SetFloat32FlushDeclared(options.float32FlushDeclared);
|
||||
|
||||
@@ -15,14 +15,32 @@ IrU32 TranslationContext::hostExecWord(std::uint32_t half) {
|
||||
if (half != 0u && program.WaveSize() != 64u) {
|
||||
return word;
|
||||
}
|
||||
auto active = ballotMask(IrU1(ir.ConstantBool(true)));
|
||||
if (program.Resources().stage == IrShaderStage::Pixel) {
|
||||
const IrU64 lanes(ir.ConstructU64(active[0].Value(), active[1].Value()));
|
||||
active = extractU64(IrU64(ir.Emit(IrOpcode::WqmU64, IrType::U64, {&lanes.Value()})));
|
||||
}
|
||||
const auto active = ballotMask(IrU1(ir.ConstantBool(true)));
|
||||
return IrU32(ir.BitwiseAnd(word.Value(), active[half].Value()));
|
||||
}
|
||||
|
||||
IrU1 TranslationContext::execLanes(const std::array<IrU32, 2>& mask) {
|
||||
if (program.Resources().stage != IrShaderStage::Pixel || !wholeQuad) return threadBit(mask);
|
||||
if (mask[0].Value().HasImmediate() && mask[1].Value().HasImmediate()) {
|
||||
const std::uint64_t value = (static_cast<std::uint64_t>(mask[1].Value().ImmediateU32()) << 32u) | mask[0].Value().ImmediateU32();
|
||||
std::uint64_t quads = value | (value >> 1u) | (value >> 2u) | (value >> 3u);
|
||||
quads &= 0x1111111111111111ull;
|
||||
if (quads * 0xfull == value) return threadBit(mask);
|
||||
}
|
||||
IrValue& builtin = ir.Emit(IrOpcode::GetBuiltin, IrOpcodeType(IrOpcode::GetBuiltin), {&ir.Constant(static_cast<std::uint32_t>(StageInputKind::HelperInvocation)), &ir.Constant(0u)});
|
||||
const auto live = ballotMask(IrU1(ir.IEqual(builtin, ir.Constant(0u))));
|
||||
const IrU64 liveLanes(ir.ConstructU64(live[0].Value(), live[1].Value()));
|
||||
const auto liveQuads = extractU64(IrU64(ir.Emit(IrOpcode::WqmU64, IrType::U64, {&liveLanes.Value()})));
|
||||
const IrU64 lanes(ir.ConstructU64(mask[0].Value(), mask[1].Value()));
|
||||
const auto quads = extractU64(IrU64(ir.Emit(IrOpcode::WqmU64, IrType::U64, {&lanes.Value()})));
|
||||
std::array<IrU32, 2> words = mask;
|
||||
for (std::size_t half = 0; half < words.size(); ++half) {
|
||||
IrValue& helpers = ir.BitwiseAnd(liveQuads[half].Value(), ir.BitwiseNot(live[half].Value()));
|
||||
words[half] = IrU32(ir.BitwiseOr(mask[half].Value(), ir.BitwiseAnd(quads[half].Value(), helpers)));
|
||||
}
|
||||
return threadBit(words);
|
||||
}
|
||||
|
||||
IrU1 TranslationContext::threadBit(const std::array<IrU32, 2>& mask) {
|
||||
const bool wide = program.WaveSize() == 64u;
|
||||
if (mask[0].Value().HasImmediate() && (!wide || mask[1].Value().HasImmediate())) {
|
||||
|
||||
@@ -567,11 +567,11 @@ void TranslationContext::writeRawU32(const RdnaOperand& operand, IrU32 value) {
|
||||
case RdnaOperandKind::M0: ir.SetM0(value.Value()); break;
|
||||
case RdnaOperandKind::ExecLo:
|
||||
ir.SetExecLo(value.Value());
|
||||
ir.SetExec(threadBit({value, IrU32(ir.GetExecHi())}).Value());
|
||||
ir.SetExec(execLanes({value, IrU32(ir.GetExecHi())}).Value());
|
||||
break;
|
||||
case RdnaOperandKind::ExecHi:
|
||||
ir.SetExecHi(value.Value());
|
||||
ir.SetExec(threadBit({IrU32(ir.GetExecLo()), value}).Value());
|
||||
ir.SetExec(execLanes({IrU32(ir.GetExecLo()), value}).Value());
|
||||
break;
|
||||
case RdnaOperandKind::Scc:
|
||||
ir.SetScc(ir.INotEqual(value.Value(), ir.Constant(0u)));
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
- [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035). How the console routes a flat address was not measured. Its bases, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), first inferred from PPSA04263's traversal addresses, are measured: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) `src_shared_base` / `src_shared_limit` read 0x80000000_00000000 / 0x80000000_FFFFFFFF and `src_private_base` / `src_private_limit` 0x70000000_00000000 / 0x70000000_FFFFFFFF. The scalar aperture sources read the same two values
|
||||
- [Flat accesses outside the aperture routing](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - only compute stages route flat accesses, so graphics-stage flat accesses to the shared or private aperture still go to BDA. In compute, a 64-bit flat atomic whose address is in the shared aperture takes the workgroup lock (`ldsLock`) for its two-dword update, and every such atomic requests that lock, so a shader with one pays for it even on global addresses (the path is exercised by `agc_driver_global_atomics_float`). Inferred, not measured on LDS or scratch: an aperture access that is not inside one dword is done byte by byte, an unaligned aperture atomic records the BDA unaligned fault, and LDS or scratch accesses past their end are dropped or read zero.
|
||||
- On a host subgroup narrower than 32 lanes, [`threadBit`](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit.
|
||||
- [EXEC read as a value](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (`s_mov_b64 sN, exec`, the `s_*_saveexec` copy, ALU sources) is limited to host lanes. Pixel reads retain helper bits in quads with an active fragment, preserving whole-quad masks across copies. Quad expansion follows the [RDNA 2 ISA, section 12.3, S_WQM_B32/B64, page 111](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf#page=119); save/restore followed by implicit-LOD sampling is checked on an RX 6700 XT, not on a console. The console also holds lanes the host lacks, e.g. 32-63 of a wave64 on 32 host lanes.
|
||||
- [EXEC read as a value](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) and every [pixel-stage ballot](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) leave out helper invocations, unlike the console: guest SGPR masks, lane reads and lane counts never include a helper lane, so a waterfall over helper lanes skips them (not measured on a console). Vulkan lets helpers stay out of non-quad subgroup operations, and on an RX 9070 XT they ran no ordinary instructions, so a helper bit in a saved whole-quad EXEC could never be cleared. In a pixel program that runs `s_wqm` on EXEC, writing EXEC from a value keeps a helper active while a live lane of its quad is, for implicit-LOD sampling ([RDNA 2 ISA, S_WQM_B32/B64, page 111](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf#page=119)). EXEC reads also stay within host lanes; the console holds lanes the host lacks (32-63 of a wave64 on 32 host lanes).
|
||||
- [`v_readlane_b32`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) and fetch-inactive `v_permlane16_b32`/`v_permlanex16_b32` read undefined data from a lane that the host subgroup does not hold or whose host invocation is inactive; the console reads the lane's VGPR.
|
||||
- Without fetch-inactive, [`v_permlane16_b32` and `v_permlanex16_b32`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take a source lane that the host subgroup does not hold as inactive and keep the old value (0 with bound_ctrl); the console reads that lane's VGPR.
|
||||
- On a host subgroup narrower than the wave, [`v_readlane_b32`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1) or, in a program whose entry EXEC is every lane (vertex, local and tessellation stages), as 0 when the value is 0 in lanes 32-63; other such reads throw. A value is 0 there when it is built from 0 through selects under a lane index (lane id or `v_mbcnt` of -1) below at most 32, `v_cmp_ne_u32` against 0, OR, DPP and permlanes; the counts in `s3` give such a bound on a 32-lane host. On a 32-lane host a half-wave scan may also run under `s_mov_b64 exec, -1`, use bound_ctrl:1 steps for an OR, add or unsigned max, and take keys that are 0 in lanes 32-63; its lane 31 is then the subgroup reduction of the host's invocations (a shuffle of the scan on a device without subgroup arithmetic): idle invocations add the identity, where the console would add the keys of the lanes `s3` claims.
|
||||
|
||||
Reference in new issue
Block a user