Merge pull request #1179 from MichonGoddijn231849/feat/pixel-ancillary-whole-word
feat(recompiler): pixel ancillary word used whole
This commit is contained in:
commit
0df342c640
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue