Compare commits

...

7 Commits

Author SHA1 Message Date
YuhiAida e500333bcd fix(agc): implement AgcDcbCopyDataGetSize (#410)
Register b5u0Jzm8TF8 for SpongeBob PPSA26893 using the native zero-argument, uint64_t size-helper ABI.

Adapted from PR #410.
2026-08-30 13:59:16 +04:00
nmzik 4498d75b9b add agc ver 13 2026-08-30 13:29:49 +04:00
Brandon Strong 1df53b169f kernel: stop sign-extending batch-map protection and type (#405) 2026-08-30 01:46:43 +04:00
Mehmet Cambaz ff61c1cecd shader: implement V_SIN_F16 (#399)
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-08-30 01:32:49 +04:00
nmzik 60a01817ce loader: add support for etaHEN cheats 2026-08-29 20:48:05 +04:00
nmzik 1a87cfedb5 shader: implement V_COS_F16 2026-08-29 19:23:19 +04:00
nmzik 5bd447d2a2 shader: implement DS_BPERMUTE_B32
Co-authored-by: Techx3 <56268269+Techx3@users.noreply.github.com>
2026-08-29 19:23:19 +04:00
25 changed files with 713 additions and 217 deletions
@@ -266,7 +266,8 @@ bool AnalyzeProgramRequirements(IR::Program& program, std::string* error) {
requirements.subgroup_local_invocation_id = true;
break;
}
case IR::ValueOpcode::SwizzleU32: {
case IR::ValueOpcode::SwizzleU32:
case IR::ValueOpcode::BpermuteU32: {
MarkBallot();
requirements.subgroup_shuffle = true;
requirements.subgroup_local_invocation_id = true;
@@ -30,6 +30,19 @@ uint32_t AndCondition(EmitterState& state, uint32_t lhs, uint32_t rhs) {
return Binary(state, OpLogicalAnd, TypeBool(state), lhs, rhs);
}
uint32_t EmitDsMaskedLaneRead(EmitterState& state, uint32_t source, uint32_t target,
uint32_t exec) {
const auto shuffled = state.builder.AllocateId();
state.builder.AddFunction({OpGroupNonUniformShuffle, TypeU32(state), shuffled,
ConstantU32(state, ScopeSubgroup), source, target});
const auto source_exec = state.builder.AllocateId();
state.builder.AddFunction({OpGroupNonUniformShuffle, TypeBool(state), source_exec,
ConstantU32(state, ScopeSubgroup), exec, target});
const auto source_active =
AndCondition(state, source_exec, EmitSubgroupLaneActiveBool(state, target));
return Select(state, TypeU32(state), source_active, shuffled, ConstantU32(state, 0));
}
uint32_t BufferByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
uint32_t index, uint32_t offset, uint32_t soffset) {
auto& state = ctx.state;
@@ -1181,24 +1194,27 @@ bool EmitValueMemory(ValueEmitContext& ctx, const IR::Inst& inst) {
ctx.Define(inst, AppendConsume(ctx, inst, op == IR::ValueOpcode::DataAppend));
return true;
}
if (op == IR::ValueOpcode::SwizzleU32) {
ctx.state.builder.AddFunction({OpStore, ctx.scratch_u32_variable, ctx.Arg(inst, 0)});
const auto source = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(
{OpLoad, TypeU32(ctx.state), source, ctx.scratch_u32_variable});
const auto target =
EmitDsSwizzleTargetLane(state, EmitSubgroupLocalInvocationId(state),
inst.Arg(1).IsImmediate() ? inst.Arg(1).U32() : 0);
const auto shuffled = state.builder.AllocateId();
state.builder.AddFunction({OpGroupNonUniformShuffle, TypeU32(state), shuffled,
ConstantU32(state, ScopeSubgroup), source, target});
const auto source_exec = state.builder.AllocateId();
state.builder.AddFunction({OpGroupNonUniformShuffle, TypeBool(state), source_exec,
ConstantU32(state, ScopeSubgroup), ctx.Arg(inst, 2), target});
const auto source_active =
AndCondition(state, source_exec, EmitSubgroupLaneActiveBool(state, target));
ctx.Define(inst,
Select(state, TypeU32(state), source_active, shuffled, ConstantU32(state, 0)));
if (op == IR::ValueOpcode::SwizzleU32 || op == IR::ValueOpcode::BpermuteU32) {
uint32_t source = ctx.Arg(inst, 0);
uint32_t target = 0;
if (op == IR::ValueOpcode::SwizzleU32) {
state.builder.AddFunction({OpStore, ctx.scratch_u32_variable, source});
source = state.builder.AllocateId();
state.builder.AddFunction({OpLoad, TypeU32(state), source, ctx.scratch_u32_variable});
target = EmitDsSwizzleTargetLane(state, EmitSubgroupLocalInvocationId(state),
inst.Arg(1).IsImmediate() ? inst.Arg(1).U32() : 0);
} else {
const auto index = Binary(
state, OpBitwiseAnd, TypeU32(state),
Binary(state, OpShiftRightLogical, TypeU32(state), ctx.Arg(inst, 1),
ConstantU32(state, 2)),
ConstantU32(state, 31));
const auto base =
Binary(state, OpBitwiseAnd, TypeU32(state), EmitSubgroupLocalInvocationId(state),
ConstantU32(state, ~31u));
target = Binary(state, OpBitwiseOr, TypeU32(state), base, index);
}
ctx.Define(inst, EmitDsMaskedLaneRead(state, source, target, ctx.Arg(inst, 2)));
return true;
}
return false;
@@ -107,6 +107,7 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
{0xa6u, Opcode::DS_READ_U16_D16, 1, 16},
{0xa7u, Opcode::DS_READ_U16_D16_HI, 1, 16},
{0xb0u, Opcode::DS_WRITE_ADDTID_B32, 1, 32}, {0xb1u, Opcode::DS_READ_ADDTID_B32, 1, 32},
{0xb3u, Opcode::DS_BPERMUTE_B32, 1, 32},
{0xdeu, Opcode::DS_WRITE_B96, 3, 32}, {0xdfu, Opcode::DS_WRITE_B128, 4, 32},
{0xfeu, Opcode::DS_READ_B96, 3, 32}, {0xffu, Opcode::DS_READ_B128, 4, 32},
};
@@ -195,6 +196,7 @@ uint32_t DsSourceCount(Opcode opcode) {
case Opcode::DS_WRITE2ST64_B64:
case Opcode::DS_MIN_F32:
case Opcode::DS_MAX_F32: return 3u;
case Opcode::DS_BPERMUTE_B32: return 2u;
case Opcode::DS_READ_ADDTID_B32:
case Opcode::DS_CONSUME:
case Opcode::DS_APPEND: return 0u;
@@ -395,9 +397,10 @@ void DecodeDs(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
SetUnsupported(inst, Family::DS, opcode, "DS swizzle FFT mode is not implemented");
}
if (inst.gds &&
(inst.opcode == Opcode::DS_SWIZZLE_B32 || inst.opcode == Opcode::DS_WRITE_ADDTID_B32 ||
(inst.opcode == Opcode::DS_SWIZZLE_B32 || inst.opcode == Opcode::DS_BPERMUTE_B32 ||
inst.opcode == Opcode::DS_WRITE_ADDTID_B32 ||
inst.opcode == Opcode::DS_READ_ADDTID_B32)) {
SetUnsupported(inst, Family::DS, opcode, "DS swizzle/addtid is available only for LDS");
SetUnsupported(inst, Family::DS, opcode, "DS lane operation is available only for LDS");
}
if (inst.opcode == Opcode::DS_WRITE_ADDTID_B32 && data1 != 0u) {
SetUnsupported(inst, Family::DS, opcode,
@@ -661,6 +661,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::DS_MIN_F32:
case Opcode::DS_MAX_F32:
case Opcode::DS_SWIZZLE_B32:
case Opcode::DS_BPERMUTE_B32:
case Opcode::DS_READ_I8:
case Opcode::DS_READ_U8:
case Opcode::DS_READ_I16:
@@ -179,6 +179,8 @@ enum class Opcode {
V_CEIL_F16,
V_TRUNC_F16,
V_RNDNE_F16,
V_SIN_F16,
V_COS_F16,
V_SIN_F32,
V_COS_F32,
V_NOT_B32,
@@ -492,6 +494,7 @@ enum class Opcode {
DS_MIN_F32,
DS_MAX_F32,
DS_SWIZZLE_B32,
DS_BPERMUTE_B32,
DS_CONSUME,
DS_APPEND,
DS_READ_I8,
@@ -137,6 +137,8 @@ constexpr OpcodeMap VOP1_OPCODE_LIST[] = {
{0x5cu, Opcode::V_CEIL_F16},
{0x5du, Opcode::V_TRUNC_F16},
{0x5eu, Opcode::V_RNDNE_F16},
{0x60u, Opcode::V_SIN_F16},
{0x61u, Opcode::V_COS_F16},
};
constexpr auto VOP1_OPS = Detail::MakeOpcodeTable<0x100>(VOP1_OPCODE_LIST);
@@ -188,6 +190,8 @@ constexpr OpcodeMap VOP3_ENCODED_VOP1_OPCODE_LIST[] = {
{0x5cu, Opcode::V_CEIL_F16},
{0x5du, Opcode::V_TRUNC_F16},
{0x5eu, Opcode::V_RNDNE_F16},
{0x60u, Opcode::V_SIN_F16},
{0x61u, Opcode::V_COS_F16},
};
constexpr auto VOP3_ENCODED_VOP1_OPS = Detail::MakeOpcodeTable<0x80>(VOP3_ENCODED_VOP1_OPCODE_LIST);
@@ -482,6 +486,8 @@ bool IsVop1FloatSourceOpcode(Opcode opcode) {
case Opcode::V_CEIL_F16:
case Opcode::V_TRUNC_F16:
case Opcode::V_RNDNE_F16:
case Opcode::V_SIN_F16:
case Opcode::V_COS_F16:
case Opcode::V_SIN_F32:
case Opcode::V_COS_F32: return true;
default: return false;
@@ -548,6 +554,10 @@ constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = {
SdwaSelWords() | SdwaSelFull(), false},
{Opcode::V_RNDNE_F16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelWords() | SdwaSelFull(), false},
{Opcode::V_SIN_F16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelWords() | SdwaSelFull(), true},
{Opcode::V_COS_F16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelWords() | SdwaSelFull(), true},
{Opcode::V_CVT_U32_F32, SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(), SdwaSelFull(), false},
{Opcode::V_CVT_I32_F32, SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(), SdwaSelFull(), false},
{Opcode::V_CVT_RPI_I32_F32, SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(), SdwaSelFull(),
@@ -832,6 +842,8 @@ bool IsVop1FloatResultOpcode(Opcode opcode) {
case Opcode::V_RSQ_F16:
case Opcode::V_LOG_F16:
case Opcode::V_EXP_F16:
case Opcode::V_SIN_F16:
case Opcode::V_COS_F16:
case Opcode::V_SIN_F32:
case Opcode::V_COS_F32: return true;
default: return false;
@@ -63,6 +63,42 @@ bool Translator::Float16Unary(const Decoder::Instruction& inst, IR::ValueOpcode
return true;
}
bool Translator::Float16Trig(const Decoder::Instruction& inst, IR::ValueOpcode opcode) {
const auto argument = ReadF16AsF32(inst.src0);
const auto magnitude =
ir.BitwiseAnd(ir.BitCastU32(argument), IR::U32(IR::Value(0x7fffffffu)));
auto result = IR::F32(ir.Emit(opcode, {argument}));
if (opcode == IR::ValueOpcode::FPSin) {
const auto fraction = IR::F32(ir.Emit(IR::ValueOpcode::FPFract32, {argument}));
const auto whole = IR::U1(
ir.Emit(IR::ValueOpcode::FPOrdEqual32, {fraction, IR::Value::F32(0.0f)}));
const auto half = IR::U1(
ir.Emit(IR::ValueOpcode::FPOrdEqual32, {fraction, IR::Value::F32(0.5f)}));
const auto nonzero = ir.INotEqual(magnitude, IR::U32(IR::Value(0u)));
const auto cardinal = ir.LogicalOr(ir.LogicalAnd(whole, nonzero), half);
result = IR::F32(ir.Emit(
IR::ValueOpcode::SelectF32, {cardinal, IR::Value::F32(0.0f), result}));
} else {
// The architectural quarter-cycle result is exactly zero. Evaluating cos(pi/2) with a
// rounded F32 PI can otherwise produce a half-precision subnormal instead.
const auto fraction = IR::F32(ir.Emit(IR::ValueOpcode::FPFract32, {argument}));
const auto quarter = IR::U1(
ir.Emit(IR::ValueOpcode::FPOrdEqual32, {fraction, IR::Value::F32(0.25f)}));
const auto three_quarters = IR::U1(
ir.Emit(IR::ValueOpcode::FPOrdEqual32, {fraction, IR::Value::F32(0.75f)}));
result = IR::F32(
ir.Emit(IR::ValueOpcode::SelectF32,
{ir.LogicalOr(quarter, three_quarters), IR::Value::F32(0.0f), result}));
}
const auto infinite = ir.IEqual(magnitude, IR::U32(IR::Value(0x7f800000u)));
result = ApplyF32ResultModifiers(inst.dst, result);
const auto bits = PackHalf2x16(result, IR::F32(IR::Value::F32(0.0f)));
const auto invalid = IR::U32(IR::Value(inst.dst.clamp ? 0u : 0xfe00u));
WriteU16(DestinationOperand(inst), ir.Select(infinite, invalid, bits));
return true;
}
bool Translator::Float16Binary(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool reverse) {
const auto lhs = ReadF16AsF32(reverse ? inst.src1 : inst.src0);
@@ -878,6 +878,13 @@ bool Translator::DS_SWIZZLE_B32(const Decoder::Instruction& inst) {
return true;
}
bool Translator::DS_BPERMUTE_B32(const Decoder::Instruction& inst) {
const auto address = ir.IAdd(ReadU32(inst.src0), IR::U32(IR::Value(inst.offset)));
WriteOperand(inst.dst, ir.Emit(IR::ValueOpcode::BpermuteU32,
{ReadU32(inst.src1), address, ir.GetExec()}));
return true;
}
bool Translator::EmitMemory(const Decoder::Instruction& inst, std::string* error) {
switch (inst.opcode) {
case Decoder::Opcode::S_LOAD_DWORD:
@@ -1036,6 +1043,7 @@ bool Translator::EmitMemory(const Decoder::Instruction& inst, std::string* error
case Decoder::Opcode::DS_MAX_F32:
return DS_MINMAX_F32(inst, IR::ValueOpcode::SharedAtomicFMax32);
case Decoder::Opcode::DS_SWIZZLE_B32: return DS_SWIZZLE_B32(inst);
case Decoder::Opcode::DS_BPERMUTE_B32: return DS_BPERMUTE_B32(inst);
case Decoder::Opcode::DS_CONSUME:
return DS_APPEND_CONSUME(inst, IR::ValueOpcode::DataConsume);
case Decoder::Opcode::DS_APPEND:
@@ -109,6 +109,7 @@ private:
bool DS_APPEND_CONSUME(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
bool DS_ADDTID(const Decoder::Instruction& inst, bool write);
bool DS_SWIZZLE_B32(const Decoder::Instruction& inst);
bool DS_BPERMUTE_B32(const Decoder::Instruction& inst);
IR::F32 SelectF32(IR::U1 condition, IR::F32 true_value, IR::F32 false_value);
IR::U32 ConvertF32ToU32Saturated(IR::F32 value, float upper_bound, float safe_upper,
@@ -148,6 +149,7 @@ private:
bool quiet_snan);
bool Float16Unary(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool invalid_negative);
bool Float16Trig(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
bool Float16Binary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool reverse);
bool Float16Ternary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool accumulator,
bool mix);
@@ -365,6 +365,8 @@ bool Translator::EmitVector(const Decoder::Instruction& inst, std::string* error
case O::V_CEIL_F16: return Float16Unary(inst, IR::ValueOpcode::FPCeil32, false);
case O::V_TRUNC_F16: return Float16Unary(inst, IR::ValueOpcode::FPTrunc32, false);
case O::V_RNDNE_F16: return Float16Unary(inst, IR::ValueOpcode::FPRoundEven32, false);
case O::V_SIN_F16: return Float16Trig(inst, IR::ValueOpcode::FPSin);
case O::V_COS_F16: return Float16Trig(inst, IR::ValueOpcode::FPCos);
case O::V_MIN3_F16: return Float16Ternary(inst, IR::ValueOpcode::FPMinTri32, false, false);
case O::V_MAX3_F16: return Float16Ternary(inst, IR::ValueOpcode::FPMaxTri32, false, false);
case O::V_MED3_F16: return Float16Ternary(inst, IR::ValueOpcode::FPMedTri32, false, false);
@@ -188,6 +188,7 @@ VALUE_OPCODE(ReadFirstLane, U32, U32, U1)
VALUE_OPCODE(ReadLane, U32, U32, U32)
VALUE_OPCODE(WriteLane, U32, U32, U32, U32)
VALUE_OPCODE(Permlane16U32, U32, U32, U32, U32, U1)
VALUE_OPCODE(BpermuteU32, U32, U32, U32, U1)
// Typed shader resources and memory operations. Descriptor state is represented by SSA values;
// dense bindings are assigned by resource tracking.
+2 -2
View File
@@ -3804,8 +3804,8 @@ int KYTY_SYSV_ABI KernelBatchMap2(KernelBatchMapEntry* entries, int num_entries,
LOGF("\t [%d] start = %p, offset = 0x%016" PRIx64 ", length = 0x%016" PRIx64
", prot = 0x%02" PRIx32 ", type = 0x%02" PRIx32 ", op = %d\n",
i, entry->start, entry->offset, entry->length,
static_cast<uint32_t>(static_cast<unsigned char>(entry->protection)),
static_cast<uint32_t>(static_cast<unsigned char>(entry->type)), entry->operation);
static_cast<uint32_t>(entry->protection), static_cast<uint32_t>(entry->type),
entry->operation);
if (entry->length == 0 || entry->operation < MAP_OP_MAP_DIRECT ||
entry->operation > MAP_OP_TYPE_PROTECT) {
+7 -7
View File
@@ -47,13 +47,13 @@ struct VirtualQueryInfo {
static_assert(sizeof(VirtualQueryInfo) == 72, "VirtualQueryInfo struct size is incorrect");
struct KernelBatchMapEntry {
void* start;
uint64_t offset;
uint64_t length;
char protection;
char type;
int16_t reserved;
int32_t operation;
void* start;
uint64_t offset;
uint64_t length;
unsigned char protection;
unsigned char type;
int16_t reserved;
int32_t operation;
};
static_assert(sizeof(KernelBatchMapEntry) == 32, "KernelBatchMapEntry struct size is incorrect");
+1 -1
View File
@@ -889,7 +889,7 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
style()->standardIcon(QStyle::SP_FileDialogContentsView), tr("View trophies..."));
connect(action_view_trophies, &QAction::triggered, this,
&ConfigurationListWidget::ViewTrophies);
QAction* action_patches = menu.addAction(tr("Patches (experimental)..."));
QAction* action_patches = menu.addAction(tr("Cheats (experimental)..."));
connect(action_patches, &QAction::triggered, this, [this, item]() {
if (item != nullptr) {
auto* dialog = new PatchesDialog(item->GetInfo(), this);
+49 -37
View File
@@ -18,7 +18,7 @@
PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()) {
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle(tr("Patches (Experimental) - %1").arg(game.name));
setWindowTitle(tr("Cheats (Experimental) - %1").arg(game.name));
resize(640, 480);
auto* layout = new QVBoxLayout(this);
@@ -54,46 +54,50 @@ QString PatchesDialog::PatchPlanPath(const QString& title_id) {
void PatchesDialog::Load() {
QFile file(PatchPlanPath(m_title_id));
if (!file.open(QIODevice::ReadOnly)) {
m_status->setText(tr("No local patch file: %1").arg(file.fileName()));
m_status->setText(tr("No local cheat file: %1").arg(file.fileName()));
m_apply->setEnabled(false);
return;
}
const auto document = QJsonDocument::fromJson(file.readAll());
if (document.isNull() || !document.isObject()) {
m_status->setText(tr("Invalid patch JSON in %1.").arg(file.fileName()));
m_status->setText(tr("Invalid cheat JSON in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
return;
}
const auto root = document.object();
const auto patches = root.value(QStringLiteral("patches")).toArray();
for (const auto& value: patches) {
const auto patch = value.toObject();
auto* item = new QListWidgetItem(patch.value(QStringLiteral("name")).toString(), m_patches);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(patch.value(QStringLiteral("enabled")).toBool(true) ? Qt::Checked
: Qt::Unchecked);
}
m_apply->setEnabled(!patches.isEmpty());
if (patches.isEmpty()) {
// shadPS4/ETAHen cheat files use "mods"; Kyty only reads a "patches" array.
if (root.contains(QStringLiteral("mods"))) {
m_status->setText(
tr("This file uses a \"mods\" list. Kyty expects a top-level \"patches\" "
"array in %1.")
.arg(file.fileName()));
} else if (!root.contains(QStringLiteral("patches"))) {
m_status->setText(
tr("No \"patches\" array in %1.").arg(file.fileName()));
} else {
m_status->setText(tr("Loaded 0 patch(es) from %1.").arg(file.fileName()));
}
const auto root = document.object();
if (!root.value(QStringLiteral("mods")).isArray()) {
m_status->setText(tr("Cheat JSON has no \"mods\" array in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
return;
}
m_status->setText(tr("Loaded %1 patch(es) from %2.").arg(patches.size()).arg(file.fileName()));
const auto mods = root.value(QStringLiteral("mods")).toArray();
for (const auto& value: mods) {
if (!value.isObject()) {
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_patches->clear();
return;
}
const auto mod = value.toObject();
const auto name = mod.value(QStringLiteral("name"));
const auto enabled = mod.value(QStringLiteral("enabled"));
if (!name.isString() || name.toString().isEmpty() ||
(!enabled.isUndefined() && !enabled.isBool())) {
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_patches->clear();
return;
}
auto* item = new QListWidgetItem(name.toString(), m_patches);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(enabled.toBool(true) ? Qt::Checked : Qt::Unchecked);
}
m_apply->setEnabled(!mods.isEmpty());
m_status->setText(tr("Loaded %1 cheat(s) from %2.").arg(mods.size()).arg(file.fileName()));
}
void PatchesDialog::Save() {
@@ -105,20 +109,28 @@ void PatchesDialog::Save() {
auto document = QJsonDocument::fromJson(input.readAll());
input.close();
auto root = document.object();
auto patches = root.value(QStringLiteral("patches")).toArray();
for (int index = 0; index < patches.size(); index++) {
auto patch = patches[index].toObject();
patch.insert(QStringLiteral("enabled"),
m_patches->item(index)->checkState() == Qt::Checked);
patches[index] = patch;
const auto changed_message = tr("Cheat file changed; reload the dialog.");
if (!document.isObject()) {
m_status->setText(changed_message);
return;
}
root.insert(QStringLiteral("patches"), patches);
auto root = document.object();
auto mods = root.value(QStringLiteral("mods")).toArray();
if (mods.size() != m_patches->count()) {
m_status->setText(changed_message);
return;
}
for (int index = 0; index < mods.size(); index++) {
auto mod = mods[index].toObject();
mod.insert(QStringLiteral("enabled"), m_patches->item(index)->checkState() == Qt::Checked);
mods[index] = mod;
}
root.insert(QStringLiteral("mods"), mods);
document.setObject(root);
QSaveFile output(path);
if (output.open(QIODevice::WriteOnly) && output.write(document.toJson()) >= 0 &&
output.commit()) {
m_status->setText(tr("Patch selection saved."));
m_status->setText(tr("Cheat selection saved."));
}
}
+9 -2
View File
@@ -25,6 +25,7 @@
#include "libs/libs.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstddef>
@@ -142,8 +143,8 @@ struct RegisterDefaultsStorage {
#include <fmt/format.h>
static constexpr uint32_t GRAPHICS_REGISTER_DEFAULTS_MAX_VERSION = 12;
static constexpr uint32_t GRAPHICS_REGISTER_DEFAULTS_FALLBACK_VERSION = 11;
static constexpr uint32_t GRAPHICS_REGISTER_DEFAULTS_MAX_VERSION = 13;
static constexpr uint32_t GRAPHICS_REGISTER_DEFAULTS_FALLBACK_VERSION = 13;
static constexpr uint32_t GRAPHICS_INIT_NO_FEATURE_STATE = 0;
static std::mutex g_register_defaults_mutex;
@@ -3439,6 +3440,12 @@ uint32_t* KYTY_SYSV_ABI AgcDcbCopyData(CommandBuffer* buf, uint8_t dst, uint8_t
return cmd;
}
uint64_t KYTY_SYSV_ABI AgcDcbCopyDataGetSize() {
PRINT_NAME();
return 6 * sizeof(uint32_t);
}
uint32_t* KYTY_SYSV_ABI AgcDcbDmaData(CommandBuffer* buf, uint8_t engine, uint8_t dst,
uint8_t dst_cache_policy, uint64_t dst_address_or_offset,
uint8_t src, uint8_t src_cache_policy,
+1
View File
@@ -233,6 +233,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbCopyData(CommandBuffer* buf, uint8_t dst, uint8_t
uint64_t dst_address, uint8_t src, uint8_t src_cache_policy,
uint64_t src_address_or_immediate, uint8_t item_size,
uint8_t write_confirm);
uint64_t KYTY_SYSV_ABI AgcDcbCopyDataGetSize();
uint32_t* KYTY_SYSV_ABI AgcDcbDmaData(CommandBuffer* buf, uint8_t engine, uint8_t dst,
uint8_t dst_cache_policy, uint64_t dst_address_or_offset,
uint8_t src, uint8_t src_cache_policy,
+85 -3
View File
@@ -1,4 +1,4 @@
// No-feature, non-0x840f00 AgcGetRegisterDefaults2 dispatch, versions 0..12.
// No-feature, non-0x840f00 AgcGetRegisterDefaults2 dispatch, versions 0..13.
// Type entries keep the pointer index in bits 0..9; higher bits are AGC flags.
// g_agc_internal_reg_defaults_v0: regs=(1, 12, 0, 4), ptrs=(1, 12, 0, 2), types=15.
@@ -185,6 +185,45 @@ static CompactRegisterDefaults g_agc_internal_reg_defaults_v11 = {
g_agc_internal_reg_defaults_v11_types, 24,
};
// Version 13 takes the version-11 fallback and selects the >=13 state-0 lookup bank.
// Only two internal table-1 values differ in that bank.
static auto g_agc_internal_reg_defaults_v13_tbl1_regs = [] {
constexpr auto count = sizeof(g_agc_internal_reg_defaults_v11_tbl1_regs) /
sizeof(g_agc_internal_reg_defaults_v11_tbl1_regs[0]);
std::array<ShaderRegister, count> regs {};
std::copy_n(g_agc_internal_reg_defaults_v11_tbl1_regs, count, regs.begin());
for (auto& reg: regs) {
if (reg.offset == 0x0081) {
reg.value = 0x00000001;
} else if (reg.offset == 0x0101) {
reg.value = 0x00000003;
}
}
return regs;
}();
static CompactRegisterDefaults g_agc_internal_reg_defaults_v13 = {
g_agc_internal_reg_defaults_v11_tbl0_regs,
8,
g_agc_internal_reg_defaults_v11_tbl0_ptrs,
8,
g_agc_internal_reg_defaults_v13_tbl1_regs.data(),
12,
g_agc_internal_reg_defaults_v11_tbl1_ptrs,
12,
g_agc_internal_reg_defaults_v11_tbl2_regs,
1,
g_agc_internal_reg_defaults_v11_tbl2_ptrs,
1,
g_agc_internal_reg_defaults_v11_tbl3_regs,
6,
g_agc_internal_reg_defaults_v11_tbl3_ptrs,
3,
g_agc_internal_reg_defaults_v11_types,
24,
};
// g_agc_internal_reg_defaults_v4: regs=(2, 15, 0, 4), ptrs=(2, 15, 0, 2), types=19.
static ShaderRegister g_agc_internal_reg_defaults_v4_tbl0_regs[] = {
{0x02af, 0x00040000},
@@ -1366,8 +1405,28 @@ static uint32_t g_agc_public_reg_defaults_v11_types[] = {
0x036ac8a6, 0x000c0452, 0x00000000,
};
template <std::size_t Size>
static std::array<ShaderRegister, Size>
make_v11_plus_public_tbl0(const ShaderRegister (&source)[Size]) {
std::array<ShaderRegister, Size> regs {};
std::copy_n(source, Size, regs.begin());
for (auto& reg: regs) {
if (reg.offset == 0x0201) {
reg.value |= 0x00010000;
}
}
return regs;
}
// The common setup helper forces this table-0 patch for every version >= 11.
static auto g_agc_public_reg_defaults_v11_tbl0_effective =
make_v11_plus_public_tbl0(g_agc_public_reg_defaults_v11_tbl0_regs);
static auto g_agc_public_reg_defaults_v12_tbl0_effective =
make_v11_plus_public_tbl0(g_agc_public_reg_defaults_v10_tbl0_regs);
static CompactRegisterDefaults g_agc_public_reg_defaults_v11 = {
g_agc_public_reg_defaults_v11_tbl0_regs,
g_agc_public_reg_defaults_v11_tbl0_effective.data(),
488,
g_agc_public_reg_defaults_v11_tbl0_ptrs,
84,
@@ -1387,6 +1446,27 @@ static CompactRegisterDefaults g_agc_public_reg_defaults_v11 = {
137,
};
static CompactRegisterDefaults g_agc_public_reg_defaults_v12 = {
g_agc_public_reg_defaults_v12_tbl0_effective.data(),
482,
g_agc_public_reg_defaults_v10_tbl0_ptrs,
79,
g_agc_public_reg_defaults_v10_tbl1_regs,
159,
g_agc_public_reg_defaults_v10_tbl1_ptrs,
29,
g_agc_public_reg_defaults_v10_tbl2_regs,
55,
g_agc_public_reg_defaults_v10_tbl2_ptrs,
20,
nullptr,
0,
nullptr,
0,
g_agc_public_reg_defaults_v10_types,
128,
};
// g_agc_public_reg_defaults_v4: regs=(488, 158, 43, 0), ptrs=(80, 28, 42, 0), types=150.
static ShaderRegister g_agc_public_reg_defaults_v4_tbl0_regs[] = {
{0x0202, 0x00cc0010}, {0x0109, 0x00000008}, {0x0104, 0x00000000}, {0x008f, 0x00000000},
@@ -2825,7 +2905,8 @@ static CompactRegisterDefaults* g_agc_public_reg_defaults_by_version[] = {
&g_agc_public_reg_defaults_v9, // version 9
&g_agc_public_reg_defaults_v10, // version 10
&g_agc_public_reg_defaults_v11, // version 11
&g_agc_public_reg_defaults_v10, // version 12
&g_agc_public_reg_defaults_v12, // version 12
&g_agc_public_reg_defaults_v11, // version 13: v11 fallback with the >=13 state-0 bank
};
static CompactRegisterDefaults* g_agc_internal_reg_defaults_by_version[] = {
@@ -2842,4 +2923,5 @@ static CompactRegisterDefaults* g_agc_internal_reg_defaults_by_version[] = {
&g_agc_internal_reg_defaults_v10, // version 10
&g_agc_internal_reg_defaults_v11, // version 11
&g_agc_internal_reg_defaults_v10, // version 12
&g_agc_internal_reg_defaults_v13, // version 13
};
+1
View File
@@ -121,6 +121,7 @@ LIB_DEFINE(InitAgcDriver_1) {
LIB_FUNC("57labkp+rSQ", Gen5::AgcDcbAcquireMem);
LIB_FUNC("-vnlTPPXPrw", Gen5::AgcDcbAcquireMemGetSize);
LIB_FUNC("1rZSWUv1IRc", Gen5::AgcDcbCopyData);
LIB_FUNC("b5u0Jzm8TF8", Gen5::AgcDcbCopyDataGetSize); // SpongeBob PPSA26893
LIB_FUNC("WmAc2MEj6Io", Gen5::AgcDcbDmaData);
LIB_FUNC("xSAR0LTcRKM", Gen5::AgcDcbJump);
LIB_FUNC("VEGu4dixjUg", Gen5::AgcDcbJumpGetSize);
+211 -139
View File
@@ -8,207 +8,235 @@
#include "loader/systemContent.h"
#include <algorithm>
#include <charconv>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <memory>
#include <nlohmann/json.hpp>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
#include <vector>
namespace Loader::GamePatch {
namespace {
constexpr uint64_t kPageSize = 0x4000;
struct Write {
std::string patch_name;
uint64_t address = 0;
std::vector<uint8_t> expected;
std::vector<uint8_t> replacement;
uint64_t source_address = 0;
uint64_t target_address = 0;
std::vector<uint8_t> off;
std::vector<uint8_t> on;
};
struct Plan {
std::string title_id;
std::string game_version;
std::string version;
std::string process;
std::vector<Write> writes;
std::vector<std::string> patch_names;
std::vector<std::string> mod_names;
std::vector<uint64_t> cave_pages;
};
using Json = nlohmann::json;
std::unique_ptr<Plan> g_pending_plan;
std::vector<uint64_t> g_applied_cave_pages;
bool Fail(std::string* error, std::string message) {
*error = std::move(message);
return false;
}
const Json::string_t* StringField(const Json& object, const char* name) {
const auto value = object.find(name);
return value == object.end() ? nullptr : value->get_ptr<const Json::string_t*>();
}
const Json::array_t* ArrayField(const Json& object, const char* name) {
const auto value = object.find(name);
return value == object.end() ? nullptr : value->get_ptr<const Json::array_t*>();
}
bool ParseBytes(std::string_view text, std::vector<uint8_t>* bytes) {
bool ParseBytes(const std::string& text, std::vector<uint8_t>* bytes) {
if (text.empty() || (text.size() % 2) != 0) {
return false;
}
bytes->resize(text.size() / 2);
for (size_t index = 0; index < bytes->size(); index++) {
unsigned int value = 0;
const char* begin = text.data() + index * 2;
const auto [end, error] = std::from_chars(begin, begin + 2, value, 16);
if (error != std::errc {} || end != begin + 2) {
return false;
}
(*bytes)[index] = static_cast<uint8_t>(value);
(*bytes)[index] = static_cast<uint8_t>(std::stoul(text.substr(index * 2, 2), nullptr, 16));
}
return true;
}
bool ReadJson(const std::filesystem::path& path, Json* root, std::string* error) {
bool LoadPlan(const std::filesystem::path& path, Plan* plan, std::string* error) {
std::ifstream file(path, std::ios::binary);
if (!file) {
return Fail(error, "could not read patch plan");
return Fail(error, "could not read cheat file");
}
*root = Json::parse(file, nullptr, false);
return root->is_object() || Fail(error, "patch plan is not valid JSON");
}
bool LoadPlan(const std::filesystem::path& path, Plan* plan, std::string* error) {
Json root;
if (!ReadJson(path, &root, error)) {
return false;
const auto root = Json::parse(file, nullptr, false);
if (!root.is_object() || !root.contains("id") || !root.contains("version") ||
!root.contains("process") || !root.contains("mods")) {
return Fail(error, "expected a GoldHEN-style mods JSON file");
}
plan->title_id = root["id"].get<std::string>();
plan->version = root["version"].get<std::string>();
plan->process = root["process"].get<std::string>();
const auto* title_id = StringField(root, "title_id");
const auto* game_version = StringField(root, "game_version");
const auto* process = StringField(root, "process");
const auto* patches = ArrayField(root, "patches");
if (title_id == nullptr || game_version == nullptr || process == nullptr ||
patches == nullptr) {
return Fail(error, "invalid patch plan");
}
plan->title_id = *title_id;
plan->game_version = *game_version;
plan->process = *process;
for (const auto& patch_json: *patches) {
const auto enabled = patch_json.find("enabled");
if (enabled != patch_json.end() && enabled->is_boolean() && !enabled->get<bool>()) {
for (const auto& mod: root["mods"]) {
if (!mod.value("enabled", true)) {
continue;
}
const auto* name = StringField(patch_json, "name");
const auto* writes = ArrayField(patch_json, "writes");
if (name == nullptr || writes == nullptr) {
return Fail(error, "invalid patch entry");
}
plan->patch_names.push_back(*name);
for (const auto& write_json: *writes) {
const auto* expected = StringField(write_json, "expected");
const auto* replacement = StringField(write_json, "replacement");
if (expected == nullptr || replacement == nullptr) {
return Fail(error, "invalid patch write");
const auto name = mod["name"].get<std::string>();
plan->mod_names.push_back(name);
for (const auto& entry: mod["memory"]) {
Write write;
write.source_address = std::stoull(entry["offset"].get<std::string>(), nullptr, 16);
if (!ParseBytes(entry["off"].get<std::string>(), &write.off) ||
!ParseBytes(entry["on"].get<std::string>(), &write.on) ||
write.off.size() != write.on.size()) {
return Fail(error, "invalid mod memory bytes");
}
Write write {
.patch_name = *name,
};
if (!ParseBytes(*expected, &write.expected) ||
!ParseBytes(*replacement, &write.replacement) ||
write.expected.size() != write.replacement.size()) {
return Fail(error, "invalid patch bytes");
}
plan->writes.push_back(std::move(write));
}
}
return true;
}
bool ValidateTarget(const Plan& plan, const Program* program, std::string* error) {
if (program == nullptr || program->elf == nullptr || program->base_vaddr == 0) {
bool ValidateGame(const Plan& plan, const Program* main_program, std::string* error) {
if (main_program == nullptr || main_program->base_vaddr == 0) {
return Fail(error, "main executable is not loaded");
}
std::string title_id;
std::string game_version;
std::string version;
if (!SystemContentParamSfoGetString("TITLE_ID", &title_id) ||
!SystemContentParamSfoGetString("APP_VER", &game_version)) {
return Fail(error, "game metadata is incomplete");
}
if (!Common::EqualNoCase(program->file_name.filename().string(), plan.process) ||
!Common::EqualNoCase(title_id, plan.title_id) || game_version != plan.game_version) {
return Fail(error, "patch plan does not match the loaded game");
!SystemContentParamSfoGetString("APP_VER", &version) ||
!Common::EqualNoCase(main_program->file_name.filename().string(), plan.process) ||
!Common::EqualNoCase(title_id, plan.title_id) || version != plan.version) {
return Fail(error, "cheat file does not match the loaded game");
}
return true;
}
bool ResolveWrite(const Program& program, Write* write, std::string* error) {
const auto* ehdr = program.elf->GetEhdr();
const auto* phdr = program.elf->GetPhdr();
uint64_t match = 0;
size_t match_count = 0;
for (Elf64_Half index = 0; index < ehdr->e_phnum; index++) {
const auto& segment = phdr[index];
const bool loaded = segment.p_type == PT_LOAD || segment.p_type == PT_OS_RELRO;
if (!loaded || segment.p_filesz < write->expected.size()) {
continue;
}
const auto segment_address = program.base_vaddr + segment.p_vaddr;
const auto* begin = reinterpret_cast<const uint8_t*>(segment_address);
const auto* end = begin + segment.p_filesz;
for (auto* current = begin; current < end;) {
const auto* found =
std::search(current, end, write->expected.begin(), write->expected.end());
if (found == end) {
break;
}
const auto address = reinterpret_cast<uint64_t>(found);
if (match == 0) {
match = address;
}
match_count++;
current = found + 1;
}
void Translate(const Program& program, uint64_t source_base, uint64_t source_address,
uint64_t* target_address) {
if (source_address >= source_base) {
*target_address = program.base_vaddr + source_address - source_base;
return;
}
::printf("Game patch: found %zu entries for '%s'\n", match_count, write->patch_name.c_str());
if (match == 0) {
return Fail(error, "original bytes not found for '" + write->patch_name + "'");
}
write->address = match;
return true;
*target_address = program.base_vaddr - (source_base - source_address);
}
bool PrepareWrites(Plan* plan, const Program& program, std::string* error) {
bool IsInsideProgram(const Program& program, uint64_t address, size_t size) {
return size != 0 && address >= program.base_vaddr &&
address + size <= program.base_vaddr + program.mapped_size;
}
bool IsZero(const std::vector<uint8_t>& bytes) {
return std::all_of(bytes.begin(), bytes.end(), [](uint8_t byte) { return byte == 0; });
}
bool TranslateAndValidate(Plan* plan, const Program& program, uint64_t source_base) {
for (auto& write: plan->writes) {
if (!ResolveWrite(program, &write, error)) {
Translate(program, source_base, write.source_address, &write.target_address);
if (!IsZero(write.off) &&
(!IsInsideProgram(program, write.target_address, write.off.size()) ||
std::memcmp(reinterpret_cast<const void*>(write.target_address), write.off.data(),
write.off.size()) != 0)) {
return false;
}
}
return true;
}
bool ApplyWrites(Plan* plan, std::string* error) {
for (auto& write: plan->writes) {
const auto size = write.replacement.size();
std::memcpy(reinterpret_cast<void*>(write.address), write.replacement.data(), size);
if (!Common::VirtualMemory::FlushInstructionCache(write.address, size)) {
return Fail(error, "could not finalize patch");
bool Matches(Plan* plan, const Program& program, uint64_t* source_base) {
const auto anchor = std::find_if(plan->writes.begin(), plan->writes.end(),
[](const Write& write) { return !IsZero(write.off); });
if (anchor == plan->writes.end() || program.mapped_size < anchor->off.size()) {
return false;
}
const auto* ehdr = program.elf->GetEhdr();
const auto* phdr = program.elf->GetPhdr();
for (Elf64_Half index = 0; index < ehdr->e_phnum; index++) {
const auto& segment = phdr[index];
if ((segment.p_type != PT_LOAD && segment.p_type != PT_OS_RELRO) ||
segment.p_filesz < anchor->off.size()) {
continue;
}
const auto* begin = reinterpret_cast<const uint8_t*>(program.base_vaddr + segment.p_vaddr);
const auto* end = begin + segment.p_filesz;
for (auto* current = begin; current < end;) {
const auto* found = std::search(current, end, anchor->off.begin(), anchor->off.end());
if (found == end) {
break;
}
const auto rva = static_cast<uint64_t>(found - begin) + segment.p_vaddr;
if (anchor->source_address >= rva) {
*source_base = anchor->source_address - rva;
if (TranslateAndValidate(plan, program, *source_base)) {
return true;
}
}
current = found + 1;
}
}
return false;
}
void ReleaseCaves(std::vector<uint64_t>* cave_pages) {
for (auto page = cave_pages->rbegin(); page != cave_pages->rend(); ++page) {
EXIT_IF(!Libs::LibKernel::Memory::FreeGuestMemory(*page, kPageSize));
}
cave_pages->clear();
}
bool AllocateCave(Plan* plan, const Program& program, std::string* error) {
std::vector<uint64_t> pages;
for (const auto& write: plan->writes) {
if (IsInsideProgram(program, write.target_address, write.off.size())) {
continue;
}
if (!IsZero(write.off)) {
return Fail(error, "non-zero write is outside the matched module");
}
const auto write_end = write.target_address + write.off.size();
for (auto page = write.target_address & ~(kPageSize - 1); page < write_end;
page += kPageSize) {
if (std::find(pages.begin(), pages.end(), page) == pages.end()) {
pages.push_back(page);
}
}
}
for (const auto page: pages) {
const auto allocated = Libs::LibKernel::Memory::AllocateRuntimeMemory(
page, kPageSize, Common::VirtualMemory::Mode::ExecuteReadWrite, "game_cheat_code_cave",
true);
if (allocated != page) {
if (allocated != 0) {
Libs::LibKernel::Memory::FreeGuestMemory(allocated, kPageSize);
}
ReleaseCaves(&plan->cave_pages);
return Fail(error, "could not allocate code-cave page");
}
plan->cave_pages.push_back(page);
}
return true;
}
bool ApplyWrites(Plan* plan, const Program& program, std::string* error) {
if (!AllocateCave(plan, program, error)) {
return false;
}
for (int external = 1; external >= 0; external--) {
for (const auto& write: plan->writes) {
if (static_cast<int>(
!IsInsideProgram(program, write.target_address, write.on.size())) != external) {
continue;
}
std::memcpy(reinterpret_cast<void*>(write.target_address), write.on.data(),
write.on.size());
if (!Common::VirtualMemory::FlushInstructionCache(write.target_address,
write.on.size())) {
return Fail(error, "could not finalize cheat write");
}
}
}
return true;
@@ -216,19 +244,63 @@ bool ApplyWrites(Plan* plan, std::string* error) {
} // namespace
bool Apply(const std::filesystem::path& plan_path, Program* program) {
bool Apply(const std::filesystem::path& plan_path, Program* main_program,
const std::vector<Program*>& programs) {
Plan plan;
std::string error;
if (LoadPlan(plan_path, &plan, &error) && ValidateTarget(plan, program, &error) &&
PrepareWrites(&plan, *program, &error) && ApplyWrites(&plan, &error)) {
for (const auto& name: plan.patch_names) {
::printf("Successfully applied patch: %s\n", name.c_str());
}
if (!LoadPlan(plan_path, &plan, &error) || !ValidateGame(plan, main_program, &error)) {
::printf("Game cheat error: %s\n", error.c_str());
return false;
}
if (plan.writes.empty()) {
return true;
}
::printf("Game patch error: %s\n", error.c_str());
return false;
g_pending_plan = std::make_unique<Plan>(std::move(plan));
for (auto* program: programs) {
if (!ApplyPending(program)) {
return false;
}
if (g_pending_plan == nullptr) {
return true;
}
}
::printf("Game cheat: waiting for the matching module\n");
return true;
}
bool ApplyPending(Program* program) {
uint64_t source_base = 0;
if (g_pending_plan == nullptr || program == nullptr || program->elf == nullptr ||
!Matches(g_pending_plan.get(), *program, &source_base)) {
return true;
}
::printf("Game cheat: matched %s with source base 0x%llx\n",
program->file_name.filename().string().c_str(),
static_cast<unsigned long long>(source_base));
std::string error;
if (!ApplyWrites(g_pending_plan.get(), *program, &error)) {
ReleaseCaves(&g_pending_plan->cave_pages);
g_pending_plan.reset();
::printf("Game cheat error: %s\n", error.c_str());
return false;
}
for (const auto& name: g_pending_plan->mod_names) {
::printf("Successfully applied cheat: %s\n", name.c_str());
}
g_applied_cave_pages = std::move(g_pending_plan->cave_pages);
g_pending_plan.reset();
return true;
}
void Clear() {
if (g_pending_plan != nullptr) {
ReleaseCaves(&g_pending_plan->cave_pages);
}
g_pending_plan.reset();
ReleaseCaves(&g_applied_cave_pages);
}
} // namespace Loader::GamePatch
+5 -1
View File
@@ -4,6 +4,7 @@
#include "common/common.h"
#include <filesystem>
#include <vector>
namespace Loader {
@@ -11,7 +12,10 @@ struct Program;
namespace GamePatch {
bool Apply(const std::filesystem::path& plan_path, Program* program);
bool Apply(const std::filesystem::path& plan_path, Program* main_program,
const std::vector<Program*>& programs);
bool ApplyPending(Program* program);
void Clear();
} // namespace GamePatch
+8 -1
View File
@@ -1289,6 +1289,9 @@ void RuntimeLinker::RelocateProgram(Program* program) {
EXIT_IF(std::find(m_programs.begin(), m_programs.end(), program) == m_programs.end());
Relocate(program);
if (!GamePatch::ApplyPending(program)) {
EXIT("Failed to apply pending game cheat\n");
}
}
void RuntimeLinker::UnloadProgram(Program* program) {
@@ -1412,7 +1415,10 @@ void RuntimeLinker::Execute(const std::filesystem::path& game_patch) {
RelocateAll();
if (!game_patch.empty()) {
GamePatch::Apply(game_patch, m_programs.empty() ? nullptr : m_programs.front());
if (!GamePatch::Apply(game_patch, m_programs.empty() ? nullptr : m_programs.front(),
m_programs)) {
EXIT("Failed to apply game cheat\n");
}
}
StartAllModules();
@@ -1436,6 +1442,7 @@ void RuntimeLinker::Clear() {
// EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
Common::LockGuard lock(m_mutex);
GamePatch::Clear();
for (auto* p: m_programs) {
DeleteProgram(p);
+3 -3
View File
@@ -43,15 +43,15 @@ static void PrintUsage() {
::printf("kyty_emulator --game <dir|elf> [options]\n\n");
::printf("Options:\n");
::printf(" --game <dir|elf> Game directory or ELF to load.\n");
::printf(
" --game-patch <json> Validated patch plan to apply before entry.\n");
::printf(" --game-patch <json> ETAHen cheat file.\n");
::printf(" --screen-width <num> Window width. Default: 1280.\n");
::printf(" --screen-height <num> Window height. Default: 720.\n");
::printf(
" --user-name <name> Local user name (1-16 bytes). Default: Kyty.\n");
::printf(" --user-id <num> Local user ID. Default: %d.\n",
Config::DEFAULT_USER_ID);
::printf(" --present-mode <value> Fifo, Mailbox, or Immediate. Default: Fifo.\n");
::printf(
" --present-mode <value> Fifo, Mailbox, or Immediate. Default: Fifo.\n");
::printf(" --fullscreen Run in borderless desktop fullscreen.\n");
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n");
+175
View File
@@ -11896,6 +11896,8 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode,
case Opcode::V_RSQ_F16:
case Opcode::V_LOG_F16:
case Opcode::V_EXP_F16:
case Opcode::V_SIN_F16:
case Opcode::V_COS_F16:
case Opcode::V_MAD_MIXLO_F16:
case Opcode::V_MAD_MIXHI_F16:
case Opcode::DS_MIN_F32:
@@ -12029,6 +12031,7 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode,
case Opcode::DS_XOR_RTN_B32:
case Opcode::DS_WRXCHG_RTN_B32:
case Opcode::DS_SWIZZLE_B32:
case Opcode::DS_BPERMUTE_B32:
case Opcode::DS_READ_I8:
case Opcode::DS_READ_U8:
case Opcode::DS_READ_I16:
@@ -14488,6 +14491,91 @@ TestCase VectorSpecialF16Ops() {
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
}
TestCase VectorCosF16CapturedSdwaAndEdges() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovLiteral(&code, 4, 0x34001234u); // high=+0.25h, low sentinel
code.push_back(0x7e08c2f9u);
code.push_back(0x00051504u); // captured v_cos_f16 v4.word1, v4.word1
AppendVMovLiteral(&code, 5, 0x00008000u); // -0.0h
code.push_back(EncodeVop1(0x61, 6, Vgpr(5)));
AppendVMovLiteral(&code, 7, 0x00007bffu); // max finite
code.push_back(EncodeVop1(0x61, 8, Vgpr(7)));
AppendVMovLiteral(&code, 9, 0x00007c00u); // +inf
code.push_back(EncodeVop1(0x61, 10, Vgpr(9)));
AppendVMovLiteral(&code, 11, 0x00000001u); // minimum positive denormal
code.push_back(EncodeVop1(0x61, 12, Vgpr(11)));
AppendVMovLiteral(&code, 13, 0x00003800u); // +0.5h
code.push_back(EncodeVop1(0x61, 14, Vgpr(13)));
for (u32 i = 0; i < 6; i++) {
AppendStoreVgpr(&code, 4 + i * 2, i);
}
AppendEnd(&code);
TestCase test{"VectorCosF16CapturedSdwaAndEdges",
code,
{},
{0x00001234u, 0x00003c00u, 0x00003c00u, 0x0000fe00u,
0x00003c00u, 0x0000bc00u},
{O::V_MOV_B32, O::V_COS_F16, O::BUFFER_STORE_DWORD,
O::S_ENDPGM}};
test.decoded_counts = {{"V_COS_F16", 6},
{"V_COS_F16 v4.sdwa(sel=5,sext=0), "
"v4.sdwa(sel=5,sext=0)",
1}};
test.ir_counts = {{" = FPCos ", 6}};
test.required_spirv = {" Fract ", " Cos ", " PackHalf2x16 "};
test.forbidden_spirv = {"OpCapability Float16"};
return test;
}
TestCase VectorSinF16SdwaAndEdges() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovLiteral(&code, 4, 0x34001234u); // high=+0.25h, low sentinel
code.push_back(0x7e08c0f9u);
code.push_back(0x00051504u); // v_sin_f16 v4.word1, v4.word1
AppendVMovLiteral(&code, 5, 0x00008000u); // -0.0h
code.push_back(EncodeVop1(0x60, 6, Vgpr(5)));
AppendVMovLiteral(&code, 7, 0x00007bffu); // max finite
code.push_back(EncodeVop1(0x60, 8, Vgpr(7)));
AppendVMovLiteral(&code, 9, 0x0000fbffu); // minimum finite
code.push_back(EncodeVop1(0x60, 10, Vgpr(9)));
AppendVMovLiteral(&code, 11, 0x00007c00u); // +inf
code.push_back(EncodeVop1(0x60, 12, Vgpr(11)));
AppendVMovLiteral(&code, 13, 0x0000fc00u); // -inf
code.push_back(EncodeVop1(0x60, 14, Vgpr(13)));
AppendVMovLiteral(&code, 15, 0x00003800u); // +0.5h
code.push_back(EncodeVop1(0x60, 16, Vgpr(15)));
AppendVMovLiteral(&code, 17, 0x00003a00u); // +0.75h
code.push_back(EncodeVop1(0x60, 18, Vgpr(17)));
AppendVMovLiteral(&code, 19, 0x00003400u); // +0.25h
AppendVop3(&code, 0x1e0, 20, Vgpr(19), 0);
for (u32 i = 0; i < 9; i++) {
AppendStoreVgpr(&code, 4 + i * 2, i);
}
AppendEnd(&code);
TestCase test{"VectorSinF16SdwaAndEdges",
code,
{},
{0x3c001234u, 0x00008000u, 0x00000000u, 0x00000000u,
0x0000fe00u, 0x0000fe00u, 0x00000000u, 0x0000bc00u,
0x00003c00u},
{O::V_MOV_B32, O::V_SIN_F16, O::BUFFER_STORE_DWORD,
O::S_ENDPGM}};
test.decoded_counts = {{"V_SIN_F16", 9},
{"V_SIN_F16 v4.sdwa(sel=5,sext=0), "
"v4.sdwa(sel=5,sext=0)",
1}};
test.ir_counts = {{" = FPSin ", 9}};
test.required_spirv = {" Fract ", " Sin ", " PackHalf2x16 "};
test.forbidden_spirv = {"OpCapability Float16"};
return test;
}
TestCase VectorWritelaneIgnoresExecMask() {
using O = ShaderOpcode;
@@ -18558,6 +18646,89 @@ TestCase DsSwizzleInvalidSourceLaneZero() {
return test;
}
TestCase DsBpermuteCapturedExecOffsetAndWrap() {
using O = ShaderOpcode;
std::vector<u32> code;
code.push_back(EncodeVop2(0x1a, 30, InlineU32(2), 0));
AppendBufferLoadDword(&code, 17, 30);
AppendVMovLiteral(&code, 1, 0xdeadbeefu);
AppendVMovU32(&code, 3, 100);
code.push_back(EncodeVop2(0x25, 3, Vgpr(0), 3));
code.push_back(EncodeSop1(0x04, 4, 126));
code.push_back(EncodeVop2(0x1b, 18, InlineU32(1), 0));
code.push_back(EncodeVopc(0xc2, InlineU32(1), 18));
code.push_back(EncodeSop1(0x04, 126, 106));
code.push_back(0xdacc0000u);
code.push_back(0x01000311u); // ds_bpermute_b32 v1, v17, v3
code.push_back(EncodeSop1(0x04, 126, 4));
AppendStoreVgprAtLaneDwordOffset(&code, 1, 0, 4);
AppendVMovU32(&code, 17, 124);
code.push_back(EncodeDs0(0xb3, 4));
code.push_back(EncodeDs1(2, 3, 17));
AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, 8);
AppendEnd(&code);
TestCase test;
test.name = "DsBpermuteCapturedExecOffsetAndWrap";
test.code = code;
test.initial = {0, 0, 12, 4, 0, 0, 0, 0, 0, 0, 0, 0};
test.expected = {0, 0, 12, 4,
0xdeadbeefu, 0, 0xdeadbeefu, 101,
100, 100, 100, 100};
test.opcodes = {O::V_LSHLREV_B32, O::BUFFER_LOAD_DWORD,
O::V_MOV_B32, O::V_ADD_NC_U32,
O::V_AND_B32, O::V_CMP_EQ_U32,
O::S_MOV_B64,
O::DS_BPERMUTE_B32, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.required_spirv = {"OpGroupNonUniformShuffle",
"OpGroupNonUniformBallot"};
test.forbidden_spirv = {" Workgroup", "OpControlBarrier",
"OpMemoryBarrier"};
test.decoded_counts = {{"DS_BPERMUTE_B32", 2}};
test.ir_counts = {{"BpermuteU32", 2}};
test.compute_info.threads_num[0] = 4;
test.compute_info.threads_num[1] = 1;
test.compute_info.threads_num[2] = 1;
test.compute_info.wave_size = 32;
test.compute_info.thread_ids_num = 1;
test.has_compute_info = true;
return test;
}
TestCase DsBpermuteWave64UsesIndependentHalves() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovU32(&code, 3, 100);
code.push_back(EncodeVop2(0x25, 3, Vgpr(0), 3));
AppendVMovU32(&code, 17, 128);
code.push_back(0xdacc0000u);
code.push_back(0x01000311u); // ds_bpermute_b32 v1, v17, v3
AppendStoreVgprAtLaneDwordOffset(&code, 1, 0, 0);
AppendEnd(&code);
TestCase test;
test.name = "DsBpermuteWave64UsesIndependentHalves";
test.code = code;
test.initial = std::vector<u32>(64, 0);
test.expected = std::vector<u32>(32, 100);
test.expected.insert(test.expected.end(), 32, 132);
test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32,
O::DS_BPERMUTE_B32, O::V_LSHLREV_B32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpGroupNonUniformShuffle"};
test.compute_info.threads_num[0] = 64;
test.compute_info.threads_num[1] = 1;
test.compute_info.threads_num[2] = 1;
test.compute_info.wave_size = 64;
test.compute_info.thread_ids_num = 1;
test.has_compute_info = true;
return test;
}
TestCase BufferAtomicVariants() {
using O = ShaderOpcode;
@@ -20604,6 +20775,8 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorCvtU16F16Sdwa);
AddCase(VectorMinMaxMed3F16Ops);
AddCase(VectorSpecialF16Ops);
AddCase(VectorCosF16CapturedSdwaAndEdges);
AddCase(VectorSinF16SdwaAndEdges);
AddCase(VectorWritelaneIgnoresExecMask);
AddCase(VectorReadlaneFromInactiveWrittenLane);
AddCase(VectorLaneWave32RuntimeSelectorWraps);
@@ -20746,6 +20919,8 @@ std::vector<TestCase> MakeCases() {
AddCase(DsMiscVariants);
AddCase(DsFloatMinMaxUsesSeparateCompareOperand);
AddCase(DsSwizzleInvalidSourceLaneZero);
AddCase(DsBpermuteCapturedExecOffsetAndWrap);
AddCase(DsBpermuteWave64UsesIndependentHalves);
AddCase(BufferAtomicVariants);
AddCase(BufferAtomicGlc0DoesNotReturnOldValue);
AddCase(BufferAtomicFMinExactRawGlcModes);
+50
View File
@@ -4031,6 +4031,38 @@ void TestNewShaderDecoderArchitecture() {
ffbh_i32.src0.reg == 14u,
"decoder rejected the captured VOP1 V_FFBH_I32 instruction");
const uint32_t cos_f16_code[] = {0x7e08c2f9u, 0x00051504u};
Instruction cos_f16;
Check(DecodeInstruction(cos_f16_code, 0u, cos_f16, &error), error.c_str());
Check(cos_f16.family == Family::VOP1 &&
cos_f16.opcode == Opcode::V_COS_F16 &&
cos_f16.word_count == 2u && cos_f16.src_count == 1u &&
cos_f16.dst.kind == OperandKind::Vgpr && cos_f16.dst.reg == 4u &&
cos_f16.dst.sdwa_sel == 5u &&
cos_f16.dst.sdwa_dst_unused == 2u && !cos_f16.dst.clamp &&
cos_f16.dst.omod == 0u &&
cos_f16.src0.kind == OperandKind::Vgpr &&
cos_f16.src0.reg == 4u && cos_f16.src0.sdwa_sel == 5u &&
!cos_f16.src0.sdwa_sext && !cos_f16.src0.negate &&
!cos_f16.src0.absolute,
"decoder rejected the captured VOP1 SDWA V_COS_F16 instruction");
const uint32_t sin_f16_code[] = {0x7e08c0f9u, 0x00051504u};
Instruction sin_f16;
Check(DecodeInstruction(sin_f16_code, 0u, sin_f16, &error), error.c_str());
Check(sin_f16.family == Family::VOP1 &&
sin_f16.opcode == Opcode::V_SIN_F16 &&
sin_f16.word_count == 2u && sin_f16.src_count == 1u &&
sin_f16.dst.kind == OperandKind::Vgpr && sin_f16.dst.reg == 4u &&
sin_f16.dst.sdwa_sel == 5u &&
sin_f16.dst.sdwa_dst_unused == 2u && !sin_f16.dst.clamp &&
sin_f16.dst.omod == 0u &&
sin_f16.src0.kind == OperandKind::Vgpr &&
sin_f16.src0.reg == 4u && sin_f16.src0.sdwa_sel == 5u &&
!sin_f16.src0.sdwa_sext && !sin_f16.src0.negate &&
!sin_f16.src0.absolute,
"decoder rejected the VOP1 SDWA V_SIN_F16 instruction");
const uint32_t literal_code[] = {EncodeVop1(0x01, 2, 255u), 0x12345678u};
Instruction literal;
Check(DecodeInstruction(literal_code, 0u, literal, &error), error.c_str());
@@ -4123,6 +4155,18 @@ void TestNewShaderDecoderArchitecture() {
Check(boot.opcode == Opcode::DS_SWIZZLE_B32 && boot.offset == 0xc480u,
"DS decoder rejected a captured boot-shader instruction");
const uint32_t bpermute_ds[] = {0xdacc0000u, 0x01000311u};
Instruction bpermute;
Check(DecodeInstruction(bpermute_ds, 0u, bpermute, &error), error.c_str());
Check(bpermute.family == Family::DS &&
bpermute.opcode == Opcode::DS_BPERMUTE_B32 &&
bpermute.word_count == 2u && bpermute.src_count == 2u &&
bpermute.data_dwords == 1u && bpermute.data_bits == 32u &&
bpermute.offset == 0u && !bpermute.gds &&
bpermute.dst.reg == 1u && bpermute.src0.reg == 17u &&
bpermute.src1.reg == 3u,
"DS decoder rejected or misdecoded captured DS_BPERMUTE_B32");
const uint32_t d16_hi_ds[] = {0xda9c0000u, 0x05000006u};
Instruction d16_hi;
Check(DecodeInstruction(d16_hi_ds, 0u, d16_hi, &error), error.c_str());
@@ -12083,6 +12127,12 @@ void TestNewShaderRecompilerUnsupportedMemoryDecode() {
CheckNewDecoderUnsupported(mtbuf_unknown,
static_cast<uint32_t>(std::size(mtbuf_unknown)),
"MTBUF", "opcode=0x08");
const uint32_t bpermute_gds[] = {
EncodeDs0(0xb3) | (1u << 17u), EncodeDs1(1, 3, 17), 0xbf810000u};
CheckNewDecoderUnsupported(bpermute_gds,
static_cast<uint32_t>(std::size(bpermute_gds)),
"DS", "available only for LDS");
}
void TestNewShaderRecompilerFlatUserPointerUsesDma() {