Merge pull request #1179 from MichonGoddijn231849/feat/pixel-ancillary-whole-word

feat(recompiler): pixel ancillary word used whole
This commit is contained in:
Adrià 2026-10-10 00:56:51 +02:00 committed by GitHub
commit 0df342c640
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 214 additions and 40 deletions

View File

@ -2,25 +2,54 @@
#include "Optimization/ConstantFolder.hpp"
#include "Optimization/DeadCodeEliminator.hpp"
#include "Optimization/ShaderInfoCollector.hpp"
#include <array>
#include <cstdint>
#include <stdexcept>
#include <string>
using namespace ShaderRecompiler;
static void Require(bool value) { if (!value) throw std::runtime_error("packed pixel ancillary regression"); }
static IrValue& Build(IrProgram& program, IrOpcode opcode, std::uint32_t offset, std::uint32_t count) {
constexpr std::uint32_t UnmodelledBits = 0xE000F0FFu;
constexpr std::uint32_t SelectOther = 0x5A3C9E17u;
struct Row {
std::uint32_t sample;
std::uint32_t layer;
bool frontFacing;
};
constexpr std::array<Row, 6> Rows {{{0u, 0u, false}, {0u, 0u, true}, {15u, 0x1FFFu, false}, {15u, 0x1FFFu, true}, {0u, 0x1FFFu, true}, {15u, 0u, false}}};
static IrBlock& Begin(IrProgram& program) {
program.Resources().stage = IrShaderStage::Pixel;
program.Resources().resourceTrackingComplete = true;
auto& block = program.CreateBlock();
program.SetEntryBlock(block);
program.BlockOrder().push_back(&block);
IrBuilder builder(program);
builder.SetInsertionPoint(block);
auto& ancillary = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast<std::uint32_t>(StageInputKind::PackedAncillary)), &builder.Constant(0u)});
auto& user = opcode == IrOpcode::BitwiseOr32 ? builder.Emit(opcode, IrType::U32, {&ancillary, &builder.Constant(offset)})
: builder.Emit(opcode, IrType::U32, {&ancillary, &builder.Constant(offset), &builder.Constant(count)});
return block;
}
static IrValue& AncillaryBuiltin(IrBuilder& builder) {
return builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast<std::uint32_t>(StageInputKind::PackedAncillary)), &builder.Constant(0u)});
}
static IrValue& FrontFacingBuiltin(IrBuilder& builder) {
return builder.Emit(IrOpcode::GetBuiltin, IrType::U1, {&builder.Constant(static_cast<std::uint32_t>(StageInputKind::FrontFacing)), &builder.Constant(0u)});
}
static void End(IrBuilder& builder, IrValue& user) {
static_cast<void>(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&user}));
static_cast<void>(builder.Emit(IrOpcode::Return, IrType::Void, {}));
}
static IrValue& Build(IrProgram& program, IrOpcode opcode, std::uint32_t offset, std::uint32_t count, bool throughSelect = false) {
auto& block = Begin(program);
IrBuilder builder(program);
builder.SetInsertionPoint(block);
IrValue& ancillary = AncillaryBuiltin(builder);
IrValue* source = &ancillary;
if (throughSelect) {
source = &builder.Emit(IrOpcode::SelectU32, IrType::U32, {&FrontFacingBuiltin(builder), &ancillary, &builder.Constant(SelectOther)});
}
IrValue& user = opcode == IrOpcode::BitwiseOr32 ? builder.Emit(opcode, IrType::U32, {source, &builder.Constant(offset)})
: builder.Emit(opcode, IrType::U32, {source, &builder.Constant(offset), &builder.Constant(count)});
End(builder, user);
return user;
}
static void Lower(IrProgram& program) {
@ -40,9 +69,9 @@ static void Extract(IrOpcode opcode, std::uint32_t offset, std::uint32_t count,
Require(user.Argument(1)->Resolve()->ImmediateU32() == fieldOffset);
Require(user.Argument(2)->Resolve()->ImmediateU32() == count);
}
static void Refused(IrOpcode opcode, std::uint32_t offset, std::uint32_t count) {
static void Refused(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, bool throughSelect = false) {
IrProgram program;
static_cast<void>(Build(program, opcode, offset, count));
static_cast<void>(Build(program, opcode, offset, count, throughSelect));
try {
Lower(program);
} catch (const std::runtime_error& error) {
@ -51,14 +80,96 @@ static void Refused(IrOpcode opcode, std::uint32_t offset, std::uint32_t count)
}
Require(false);
}
static std::uint32_t BitField(std::uint32_t word, std::uint32_t offset, std::uint32_t count, bool isSigned) {
const std::uint32_t mask = count == 32u ? ~0u : (1u << count) - 1u;
std::uint32_t bits = (word >> offset) & mask;
if (isSigned && ((bits >> (count - 1u)) & 1u) != 0u) {
bits |= ~mask;
}
return bits;
}
static std::uint32_t Evaluate(const IrValue* value, const Row& row) {
value = value->Resolve();
if (value->HasImmediate()) {
return value->ImmediateU32();
}
switch (value->Opcode()) {
case IrOpcode::GetBuiltin:
switch (static_cast<StageInputKind>(Evaluate(value->Argument(0), row))) {
case StageInputKind::SampleId: return row.sample;
case StageInputKind::Layer: return row.layer;
case StageInputKind::FrontFacing: return row.frontFacing ? 1u : 0u;
default: break;
}
break;
case IrOpcode::ShiftLeftLogical32: return Evaluate(value->Argument(0), row) << Evaluate(value->Argument(1), row);
case IrOpcode::BitwiseOr32: return Evaluate(value->Argument(0), row) | Evaluate(value->Argument(1), row);
case IrOpcode::BitFieldUExtract:
case IrOpcode::BitFieldSExtract:
return BitField(Evaluate(value->Argument(0), row), Evaluate(value->Argument(1), row), Evaluate(value->Argument(2), row), value->Opcode() == IrOpcode::BitFieldSExtract);
case IrOpcode::SelectU32: return Evaluate(value->Argument(0), row) != 0u ? Evaluate(value->Argument(1), row) : Evaluate(value->Argument(2), row);
default: break;
}
throw std::runtime_error("packed pixel ancillary evaluation reached an unsupported value");
}
static std::uint32_t PackedWord(const Row& row) {
return UnmodelledBits | (row.sample << 8u) | (row.layer << 16u);
}
static void Values(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, bool throughSelect) {
IrProgram program;
auto& user = Build(program, opcode, offset, count, throughSelect);
Lower(program);
for (const Row& row : Rows) {
const std::uint32_t word = throughSelect && !row.frontFacing ? SelectOther : PackedWord(row);
Require(Evaluate(&user, row) == BitField(word, offset, count, opcode == IrOpcode::BitFieldSExtract));
}
}
static void LateFold() {
IrProgram program;
auto& block = Begin(program);
IrBuilder builder(program);
builder.SetInsertionPoint(block);
IrValue& ancillary = AncillaryBuiltin(builder);
IrValue& offset = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&builder.Constant(16u), &builder.Constant(31u)});
IrValue& count = builder.Emit(IrOpcode::UMin32, IrType::U32, {&builder.Constant(11u), &builder.Constant(13u)});
IrValue& user = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&ancillary, &offset, &count});
End(builder, user);
Lower(program);
const IrValue* field = user.Argument(0)->Resolve();
Require(field->Opcode() == IrOpcode::GetBuiltin);
Require(static_cast<StageInputKind>(field->Argument(0)->Resolve()->ImmediateU32()) == StageInputKind::Layer);
Require(user.Argument(1)->Resolve()->ImmediateU32() == 0u);
Require(user.Argument(2)->Resolve()->ImmediateU32() == 11u);
for (const Row& row : Rows) {
Require(Evaluate(&user, row) == BitField(PackedWord(row), 16u, 11u, false));
}
}
int main() {
Extract(IrOpcode::BitFieldUExtract, 8u, 4u, StageInputKind::SampleId, 0u);
Extract(IrOpcode::BitFieldUExtract, 9u, 2u, StageInputKind::SampleId, 1u);
Extract(IrOpcode::BitFieldUExtract, 16u, 13u, StageInputKind::Layer, 0u);
Extract(IrOpcode::BitFieldSExtract, 20u, 9u, StageInputKind::Layer, 4u);
Values(IrOpcode::BitFieldUExtract, 8u, 4u, false);
Values(IrOpcode::BitFieldSExtract, 8u, 4u, false);
Values(IrOpcode::BitFieldUExtract, 9u, 2u, false);
Values(IrOpcode::BitFieldUExtract, 16u, 13u, false);
Values(IrOpcode::BitFieldSExtract, 20u, 9u, false);
Values(IrOpcode::BitFieldUExtract, 28u, 1u, false);
LateFold();
Values(IrOpcode::BitFieldUExtract, 8u, 4u, true);
Values(IrOpcode::BitFieldUExtract, 16u, 13u, true);
Values(IrOpcode::BitFieldSExtract, 20u, 9u, true);
Refused(IrOpcode::BitwiseOr32, 1u, 0u);
Refused(IrOpcode::BitwiseOr32, 1u, 0u, true);
Refused(IrOpcode::BitFieldUExtract, 2u, 4u);
Refused(IrOpcode::BitFieldUExtract, 10u, 4u);
Refused(IrOpcode::BitFieldUExtract, 13u, 2u);
Refused(IrOpcode::BitFieldUExtract, 16u, 14u);
Refused(IrOpcode::BitFieldUExtract, 0u, 2u);
Refused(IrOpcode::BitFieldUExtract, 7u, 1u);
Refused(IrOpcode::BitFieldUExtract, 12u, 4u);
Refused(IrOpcode::BitFieldUExtract, 29u, 1u);
Refused(IrOpcode::BitFieldUExtract, 8u, 0u);
Refused(IrOpcode::BitFieldUExtract, 0u, 2u, true);
Refused(IrOpcode::BitFieldSExtract, 12u, 4u, true);
}

View File

@ -5,6 +5,7 @@
#include <array>
#include <bit>
#include <cstdint>
#include <optional>
#include <utility>
#include <vector>
@ -184,23 +185,64 @@ 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);
bool forwardsWord(const IrUse& use) {
return use.user->Opcode() == IrOpcode::Identity || (use.user->Opcode() == IrOpcode::SelectU32 && use.operand != 0u);
}
void collectReaders(IrValue& value, std::vector<IrUse>& readers) {
const std::vector<IrUse> uses = value.OperandUses();
for (const IrUse& use : uses) {
if (forwardsWord(use)) {
collectReaders(*use.user, readers);
} else {
uses.push_back(use);
readers.push_back(use);
}
}
}
bool lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancillary) {
constexpr std::array<std::pair<std::uint32_t, std::uint32_t>, 2> packedFields {{{8u, 4u}, {16u, 13u}}};
std::optional<std::size_t> packedField(std::uint32_t offset, std::uint32_t count) {
const auto field = std::ranges::find_if(packedFields, [&](const auto& candidate) {
return offset >= candidate.first && count <= candidate.second && offset - candidate.first <= candidate.second - count;
});
if (field == packedFields.end()) {
return std::nullopt;
}
return static_cast<std::size_t>(field - packedFields.begin());
}
void 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;
return;
}
const std::vector<IrUse> uses = ancillary.OperandUses();
std::vector<IrUse> direct;
std::vector<IrUse> forwarded;
for (const IrUse& use : uses) {
if (forwardsWord(use)) {
forwarded.push_back(use);
} else {
direct.push_back(use);
}
}
std::vector<IrUse> readers = direct;
for (const IrUse& use : forwarded) {
collectReaders(*use.user, readers);
}
for (const IrUse& use : readers) {
const IrValue& user = *use.user;
if ((user.Opcode() != IrOpcode::BitFieldUExtract && user.Opcode() != IrOpcode::BitFieldSExtract) || use.operand != 0u) {
return;
}
auto& offset = resolveArg(user, 1);
auto& count = resolveArg(user, 2);
if (!isImmediate(offset, IrType::U32) || !isImmediate(count, IrType::U32) || count.ImmediateU32() == 0u ||
!packedField(offset.ImmediateU32(), count.ImmediateU32())) {
return;
}
}
std::vector<IrUse> uses;
collectUses(ancillary, uses);
std::array<IrValue*, 2> fields {};
const auto field = [&](std::size_t index) -> IrValue& {
if (fields[index] == nullptr) {
@ -213,30 +255,29 @@ bool lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancil
}
return *fields[index];
};
constexpr std::array<std::pair<std::uint32_t, std::uint32_t>, 2> ranges {{{8u, 4u}, {16u, 13u}}};
bool lowered = false;
for (const IrUse& use : uses) {
IrValue& user = *use.user;
if ((user.Opcode() != IrOpcode::BitFieldUExtract && user.Opcode() != IrOpcode::BitFieldSExtract) || use.operand != 0u) {
continue;
const auto emit = [&](IrOpcode opcode, IrValue& lhs, IrValue& rhs) -> IrValue& {
IrValue& created = program.CreateValue(opcode, IrType::U32);
created.AddArgument(&lhs);
created.AddArgument(&rhs);
ancillary.Parent()->InsertInstructionBefore(&ancillary, &created);
return created;
};
if (!forwarded.empty()) {
IrValue& sample = emit(IrOpcode::ShiftLeftLogical32, field(0u), builder.Constant(8u));
IrValue& layer = emit(IrOpcode::ShiftLeftLogical32, field(1u), builder.Constant(16u));
IrValue& packed = emit(IrOpcode::BitwiseOr32, sample, layer);
for (const IrUse& use : forwarded) {
use.user->ReplaceArgument(use.operand, &packed);
}
}
for (const IrUse& use : direct) {
IrValue& user = *use.user;
auto& offset = resolveArg(user, 1);
auto& count = resolveArg(user, 2);
if (!isImmediate(offset, IrType::U32) || !isImmediate(count, IrType::U32) || count.ImmediateU32() == 0u) {
continue;
}
const auto range = std::ranges::find_if(ranges, [&](const auto& candidate) {
return offset.ImmediateU32() >= candidate.first && count.ImmediateU32() <= candidate.second &&
offset.ImmediateU32() - candidate.first <= candidate.second - count.ImmediateU32();
});
if (range == ranges.end()) {
continue;
}
user.ReplaceArgument(0, &field(static_cast<std::size_t>(range - ranges.begin())));
user.ReplaceArgument(1, &builder.Constant(offset.ImmediateU32() - range->first));
lowered = true;
const auto index = *packedField(offset.ImmediateU32(), count.ImmediateU32());
user.ReplaceArgument(0, &field(index));
user.ReplaceArgument(1, &builder.Constant(offset.ImmediateU32() - packedFields[index].first));
}
return lowered;
}
} // namespace
@ -245,7 +286,6 @@ 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);
@ -652,6 +692,18 @@ void ConstantFolder::Fold(IrProgram& program, std::span<IrBlock* const> blocks)
const auto success = tryFoldValue(program, *inst);
}
}
std::vector<IrValue*> words;
for (IrBlock* block : blocks) {
for (IrValue* inst : block->Instructions()) {
if (inst->Opcode() == IrOpcode::GetBuiltin) {
words.push_back(inst);
}
}
}
IrBuilder builder(program);
for (IrValue* word : words) {
lowerPackedAncillary(program, builder, *word);
}
}
void ConstantFolder::Fold(IrProgram& program) const {

View File

@ -129,8 +129,18 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu
const auto kind = static_cast<StageInputKind>(kindValue->ImmediateU32());
const auto component = componentValue->ImmediateU32();
switch (kind) {
case StageInputKind::PackedAncillary:
return Fail("packed pixel ancillary input has an unsupported live use");
case StageInputKind::PackedAncillary: {
std::string uses;
for (const IrValue* user : inst->Uses()) {
uses += uses.empty() ? " (used by " : ", ";
uses += IrOpcodeName(user->Opcode());
for (std::size_t i = 1; i < user->ArgumentCount(); ++i) {
const IrValue* argument = user->Argument(i)->Resolve();
uses += argument->HasImmediate() && argument->Type() == IrType::U32 ? " " + std::to_string(argument->ImmediateU32()) : " ?";
}
}
return Fail("packed pixel ancillary input has an unsupported live use" + uses + (uses.empty() ? "" : ")"));
}
case StageInputKind::Layer:
case StageInputKind::SampleId:
if (program.Resources().stage != IrShaderStage::Pixel || component != 0u) {

View File

@ -41,6 +41,7 @@ Throughout the project, every function at every stage either **does exactly what
### Unknown function info
- [getuid, geteuid, getgid, getegid and issetugid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) report the root identity (0) and a process that never changed credentials (`issetugid` 0), as a jailbroken process has; the host user and group are not exposed to the guest. The values a title sees on a console are unverified
- [Packed pixel ancillary word](../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295, and their equality with the SampleId and Layer builtins is inferred, not measured. A value passed through a select or move is rebuilt from those two fields with bits 0-7, 12-15 and 29-31 zero. That is exact only because every read of the word must be an extract inside one modelled field; any other read throws. The other bits are unmodelled and their RDNA 2 layout is unknown: the RDNA 3.5 ISA is reported to give prim type at bits 0-1, VRS rates at bits 2-5 and eye id at bit 7, which was not checked against RDNA 2
- [sceNpEntitlementAccessGetEntitlementKey](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified
- [sceNpEntitlementAccess{Request,Poll}{Unified,Service}EntitlementInfoList](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - signatures unknown; no reference implements them, so they are declared without parameters. Like the consume requests in the same library, they report the signed-out NP state with `SCE_NP_ERROR_SIGNED_OUT` (`0x80550006`) and write nothing. PPSA15657 calls the unified request during start-up and continues past the error. Whether the console fails at the request or at the poll when signed out is unverified
- [sceRudpGetStatus](../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`; the status layout and size rules are unknown, so the buffer is zeroed like `sceRudpGetContextStatus` (state 0 = idle, no connections) and the size is not validated. It returns `RUDP_ERROR_NOT_INITIALIZED` before initialization and 0 otherwise, as `sceRudpGetContextStatus` does. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) declares it without arguments and returns success without writing status