Compare commits

...

12 Commits

Author SHA1 Message Date
nmzik aa3b2cd57f Implement AGC async rewind imports 2026-10-08 17:43:02 +04:00
Dimitar Vagalinski 4865dd92ca libNet: answer sceNpGetAccountLanguage2 as a signed-out user (#1186)
* libNet: answer sceNpGetAccountLanguage2 as a signed-out user

sceNpGetAccountLanguage2 was not registered, so games that call it hit
the unresolved-import stub. The stub returns 0, which tells the game the
request succeeded and that the language code buffer now holds the
account's language, but nothing was written to it.

Saros (#1053) calls it from its analytics thread right after
sceNpGetAccountAge, each on its own request. sceNpGetAccountAge already
reports SCE_NP_ERROR_SIGNED_OUT there, so the game is told the user is
signed out for one query and signed in for the next. Demon's Souls
(#890), Ghost of Yotei (#1095) and NHL 27 (#1101) import it as well.

Register it next to the other NpManager account queries and answer the
way they do: SCE_NP_ERROR_INVALID_ARGUMENT for a request id <= 0 or a
null output pointer, SCE_NP_ERROR_SIGNED_OUT otherwise. The output is
left untouched because nothing was looked up.

* libNet: reject missing account language requests

---------

Co-authored-by: overlordxrz-source <overlordxrz-source@users.noreply.github.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-08 16:48:01 +04:00
Jetsku 3c62359e05 Fix/trigger effect state (#1132)
* Pad: report the adaptive trigger state instead of always 0

scePadGetTriggerEffectState returned 0 for both triggers. Games that wait for
the state of the trigger effect they set before acting on L2/R2 never saw it
change; in Astro Bot, L2/R2 actions such as punches did nothing in some levels.

The state now follows from the game's last effect per trigger (already kept
for the DualSense output) and the trigger's travel, 0..255 over the effect's
ten positions: feedback no force / pushing, weapon not / almost / fully
pressed, vibration not firing / firing, with the values 1..7 that duaLib
(WujekFoliarz/duaLib) reads from a DualSense input report. SDL does not expose
that report, so this is an approximation; it also works with other pads and
the keyboard, where a digital L2/R2 is full travel. The trigger intensity
setting changes only the output, not the reported state.

Tested in Astro Bot (PPSA21567 1.018) by a player in the levels where the
triggers did nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tests: controller_settings covers scePadGetTriggerEffectState

Feedback, weapon and vibration states across the trigger travel, a trigger without an effect, a masked update keeping the other trigger's effect, and the intensity setting leaving the reported state unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pad: a feedback trigger reports pushing only while it is pressed

A feedback effect from position 0 reported FEEDBACK_IS_PUSHING on an untouched trigger, so a game waiting for the change from no force to pushing never saw it. In Astro's Playroom the gacha capsules did not break on R2. Feedback states (modes 1, 4, 5) now need some travel (>= 8 of 255). Tested in Astro's Playroom (PPSA01325): the capsules break on R2 again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pad: a vibration trigger reports firing only while it is pressed

A vibration effect from position 0 reported VIBRATION_IS_FIRING on an untouched trigger; in Astro's Playroom the gun fired by itself. Vibration states (modes 3 and 6) now need some travel, as the feedback states do. Tested in Astro's Playroom.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pad: reject invalid handles in scePadGetTriggerEffectState; test the remaining effect modes

scePadGetTriggerEffectState ignored its handle and returned pad 1's state for any handle. It now returns
SCE_PAD_ERROR_INVALID_HANDLE (0x80920003) for handles other than 1, like the other pad queries in libPad.cpp
and duaLib.

controller_settings now also covers multiple-position feedback (mode 4: pushes only at positions with a
strength), slope feedback (mode 5: pushes from its start position, not on an untouched trigger) and
multiple-position vibration (mode 6: fires only at positions with an amplitude and a frequency).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pad: scePadGetTriggerEffectState returns libScePad's invalid-argument code

A null state returned 0x80960005, which is SCE_USER_SERVICE_ERROR_INVALID_ARGUMENT,
not a pad error; libScePad's code is SCE_PAD_ERROR_INVALID_ARG (0x80920001), as
scePadSetTriggerEffect returns. The call now lives with the other pad calls in
controller.cpp so controller_settings tests its handle and argument checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pad: correct trigger effect state reporting

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-08 16:00:17 +04:00
nmzik 62643c3424 system service: deliver initial AppContent entitlement updates through shared queue 2026-10-08 15:53:14 +04:00
nmzik 210d4dcd99 sysmodule: track shared module load references and unloaded status 2026-10-08 15:52:18 +04:00
nmzik 5a8808086a ampr: honor shared counter waits and completion writes 2026-10-08 03:36:59 +04:00
nmzik 861dd76d7f net: accept sufficient Ethernet address output buffers 2026-10-08 03:36:59 +04:00
nmzik db41d0482f kernel: retain POSIX file descriptor close-on-exec flags 2026-10-08 03:36:11 +04:00
Henrique Reis Neves de62664908 kernel: report /dev/random and /dev/urandom as character devices (#1140)
kernel: report entropy sources as character devices

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-08 03:35:58 +04:00
Nawid3333 34f6fd1f5f tests: cover the untested 16-bit integer ALU opcodes (#1151)
* tests: cover the 16-bit integer VOPC compares

V_CMP_{EQ,LE,GT,NE}_I16 and V_CMP_{EQ,LE,GT,NE,GE}_U16 decode and translate but
no test exercised them. VectorCompareI16U16Ops compares low halves that
differ in sign interpretation (0x8000 vs 0x7fff) with both the signed and the
unsigned opcodes, and uses operands whose high halves differ to check that only
the low 16 bits are compared. It runs in the default list and with
--int16-compare-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tests: cover the 16-bit integer VOP3 ALU ops

V_SUB_NC_U16, V_ADD_NC_I16, V_ASHRREV_I16, V_MAX_U16 and V_MIN_U16 decode and
translate but no test exercised them. Vop3Int16Ops checks wraparound without
clamp, the shift amount taken from the low four bits of src0, unsigned
ordering for max/min on 0x8000 vs 0x7fff, that the high halves of the sources
are ignored and that the high half of the destination is preserved. It runs in
the default list and with --int16-alu-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tests: cover the packed 16-bit integer VOP3P ops

V_PK_MAD_I16, V_PK_MAD_U16, V_PK_MUL_LO_U16, V_PK_ADD_I16, V_PK_SUB_U16,
V_PK_LSHLREV_B16, V_PK_LSHRREV_B16, V_PK_ASHRREV_I16, V_PK_MIN_I16, V_PK_MAX_U16
and V_PK_MIN_U16 decode and translate but no test exercised them. Vop3pInt16Ops
runs each on both halves with the default half selection: wraparound without
clamp, shift amounts from the low four bits of each src0 half, and min/max on
0x8000 vs 0x0001 so signed and unsigned ordering give different results. It
runs in the default list and with --int16-alu-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 03:31:46 +04:00
Romain Hedouin 79a91ecfaf graphics: cache the macOS thread id in TrackingSpinLock (#1169)
Every lock and unlock called pthread_mach_thread_np. Cache it per thread like the Linux branch does.
2026-10-08 03:18:49 +04:00
nmzik 420bb51d6c fix regression 9f83fd5f0c 2026-10-08 02:42:14 +04:00
22 changed files with 979 additions and 103 deletions
+3 -1
View File
@@ -60,7 +60,9 @@ private:
return GetCurrentThreadId();
#elif defined(__APPLE__)
// mach thread port is a nonzero per-thread id (0 is the "no owner" sentinel).
return static_cast<uint32_t>(pthread_mach_thread_np(pthread_self()));
static thread_local const uint32_t tid =
static_cast<uint32_t>(pthread_mach_thread_np(pthread_self()));
return tid;
#elif defined(__linux__)
static thread_local const uint32_t tid = static_cast<uint32_t>(::syscall(SYS_gettid));
return tid;
@@ -1284,10 +1284,10 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
: ResolveDrawOffsets(ucfg.GetIndexOffset(), state.vertex_info[0]);
DrawEmitInfo emit {};
// Native mesh prologs consume the offsets written to the packet's user SGPRs.
const bool mesh_indirect = indirect && state.vertex_info[0].logical_stage == ShaderType::Mesh;
emit.vertex_offset = mesh_indirect ? 0 : vertex_offset + args.base_vertex;
emit.first_instance = mesh_indirect ? 0 : instance_offset;
// Native fetches consume the patched SGPRs; rewritten fetches need Vulkan offsets.
const bool native_indirect = indirect && !state.vertex_info[0].fetch_embedded;
emit.vertex_offset = native_indirect ? 0 : vertex_offset + args.base_vertex;
emit.first_instance = native_indirect ? 0 : instance_offset;
ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source,
primitive_restart);
@@ -1375,10 +1375,10 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
indirect ? std::pair<int32_t, uint32_t> {0, args.first_instance}
: ResolveDrawOffsets(ucfg.GetIndexOffset(), state.vertex_info[0]);
DrawEmitInfo emit {};
const bool mesh_indirect = indirect && state.vertex_info[0].logical_stage == ShaderType::Mesh;
emit.first_vertex = mesh_indirect ? 0 :
const bool native_indirect = indirect && !state.vertex_info[0].fetch_embedded;
emit.first_vertex = native_indirect ? 0 :
static_cast<uint32_t>(vertex_offset + static_cast<int32_t>(args.first_vertex));
emit.first_instance = mesh_indirect ? 0 : instance_offset;
emit.first_instance = native_indirect ? 0 : instance_offset;
DrawIndexBufferSource index_source {};
ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source, false);
+30 -15
View File
@@ -70,6 +70,7 @@ struct File {
std::atomic_bool writable;
std::atomic_bool append;
std::atomic_bool sync_writes;
std::atomic_bool close_on_exec;
SpecialFile special;
Common::Mutex mutex;
std::vector<uint8_t> dirents;
@@ -449,11 +450,12 @@ int KYTY_SYSV_ABI KernelOpen(const char* path, int flags, uint16_t mode) {
EXIT_IF(file == nullptr || file->opened || file->directory);
file->name = path;
file->readable = rw_mode != Common::File::Mode::Write;
file->writable = rw_mode != Common::File::Mode::Read;
file->append = append;
file->sync_writes = fsync || sync || dsync;
file->name = path;
file->readable = rw_mode != Common::File::Mode::Write;
file->writable = rw_mode != Common::File::Mode::Read;
file->append = append;
file->sync_writes = fsync || sync || dsync;
file->close_on_exec = (static_cast<uint32_t>(flags) & 0x00100000u) != 0;
if (!directory && IsRandomDevice(file->name)) {
file->real_name = file->name;
@@ -585,6 +587,27 @@ int KYTY_SYSV_ABI KernelClose(int d) {
return OK;
}
int KYTY_SYSV_ABI KernelFcntl(int d, int command, int arg) {
PRINT_NAME();
if (d < DESCRIPTOR_MIN) {
return KERNEL_ERROR_EBADF;
}
auto* file = g_files->GetFile(d);
if (file == nullptr || !file->opened) {
return KERNEL_ERROR_EBADF;
}
switch (command) {
case 1: return file->close_on_exec.load(std::memory_order_relaxed); // F_GETFD
case 2: // F_SETFD, FD_CLOEXEC
file->close_on_exec.store((arg & 1) != 0, std::memory_order_relaxed);
return OK;
default: return KERNEL_ERROR_EINVAL;
}
}
int64_t KYTY_SYSV_ABI KernelRead(int d, void* buf, size_t nbytes) {
PRINT_NAME();
@@ -1138,19 +1161,11 @@ int KYTY_SYSV_ABI KernelFstat(int d, FileStat* sb) {
auto wt = at;
if (file->special != SpecialFile::None) {
stat.st_mode = 0020644u;
stat.st_size = 0;
stat.st_blksize = 512;
stat.st_blocks = 0;
SecToTimespec(&stat.st_atim, at.ToUnix());
SecToTimespec(&stat.st_mtim, wt.ToUnix());
stat.st_ctim = stat.st_atim;
stat.st_birthtim = stat.st_mtim;
*sb = stat;
return OK;
}
if (!file->directory) {
} else if (!file->directory) {
file->mutex.Lock();
bool is_invalid = file->f.IsInvalid();
+1
View File
@@ -59,6 +59,7 @@ std::filesystem::path GetRealFilename(const std::string& mounted_file_name);
int KYTY_SYSV_ABI KernelOpen(const char* path, int flags, uint16_t mode);
int KYTY_SYSV_ABI KernelClose(int d);
int KYTY_SYSV_ABI KernelFcntl(int d, int command, int arg);
int64_t KYTY_SYSV_ABI KernelRead(int d, void* buf, size_t nbytes);
int64_t KYTY_SYSV_ABI KernelPread(int d, void* buf, size_t nbytes, int64_t offset);
int64_t KYTY_SYSV_ABI KernelPreadv(int d, const KernelIovec* iov, int iovcnt, int64_t offset);
+8
View File
@@ -3217,6 +3217,14 @@ uint32_t KYTY_SYSV_ABI AgcAcbJumpGetSize() {
return 0x10u;
}
uint32_t* KYTY_SYSV_ABI AgcAcbRewind(CommandBuffer* buf, uint8_t initial_state, uint8_t offload) {
auto* cmd = AgcDcbRewind(buf, initial_state);
if (cmd != nullptr) {
cmd[1] |= (static_cast<uint32_t>(offload & 0x1u) << 24u);
}
return cmd;
}
uint32_t* KYTY_SYSV_ABI AgcAcbWaitRegMem(CommandBuffer* buf, uint8_t size, uint8_t compare_function,
uint8_t cache_policy, const volatile void* address,
uint64_t reference, uint64_t mask, uint32_t poll_cycles) {
+1
View File
@@ -206,6 +206,7 @@ uint32_t KYTY_SYSV_ABI AgcAcbCondExecGetSize();
uint32_t* KYTY_SYSV_ABI AgcAcbJump(CommandBuffer* buf, uint8_t cache_policy,
const uint32_t* target, uint32_t size_in_dwords);
uint32_t KYTY_SYSV_ABI AgcAcbJumpGetSize();
uint32_t* KYTY_SYSV_ABI AgcAcbRewind(CommandBuffer* buf, uint8_t initial_state, uint8_t offload);
uint32_t* KYTY_SYSV_ABI AgcAcbWaitRegMem(CommandBuffer* buf, uint8_t size, uint8_t compare_function,
uint8_t cache_policy, const volatile void* address,
uint64_t reference, uint64_t mask, uint32_t poll_cycles);
+89
View File
@@ -67,8 +67,13 @@ struct PadTriggerEffectParam {
PadTriggerEffectCommand command[2];
};
struct PadTriggerEffectStateInformation {
int32_t state[2];
};
static_assert(sizeof(PadTriggerEffectCommand) == 56);
static_assert(sizeof(PadTriggerEffectParam) == 120);
static_assert(sizeof(PadTriggerEffectStateInformation) == 8);
struct DualSenseEffects {
uint8_t enable_bits;
@@ -118,6 +123,7 @@ public:
int GetActiveControllerId();
void SetLightBar(uint8_t r, uint8_t g, uint8_t b);
bool SetTriggerEffect(const PadTriggerEffectParam& param);
void GetTriggerEffectState(PadTriggerEffectStateInformation* info);
void CycleSetting(Setting setting);
float GetSettingScale(Setting setting) const;
void ReadState(ControllerState* state, bool* flag, int* count);
@@ -131,6 +137,7 @@ private:
// Apply cached output without changing its game-requested lifetime; caller holds m_mutex.
void ApplyVibration();
bool SendTriggerEffect(const PadTriggerEffectParam& param);
void UpdateTriggerEffectState();
Common::Mutex m_mutex;
std::vector<int> m_connected_ids;
@@ -152,6 +159,7 @@ private:
std::array<uint8_t, 2> m_vibration {};
uint64_t m_vibration_until = 0;
PadTriggerEffectParam m_trigger_effect {};
PadTriggerEffectStateInformation m_trigger_state {};
// Setting changes share the output lock; audio only needs an atomic scale snapshot.
std::array<std::atomic<uint32_t>, 3> m_setting_steps {};
};
@@ -330,6 +338,51 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command,
}
}
enum TriggerState : int32_t {
TRIGGER_STATE_OFF = 0,
TRIGGER_STATE_FEEDBACK_STANDBY = 1,
TRIGGER_STATE_FEEDBACK_ACTIVE = 2,
TRIGGER_STATE_WEAPON_STANDBY = 3,
TRIGGER_STATE_WEAPON_PULLING = 4,
TRIGGER_STATE_WEAPON_FIRED = 5,
TRIGGER_STATE_VIBRATION_STANDBY = 6,
TRIGGER_STATE_VIBRATION_ACTIVE = 7,
};
static int32_t TriggerEffectState(const PadTriggerEffectCommand& command, int value,
int32_t previous) {
// Trigger positions use nonuniform travel ranges, with endpoints at 0 and 255.
const int pos = value == 0 ? 0 : value == 255 ? 9 : 1 + value / 32;
switch (command.mode) {
case 1: // feedback: resistance from position data[0], strength data[1]
return command.data[1] != 0 && pos >= command.data[0] ? TRIGGER_STATE_FEEDBACK_ACTIVE
: TRIGGER_STATE_FEEDBACK_STANDBY;
case 2: // weapon: resistance from data[0], released at data[1]
if (value < command.data[0] * 32) {
return TRIGGER_STATE_WEAPON_STANDBY;
}
return previous == TRIGGER_STATE_WEAPON_FIRED ||
value >= std::min(command.data[1] * 32, 254)
? TRIGGER_STATE_WEAPON_FIRED
: TRIGGER_STATE_WEAPON_PULLING;
case 3: // vibration from position data[0], amplitude data[1], frequency data[2]
return command.data[1] != 0 && command.data[2] != 0 && pos >= command.data[0]
? TRIGGER_STATE_VIBRATION_ACTIVE
: TRIGGER_STATE_VIBRATION_STANDBY;
case 4: // multiple-position feedback: strength per position
return command.data[pos] != 0 ? TRIGGER_STATE_FEEDBACK_ACTIVE
: TRIGGER_STATE_FEEDBACK_STANDBY;
case 5: // slope feedback between data[0] and data[1]
return pos >= command.data[0] && pos < command.data[1] ? TRIGGER_STATE_FEEDBACK_ACTIVE
: TRIGGER_STATE_FEEDBACK_STANDBY;
case 6: // multiple-position vibration: frequency data[0], amplitude per position
return command.data[0] != 0 && command.data[1 + pos] != 0
? TRIGGER_STATE_VIBRATION_ACTIVE
: TRIGGER_STATE_VIBRATION_STANDBY;
default: return TRIGGER_STATE_OFF;
}
}
void Initialize() {
EXIT_IF(g_controller != nullptr);
@@ -419,6 +472,7 @@ void GameController::CheckActive() {
m_vibration = {};
m_vibration_until = 0;
m_trigger_effect = {};
m_trigger_state = {};
}
void GameController::AddState() {
@@ -466,6 +520,7 @@ void GameController::Axis(int id, Controller::Axis axis, int value) {
}
if (trigger != 0) {
m_state.buttons = value > 0 ? m_state.buttons | trigger : m_state.buttons & ~trigger;
UpdateTriggerEffectState();
}
AddState();
@@ -622,6 +677,7 @@ void GameController::ResetInputState() {
m_state.buttons = 0;
std::fill_n(m_state.axes, 4, 128);
std::fill_n(m_state.axes + 4, 2, 0);
UpdateTriggerEffectState();
for (auto& touch: m_state.touch) {
touch = {};
}
@@ -730,9 +786,25 @@ bool GameController::SetTriggerEffect(const PadTriggerEffectParam& param) {
m_trigger_effect.command[i] = param.command[i];
}
}
UpdateTriggerEffectState();
return true;
}
void GameController::GetTriggerEffectState(PadTriggerEffectStateInformation* info) {
Common::LockGuard lock(m_mutex);
*info = m_trigger_state;
}
void GameController::UpdateTriggerEffectState() {
// Track every input event so a weapon remains fired even when the game polls after release
// starts.
for (int i = 0; i < 2; i++) {
m_trigger_state.state[i] = TriggerEffectState(
m_trigger_effect.command[i], m_state.axes[static_cast<int>(Axis::TriggerLeft) + i],
m_trigger_state.state[i]);
}
}
bool GameController::SendTriggerEffect(const PadTriggerEffectParam& param) {
if ((param.trigger_mask & ~0x03u) != 0) {
return false;
@@ -1098,4 +1170,21 @@ int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* p
return g_controller->SetTriggerEffect(*param) ? OK : PAD_ERROR_INVALID_ARG;
}
int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle, PadTriggerEffectStateInformation* info) {
PRINT_NAME();
LOGF("\t handle = %d\n", handle);
if (handle != 1) {
return PAD_ERROR_INVALID_HANDLE;
}
if (info == nullptr) {
return PAD_ERROR_INVALID_ARG;
}
g_controller->GetTriggerEffectState(info);
return OK;
}
} // namespace Libs::Controller
+2
View File
@@ -53,6 +53,7 @@ struct PadData;
struct PadVibrationParam;
struct PadLightBarParam;
struct PadTriggerEffectParam;
struct PadTriggerEffectStateInformation;
inline int controller_get_axis(int min, int max, int value) {
int v = (255 * (value - min)) / (max - min);
@@ -92,6 +93,7 @@ int KYTY_SYSV_ABI PadReadState(int handle, PadData* data);
int KYTY_SYSV_ABI PadRead(int handle, PadData* data, int num);
int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param);
int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* param);
int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle, PadTriggerEffectStateInformation* info);
int KYTY_SYSV_ABI PadResetLightBar(int handle);
int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param);
+2
View File
@@ -74,6 +74,7 @@ LIB_DEFINE(InitAgcDriver_1) {
LIB_FUNC("ozKzBP4aki4", Gen5::AgcAcbCondExecGetSize);
LIB_FUNC("e1DFTg+Sd8U", Gen5::AgcAcbJump);
LIB_FUNC("b-oySn+G2tE", Gen5::AgcAcbJumpGetSize);
LIB_FUNC("DwICrVxerkY", Gen5::AgcAcbRewind);
LIB_FUNC("htn36gPnBk4", Gen5::AgcAcbWaitRegMem);
LIB_FUNC("idlaArvdXEs", Gen5::AgcAcbWaitOnAddressGetSize);
LIB_FUNC("-RnpfpxIhec", Gen5::AgcAcbDmaData);
@@ -156,6 +157,7 @@ LIB_DEFINE(InitAgcDriver_1) {
LIB_FUNC("k-JpyR2dYAM", Gen5::AgcCondExecPatchSetEnd);
LIB_FUNC("3ZWa3AoyWZQ", Gen5::AgcCondExecPatchSetCommandAddress);
LIB_FUNC("ziVA3whp3p4", Gen5::AgcRewindPatchSetRewindState);
LIB_FUNC("eWaWyFegzgQ", Gen5::AgcRewindPatchSetRewindState);
LIB_FUNC("YUeqkyT7mEQ", Gen5::AgcDcbSetFlip);
}
+51 -18
View File
@@ -1,4 +1,5 @@
#include "common/abi.h"
#include "common/assert.h"
#include "common/dateTime.h"
#include "common/file.h"
#include "common/logging/log.h"
@@ -649,10 +650,15 @@ struct CommandBufferState {
uint64_t address = 0;
uint64_t value = 0;
};
struct WaitAddressCommand {
uint64_t address;
struct WaitCommand {
uint64_t source;
uint64_t value;
uint8_t compare;
bool is_counter;
};
struct WriteCounterCommand {
uint8_t index;
uint32_t value;
};
struct AmmMapCommand {
AmmCommandKind kind = AmmCommandKind::MapAuto;
@@ -664,7 +670,7 @@ struct CommandBufferState {
uint8_t gpu_mask_id = 0;
};
using CommandData = std::variant<ReadFileCommand, KernelEventCommand, WriteAddressCommand,
AmmMapCommand, WaitAddressCommand>;
AmmMapCommand, WaitCommand, WriteCounterCommand>;
struct Command {
uint64_t record_offset;
CommandData data;
@@ -689,6 +695,8 @@ static_assert(sizeof(AmmUsageStatsData) == 0x18);
static std::mutex g_command_buffer_mutex;
static std::unordered_map<uint64_t, CommandBufferState> g_command_buffers;
static std::unordered_map<uint64_t, uint64_t> g_command_buffer_aliases;
// AMM and APR share 256 32-bit counters; only the first 128 are application-writable.
static std::array<std::atomic<uint32_t>, 256> g_counters {};
using CommandBufferIterator = std::unordered_map<uint64_t, CommandBufferState>::iterator;
static void RegisterCommandBufferAliasLocked(uint64_t command_buffer, uint64_t buffer) {
@@ -1191,6 +1199,9 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) {
const auto& command = *payload;
std::atomic_ref(*reinterpret_cast<uint64_t*>(command.address))
.store(command.value, std::memory_order_release);
} else if (const auto* command =
std::get_if<CommandBufferState::WriteCounterCommand>(&entry.data)) {
g_counters[command->index].store(command->value, std::memory_order_release);
} else if (const auto* payload = std::get_if<CommandBufferState::AmmMapCommand>(&entry.data)) {
const auto& command = *payload;
int result = OK;
@@ -1219,17 +1230,20 @@ struct Submission {
uint32_t id = 0;
};
static bool IsWaitSatisfied(const CommandBufferState::WaitAddressCommand& wait) {
const uint64_t value =
std::atomic_ref(*reinterpret_cast<uint64_t*>(wait.address)).load(std::memory_order_acquire);
static bool IsWaitSatisfied(const CommandBufferState::WaitCommand& wait) {
const uint64_t value = wait.is_counter
? g_counters[wait.source].load(std::memory_order_acquire)
: std::atomic_ref(*reinterpret_cast<uint64_t*>(wait.source))
.load(std::memory_order_acquire);
const uint64_t sign = uint64_t {1} << (wait.is_counter ? 31 : 63);
switch (wait.compare) {
case 0: return value == wait.value;
case 1: return value > wait.value;
case 2: return value < wait.value;
case 3: return value != wait.value;
case 4: return static_cast<int64_t>(value - wait.value) >= 0;
case 5: return static_cast<int64_t>(value) > static_cast<int64_t>(wait.value);
default: return static_cast<int64_t>(value) < static_cast<int64_t>(wait.value);
case 4: return ((value - wait.value) & sign) == 0;
case 5: return (value ^ sign) > (wait.value ^ sign);
default: return (value ^ sign) < (wait.value ^ sign);
}
}
@@ -1263,7 +1277,7 @@ private:
pending = true;
auto& submission = queue.front();
if (submission.cursor < submission.commands.size()) {
const auto* wait = std::get_if<CommandBufferState::WaitAddressCommand>(
const auto* wait = std::get_if<CommandBufferState::WaitCommand>(
&submission.commands[submission.cursor].data);
if (wait != nullptr && !IsWaitSatisfied(*wait)) {
continue;
@@ -1911,18 +1925,27 @@ static int KYTY_SYSV_ABI CommandBufferWaitOnAddress(void* command_b
return LibKernel::KERNEL_ERROR_EINVAL;
}
return AppendCommandRecord(reinterpret_cast<uint64_t>(command_buffer),
CommandBufferState::WaitAddressCommand {
reinterpret_cast<uint64_t>(address), value, compare},
CommandBufferState::WaitCommand {
reinterpret_cast<uint64_t>(address), value, compare, false},
0x20)
? OK
: LibKernel::KERNEL_ERROR_EBUSY;
}
static int KYTY_SYSV_ABI CommandBufferWaitOnCounter(void* command_buffer, uint8_t, uint8_t,
uint64_t, uint8_t, uint8_t, uint64_t, uint8_t) {
static int KYTY_SYSV_ABI CommandBufferWaitOnCounter(void* command_buffer, uint8_t index,
uint8_t access, uint64_t value, uint8_t compare,
uint8_t mask_op, uint64_t, uint8_t flush) {
PRINT_NAME();
return AppendNoOpCommand(command_buffer, 0x20);
if (command_buffer == nullptr || access > 7 || compare > 6 || mask_op > 1 || flush > 1) {
return LibKernel::KERNEL_ERROR_EINVAL;
}
EXIT_NOT_IMPLEMENTED(access != 1 || mask_op != 0 || flush != 0 || value > UINT32_MAX);
return AppendCommandRecord(reinterpret_cast<uint64_t>(command_buffer),
CommandBufferState::WaitCommand {index, value, compare, true},
0x20)
? OK
: LibKernel::KERNEL_ERROR_EBUSY;
}
static int AppendWriteAddressCommand(void* command_buffer, volatile uint64_t* address,
@@ -1954,11 +1977,21 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressOnCompletion(void*
return AppendWriteAddressCommand(command_buffer, address, value);
}
static int KYTY_SYSV_ABI CommandBufferWriteCounter(void* command_buffer, uint8_t, uint8_t, uint64_t,
uint8_t, uint32_t) {
static int KYTY_SYSV_ABI CommandBufferWriteCounter(void* command_buffer, uint8_t index,
uint8_t access, uint64_t value,
uint8_t operation, uint32_t mode) {
PRINT_NAME();
return AppendNoOpCommand(command_buffer, 0x20);
if (command_buffer == nullptr || index >= 128 || access > 7 || operation > 4 || mode > 1) {
return LibKernel::KERNEL_ERROR_EINVAL;
}
EXIT_NOT_IMPLEMENTED(access != 1 || operation != 0 || mode != 0);
return AppendCommandRecord(reinterpret_cast<uint64_t>(command_buffer),
CommandBufferState::WriteCounterCommand {
index, static_cast<uint32_t>(value)},
0x20)
? OK
: LibKernel::KERNEL_ERROR_EBUSY;
}
static int KYTY_SYSV_ABI CommandBufferWriteCounterOnCompletion(void* command_buffer, uint8_t,
+2
View File
@@ -6,6 +6,7 @@
#include "common/stringUtils.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "libs/systemService.h"
#include "loader/symbolDatabase.h"
#include "loader/systemContent.h"
@@ -96,6 +97,7 @@ int KYTY_SYSV_ABI AppContentInitialize(const AppContentInitParam* init_param,
LOGF("\t TITLE_ID missing\n");
}
SystemService::NotifyEntitlementUpdate();
return OK;
}
+7
View File
@@ -1974,6 +1974,12 @@ int64_t KYTY_SYSV_ABI fstat(int d, LibKernel::FileSystem::FileStat* sb) {
return POSIX_N_CALL(LibKernel::FileSystem::KernelFstat(d, sb));
}
int KYTY_SYSV_ABI fcntl(int d, int command, int arg) {
PRINT_NAME();
return POSIX_N_CALL(LibKernel::FileSystem::KernelFcntl(d, command, arg));
}
int KYTY_SYSV_ABI ftruncate(int d, int64_t length) {
PRINT_NAME();
@@ -2163,6 +2169,7 @@ LIB_DEFINE(InitLibKernel_1_Posix) {
LIB_FUNC("E6ao34wPw+U", stat);
LIB_FUNC("JGMio+21L4c", mkdir);
LIB_FUNC("ih4CD9-gghM", Posix::ftruncate);
LIB_FUNC("8nY19bKoiZk", Posix::fcntl);
LIB_FUNC("pDuPEf3m4fI", Posix::sem_init);
LIB_FUNC("cDW233RAwWo", Posix::sem_destroy);
LIB_FUNC("YCV5dGGBcCo", Posix::sem_wait);
+1
View File
@@ -1559,6 +1559,7 @@ LIB_DEFINE(InitNet_1_NpManager) {
LIB_FUNC("rbknaUjpqWo", NpManager::NpGetAccountIdA);
LIB_FUNC("JT+t00a3TxA", NpManager::NpGetAccountCountryA);
LIB_FUNC("+4DegjBqV1g", NpManager::NpGetAccountAge);
LIB_FUNC("3Tcz5bNCfZQ", NpManager::NpGetAccountLanguage2);
LIB_FUNC("GpLQDNKICac", NpManager::NpCreateRequest);
LIB_FUNC("eiqMCt9UshI", NpManager::NpCreateAsyncRequest);
LIB_FUNC("S7QTn72PrDw", NpManager::NpDeleteRequest);
+1 -21
View File
@@ -22,26 +22,6 @@ static int KYTY_SYSV_ABI PadSetVibrationMode(int handle, int mode) {
return 0;
}
struct PadTriggerEffectStateInformation {
int32_t state[2];
};
static int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle,
PadTriggerEffectStateInformation* info) {
PRINT_NAME();
LOGF("\t handle = %d\n", handle);
if (info == nullptr) {
return -2137653243; /* 0x80960005 */
}
info->state[0] = 0;
info->state[1] = 0;
return 0;
}
static int KYTY_SYSV_ABI PadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool enabled) {
PRINT_NAME();
@@ -214,7 +194,7 @@ LIB_DEFINE(InitPad_1) {
LIB_FUNC("yFVnOdGxvZY", Controller::PadSetVibration);
LIB_FUNC("W2G-yoyMF5U", PadSetVibrationMode);
LIB_FUNC("Yq0zOH7YNOM", PadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse);
LIB_FUNC("znaWI0gpuo8", PadGetTriggerEffectState);
LIB_FUNC("znaWI0gpuo8", Controller::PadGetTriggerEffectState);
LIB_FUNC("DscD1i9HX1w", Controller::PadResetLightBar);
LIB_FUNC("RR4novUEENY", Controller::PadSetLightBar);
LIB_FUNC("6ncge5+l5Qs", PadClose);
+25 -1
View File
@@ -6,6 +6,9 @@
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
#include <mutex>
#include <unordered_map>
namespace Libs {
LIB_VERSION("Sysmodule", 1, "Sysmodule", 1, 1);
@@ -17,6 +20,16 @@ int KYTY_SYSV_ABI KernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleI
namespace Sysmodule {
constexpr int SYSMODULE_ERROR_UNLOADED = static_cast<int>(0x805a1001u);
static std::mutex g_module_mutex;
static std::unordered_map<uint16_t, uint32_t> g_module_refs;
static void LoadModule(uint16_t id) {
std::scoped_lock lock(g_module_mutex);
++g_module_refs[id];
}
static KYTY_SYSV_ABI int SysmoduleGetModuleInfoForUnwind(uint64_t addr, int flags,
LibKernel::ModuleInfoForUnwind* info) {
return LibKernel::KernelGetModuleInfoForUnwind(addr, flags, info);
@@ -27,6 +40,7 @@ static KYTY_SYSV_ABI int SysmoduleLoadModule(uint16_t id) {
LOGF("\t id = %d\n", static_cast<int>(id));
LoadModule(id);
return 0;
}
@@ -35,6 +49,14 @@ static KYTY_SYSV_ABI int SysmoduleUnloadModule(uint16_t id) {
LOGF("\t id = %d\n", static_cast<int>(id));
std::scoped_lock lock(g_module_mutex);
const auto module = g_module_refs.find(id);
if (module == g_module_refs.end()) {
return SYSMODULE_ERROR_UNLOADED;
}
if (--module->second == 0) {
g_module_refs.erase(module);
}
return 0;
}
@@ -49,6 +71,7 @@ static KYTY_SYSV_ABI int SysmoduleLoadModuleInternalWithArg(uint16_t id, int arg
EXIT_IF(arg3 != 0);
EXIT_IF(ret == nullptr);
LoadModule(id);
*ret = 0;
return 0;
@@ -59,7 +82,8 @@ static KYTY_SYSV_ABI int SysmoduleIsLoaded(uint16_t id) {
LOGF("\t id = %d\n", static_cast<int>(id));
return 0;
std::scoped_lock lock(g_module_mutex);
return g_module_refs.contains(id) ? 0 : SYSMODULE_ERROR_UNLOADED;
}
} // namespace Sysmodule
+23 -1
View File
@@ -7,9 +7,12 @@
#include "libs/dialog.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "libs/systemService.h"
#include "loader/symbolDatabase.h"
#include <cstring>
#include <deque>
#include <mutex>
namespace Libs {
@@ -70,6 +73,14 @@ struct SystemServiceEvent {
uint8_t data[8192];
};
static std::mutex g_event_mutex;
static std::deque<int32_t> g_events;
void NotifyEntitlementUpdate() {
std::scoped_lock lock(g_event_mutex);
g_events.push_back(0x10000003); // SCE_SYSTEM_SERVICE_EVENT_ENTITLEMENT_UPDATE
}
struct SystemServiceDisplaySafeAreaInfo {
float ratio;
uint8_t reserved[128];
@@ -153,7 +164,14 @@ static int KYTY_SYSV_ABI SystemServiceReceiveEvent(SystemServiceEvent* event) {
event->event_type = -1;
std::memset(event->data, 0, sizeof(event->data));
return SYSTEM_SERVICE_ERROR_NO_EVENT;
std::scoped_lock lock(g_event_mutex);
if (g_events.empty()) {
return SYSTEM_SERVICE_ERROR_NO_EVENT;
}
event->event_type = g_events.front();
g_events.pop_front();
LOGF("\t SystemService event = 0x%08x\n", static_cast<uint32_t>(event->event_type));
return OK;
}
static int KYTY_SYSV_ABI SystemServiceGetStatus(SystemServiceStatus* status) {
@@ -167,6 +185,10 @@ static int KYTY_SYSV_ABI SystemServiceGetStatus(SystemServiceStatus* status) {
*status = SystemServiceStatus();
status->is_system_ui_overlaid = dialog.active;
status->is_in_background_execution = dialog.background;
{
std::scoped_lock lock(g_event_mutex);
status->event_num = static_cast<int32_t>(g_events.size());
}
return OK;
}
+25 -11
View File
@@ -832,10 +832,6 @@ namespace Net {
LIB_NAME("Net", "Net");
struct NetEtherAddr {
uint8_t data[6] = {0};
};
#if defined(_WIN32)
using NativeSocket = SOCKET;
static constexpr NativeSocket INVALID_NATIVE_SOCKET = INVALID_SOCKET;
@@ -1564,14 +1560,12 @@ const char* KYTY_SYSV_ABI NetInetNtop(int af, const void* src, char* dst, uint32
int KYTY_SYSV_ABI NetEtherNtostr(const NetEtherAddr* n, char* str, size_t len) {
PRINT_NAME();
NetEtherAddr zero {};
if (n == nullptr || str == nullptr || len < 18) {
return NET_ERROR_EINVAL;
}
EXIT_NOT_IMPLEMENTED(len != 18);
EXIT_NOT_IMPLEMENTED(n == nullptr);
EXIT_NOT_IMPLEMENTED(str == nullptr);
EXIT_NOT_IMPLEMENTED(memcmp(n->data, zero.data, sizeof(zero.data)) != 0);
strcpy(str, "00:00:00:00:00:00"); // NOLINT
std::snprintf(str, len, "%02x:%02x:%02x:%02x:%02x:%02x", n->data[0], n->data[1],
n->data[2], n->data[3], n->data[4], n->data[5]);
return OK;
}
@@ -4112,6 +4106,26 @@ int KYTY_SYSV_ABI NpGetAccountAge(int req_id, int user_id, uint8_t* age) {
return np_error_signed_out;
}
int KYTY_SYSV_ABI NpGetAccountLanguage2(int req_id, int user_id, void* language_code) {
PRINT_NAME();
LOGF("\t req_id = %d\n", req_id);
LOGF("\t user_id = %d\n", user_id);
LOGF("\t language_code = 0x%016" PRIx64 "\n", reinterpret_cast<uint64_t>(language_code));
if (req_id <= 0 || language_code == nullptr) {
return np_error_invalid_argument;
}
std::lock_guard lock(g_np_request_mutex);
if (np_get_request_locked(req_id) == nullptr) {
return np_error_request_not_found;
}
return np_error_signed_out;
}
int KYTY_SYSV_ABI NpCreateRequest() {
PRINT_NAME();
+4 -1
View File
@@ -16,7 +16,9 @@ struct Lifecycle {
namespace Net {
struct NetEtherAddr;
struct NetEtherAddr {
uint8_t data[6] = {0};
};
union NetEpollData {
void* ptr;
@@ -185,6 +187,7 @@ int KYTY_SYSV_ABI NpGetOnlineId(int user_id, NpOnlineId* online_id);
int KYTY_SYSV_ABI NpGetAccountIdA(int user_id, uint64_t* account_id);
int KYTY_SYSV_ABI NpGetAccountCountryA(int user_id, void* country_code);
int KYTY_SYSV_ABI NpGetAccountAge(int req_id, int user_id, uint8_t* age);
int KYTY_SYSV_ABI NpGetAccountLanguage2(int req_id, int user_id, void* language_code);
int KYTY_SYSV_ABI NpCreateRequest();
int KYTY_SYSV_ABI NpCreateAsyncRequest(const NpCreateAsyncRequestParameter* param);
int KYTY_SYSV_ABI NpDeleteRequest(int req_id);
+10
View File
@@ -0,0 +1,10 @@
#ifndef EMULATOR_LIBS_SYSTEMSERVICE_H_
#define EMULATOR_LIBS_SYSTEMSERVICE_H_
namespace Libs::SystemService {
void NotifyEntitlementUpdate();
} // namespace Libs::SystemService
#endif
+157
View File
@@ -343,6 +343,162 @@ void TestIndependentOutputsAndPadSwitch() {
"released controller retained a cached vibration");
}
}
void TestTriggerEffectState() {
Controller controller;
PadTriggerEffectStateInformation info {{-1, -1}};
const auto check_state = [&](int32_t left, int32_t right, const char* message) {
Check(PadGetTriggerEffectState(1, &info) == OK && info.state[0] == left &&
info.state[1] == right,
message);
};
const auto axis = [&](int left, int right) {
SetAxis(1, Axis::TriggerLeft, left);
SetAxis(1, Axis::TriggerRight, right);
};
PadTriggerEffectParam param {};
const auto set_effect = [&] {
Check(PadSetTriggerEffect(1, &param) == OK, "trigger request failed");
};
Check(PadGetTriggerEffectState(2, &info) == PAD_ERROR_INVALID_HANDLE && info.state[0] == -1 &&
info.state[1] == -1,
"invalid trigger handle changed the output");
Check(PadGetTriggerEffectState(2, nullptr) == PAD_ERROR_INVALID_HANDLE,
"trigger state checked the output before the handle");
Check(PadGetTriggerEffectState(1, nullptr) == PAD_ERROR_INVALID_ARG,
"trigger state accepted a null output");
check_state(0, 0, "unset trigger effects did not report off");
param.trigger_mask = 3;
param.command[0].mode = 1;
param.command[0].data[0] = 7;
param.command[0].data[1] = 8;
param.command[1].mode = 2;
param.command[1].data[0] = 2;
param.command[1].data[1] = 7;
param.command[1].data[2] = 6;
set_effect();
axis(191, 63);
check_state(1, 3, "triggers activated before the documented start positions");
axis(192, 64);
check_state(2, 4, "triggers did not activate at the documented start positions");
axis(255, 223);
check_state(2, 4, "weapon fired before its end position");
SetAxis(1, Axis::TriggerRight, 224);
SetAxis(1, Axis::TriggerRight, 100);
check_state(2, 5, "unpolled weapon firing was lost on partial release");
check_state(2, 5, "reading trigger state changed the weapon latch");
set_effect();
check_state(2, 5, "repeating a weapon request rearmed a held trigger");
CycleSetting(Setting::TriggerEffectIntensity);
check_state(2, 5, "muting trigger effects changed their reported states");
// A rejected two-trigger request must leave both cached commands and states intact.
auto invalid = param;
invalid.command[0].mode = 0;
invalid.command[1].data[1] = 2;
Check(PadSetTriggerEffect(1, &invalid) == PAD_ERROR_INVALID_ARG,
"invalid weapon end position was accepted");
SetAxis(1, Axis::TriggerLeft, 255);
check_state(2, 5, "an invalid request changed cached trigger commands or states");
SetAxis(1, Axis::TriggerRight, 63);
SetAxis(1, Axis::TriggerRight, 100);
check_state(2, 4, "unpolled release did not rearm the weapon");
param.trigger_mask = 2;
param.command[1].data[1] = 8;
set_effect();
axis(255, 253);
check_state(2, 4, "weapon end position 8 fired before output 254");
SetAxis(1, Axis::TriggerRight, 254);
check_state(2, 5, "weapon end position 8 did not fire at output 254");
// Masked updates preserve the other trigger, including its fired state.
param.trigger_mask = 1;
param.command[0].data[0] = 9;
set_effect();
SetAxis(1, Axis::TriggerLeft, 254);
check_state(1, 5, "feedback position 9 activated before full travel");
SetAxis(HOST_INPUT_CONTROLLER_ID, Axis::TriggerLeft, 255);
check_state(2, 5, "keyboard trigger travel did not activate feedback");
param.command[0].data[0] = 0;
set_effect();
SetAxis(1, Axis::TriggerLeft, 0);
check_state(2, 5, "feedback at position 0 was suppressed by a dead zone");
param.command[0].data[1] = 0;
set_effect();
check_state(1, 5, "zero-strength feedback reported active");
param.command[0] = {};
param.command[0].mode = 4;
param.command[0].data[0] = 4;
param.command[0].data[2] = 4;
param.command[0].data[8] = 4;
set_effect();
const int feedback_cases[][2] = {{0, 2}, {1, 1}, {31, 1}, {32, 2}, {63, 2},
{64, 1}, {223, 1}, {224, 2}, {254, 2}, {255, 1}};
for (const auto& test: feedback_cases) {
SetAxis(1, Axis::TriggerLeft, test[0]);
check_state(test[1], 5, "multi-position feedback used the wrong position");
}
param.command[0] = {};
param.command[0].mode = 5;
param.command[0].data[0] = 3;
param.command[0].data[1] = 7;
param.command[0].data[2] = 2;
param.command[0].data[3] = 6;
set_effect();
const int slope_cases[][2] = {{63, 1}, {64, 2}, {191, 2}, {192, 1}, {255, 1}};
for (const auto& test: slope_cases) {
SetAxis(1, Axis::TriggerLeft, test[0]);
check_state(test[1], 5, "slope feedback did not respect its start and end");
}
param.command[0].data[0] = 0;
set_effect();
SetAxis(1, Axis::TriggerLeft, 0);
check_state(2, 5, "slope feedback at position 0 was suppressed");
param.command[0] = {};
param.command[0].mode = 3;
param.command[0].data[0] = 6;
param.command[0].data[1] = 4;
param.command[0].data[2] = 20;
set_effect();
SetAxis(1, Axis::TriggerLeft, 159);
check_state(6, 5, "vibration activated before position 6");
SetAxis(1, Axis::TriggerLeft, 160);
check_state(7, 5, "vibration did not activate at position 6");
param.command[0].data[0] = 0;
set_effect();
SetAxis(1, Axis::TriggerLeft, 0);
check_state(7, 5, "vibration at position 0 was suppressed");
param.command[0].data[2] = 0;
set_effect();
check_state(6, 5, "zero-frequency vibration reported active");
param.command[0] = {};
param.command[0].mode = 6;
param.command[0].data[0] = 20;
param.command[0].data[10] = 4;
set_effect();
SetAxis(1, Axis::TriggerLeft, 254);
check_state(6, 5, "multi-position vibration activated before position 9");
SetAxis(1, Axis::TriggerLeft, 255);
check_state(7, 5, "multi-position vibration did not activate at position 9");
param.command[0].data[0] = 0;
set_effect();
check_state(6, 5, "zero-frequency multi-position vibration reported active");
ResetInputState();
check_state(6, 3, "input reset retained a fired weapon");
param.command[0].mode = 0;
set_effect();
check_state(0, 3, "off mode retained an effect state");
Connect(2);
Disconnect(1);
check_state(0, 0, "switching controllers retained effect states");
}
} // namespace
int main() {
@@ -353,6 +509,7 @@ int main() {
TestVibrationLifetime();
TestMaskedTriggersAndValidation();
TestIndependentOutputsAndPadSwitch();
TestTriggerEffectState();
Config::Shutdown();
return 0;
}
+231
View File
@@ -45,6 +45,9 @@ void InitLibKernel_1_Apr(Loader::SymbolDatabase *symbols);
namespace Libs {
void InitLibKernel_1(Loader::SymbolDatabase *symbols);
void InitSysmodule_1(Loader::SymbolDatabase *symbols);
void InitSystemService_1(Loader::SymbolDatabase *symbols);
void InitAppContent_1(Loader::SymbolDatabase *symbols);
}
namespace Libs::LibAmpr {
@@ -121,6 +124,138 @@ void CheckSaveRename(const std::filesystem::path &root,
"renamed save contents");
}
void TestRandomDevices() {
for (const auto* path : {"/dev/urandom", "/dev/random"}) {
const int fd = FileSystem::KernelOpen(path, 0, 0);
FileSystem::FileStat stat {};
Check(fd >= 3 && FileSystem::KernelFstat(fd, &stat) == OK &&
(stat.st_mode & 0170000) == 0020000,
"entropy sources are character devices");
std::array<uint8_t, 32> entropy {};
Check(FileSystem::KernelRead(fd, entropy.data(), entropy.size()) == entropy.size(),
"character device supplies requested entropy");
Check(FileSystem::KernelClose(fd) == OK, "close entropy source");
}
}
void TestFileDescriptorFlags() {
Loader::SymbolDatabase symbols;
Libs::InitLibKernel_1(&symbols);
const auto* symbol = symbols.Find(
{"8nY19bKoiZk", "Posix", 1, "libkernel", 1, 1, Loader::SymbolType::Func});
Check(symbol != nullptr, "POSIX fcntl export resolves");
const auto fcntl = reinterpret_cast<int (KYTY_SYSV_ABI *)(int, int, int)>(symbol->vaddr);
Check(fcntl(std::numeric_limits<int>::min(), 1, 0) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF,
"invalid descriptor reports EBADF");
for (const auto* path : {"/dev/urandom", "/dev/random"}) {
const int fd = FileSystem::KernelOpen(path, 0, 0);
Check(fd >= 3, "open descriptor for flag checks");
Check(fcntl(fd, 1, 0) == 0 && fcntl(fd, 2, 1) == 0 && fcntl(fd, 1, 0) == 1,
"entropy descriptor retains close-on-exec flag");
Check(fcntl(fd, 2, 0) == 0 && fcntl(fd, 1, 0) == 0,
"close-on-exec flag can be cleared");
Check(fcntl(fd, -1, 0) == -1 && *Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
"unsupported fcntl command reports EINVAL");
Check(FileSystem::KernelClose(fd) == OK, "close entropy source");
Check(fcntl(fd, 1, 0) == -1 && *Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF,
"closed descriptor reports EBADF");
const int cloexec_fd = FileSystem::KernelOpen(path, 0x00100000, 0);
Check(cloexec_fd >= 3 && fcntl(cloexec_fd, 1, 0) == 1,
"O_CLOEXEC sets the descriptor flag on open");
Check(FileSystem::KernelClose(cloexec_fd) == OK, "close flagged entropy source");
}
}
void TestSysmoduleReferences() {
Loader::SymbolDatabase symbols;
Libs::InitSysmodule_1(&symbols);
using ModuleCall = int (KYTY_SYSV_ABI *)(uint16_t);
const auto resolve = [&](const char* nid) {
const auto* symbol = symbols.Find(
{nid, "Sysmodule", 1, "Sysmodule", 1, 1, Loader::SymbolType::Func});
Check(symbol != nullptr, "Sysmodule export resolves");
return reinterpret_cast<ModuleCall>(symbol->vaddr);
};
const auto load = resolve("g8cM39EUZ6o");
const auto unload = resolve("eR2bZFAAU0Q");
const auto is_loaded = resolve("fMP5NHUOaMk");
constexpr int unloaded = static_cast<int>(0x805a1001u);
Check(load(0x113) == OK && is_loaded(0xb4) == unloaded,
"loading entitlement access does not mark AppContent loaded");
Check(unload(0x113) == OK, "release independent module reference");
for (const uint16_t id : {0x113, 0xb4}) {
Check(is_loaded(id) == unloaded, "unloaded module allows guest initialization");
Check(unload(id) == unloaded, "unloading an absent module reports UNLOADED");
Check(load(id) == OK && load(id) == OK && is_loaded(id) == OK,
"repeated loads retain references");
Check(unload(id) == OK && is_loaded(id) == OK,
"one unload preserves the remaining reference");
Check(unload(id) == OK && is_loaded(id) == unloaded,
"last unload restores unloaded status");
}
const auto* internal_symbol = symbols.Find(
{"hHrGoGoNf+s", "Sysmodule", 1, "Sysmodule", 1, 1, Loader::SymbolType::Func});
Check(internal_symbol != nullptr, "internal Sysmodule load export resolves");
const auto internal_load =
reinterpret_cast<int (KYTY_SYSV_ABI *)(uint16_t, int, int, int, int*)>(
internal_symbol->vaddr);
int result = -1;
Check(internal_load(0xb4, 0, 0, 0, &result) == OK && result == OK &&
is_loaded(0xb4) == OK && unload(0xb4) == OK && is_loaded(0xb4) == unloaded,
"internal and public module operations share load state");
}
void TestSystemServiceEntitlementEvents() {
Loader::SymbolDatabase symbols;
Libs::InitSystemService_1(&symbols);
Libs::InitAppContent_1(&symbols);
const auto resolve = [&](const char* nid, const char* library, const char* module) {
const auto* symbol = symbols.Find(
{nid, library, 1, module, 1, 1, Loader::SymbolType::Func});
Check(symbol != nullptr, "entitlement event export resolves");
return symbol->vaddr;
};
const auto initialize = reinterpret_cast<int (KYTY_SYSV_ABI *)(const void*, void*)>(
resolve("R9lA82OraNs", "AppContent", "AppContentUtil"));
struct Status {
int32_t event_num;
bool overlay, background, vr;
uint8_t reserved[127];
};
struct Event {
int32_t type;
uint8_t data[8192];
};
static_assert(sizeof(Status) == 136 && sizeof(Event) == 8196);
const auto get_status = reinterpret_cast<int (KYTY_SYSV_ABI *)(Status*)>(
resolve("rPo6tV8D9bM", "SystemService", "SystemService"));
const auto receive = reinterpret_cast<int (KYTY_SYSV_ABI *)(Event*)>(
resolve("656LMQSrg6U", "SystemService", "SystemService"));
Status status {};
Event event {};
Check(get_status(&status) == OK && status.event_num == 0,
"SystemService starts without pending events");
std::array<uint8_t, 32> init {};
std::array<uint8_t, 40> boot {};
Check(initialize(init.data(), boot.data()) == OK,
"AppContent initialization generates an entitlement notification");
Check(get_status(&status) == OK && status.event_num == 1 &&
get_status(&status) == OK && status.event_num == 1,
"status queries preserve pending notifications");
Check(receive(nullptr) == Libs::SystemService::SYSTEM_SERVICE_ERROR_PARAMETER &&
get_status(&status) == OK && status.event_num == 1,
"invalid event output does not consume a notification");
std::memset(&event, 0xff, sizeof(event));
Check(receive(&event) == OK && event.type == 0x10000003 &&
std::all_of(std::begin(event.data), std::end(event.data),
[](uint8_t byte) { return byte == 0; }) &&
get_status(&status) == OK && status.event_num == 0,
"receive delivers the entitlement event with cleared payload exactly once");
Check(receive(&event) == Libs::SystemService::SYSTEM_SERVICE_ERROR_NO_EVENT,
"empty event queue reports NO_EVENT");
}
void TestSaveOpenVisibility() {
constexpr char Path[] = "/savedata0/visible-save.dat";
constexpr char Payload[] = "saved progress";
@@ -669,6 +804,10 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
using Offset = uint64_t (KYTY_SYSV_ABI *)(void *);
using WaitAddress = int (KYTY_SYSV_ABI *)(void *, volatile uint64_t *, uint64_t,
uint8_t, uint8_t);
using WaitCounter = int (KYTY_SYSV_ABI *)(void *, uint8_t, uint8_t, uint64_t,
uint8_t, uint8_t, uint64_t, uint8_t);
using WriteCounter = int (KYTY_SYSV_ABI *)(void *, uint8_t, uint8_t, uint64_t,
uint8_t, uint32_t);
using WriteAddress = int (KYTY_SYSV_ABI *)(void *, volatile uint64_t *, uint64_t);
using ReadFile = int (KYTY_SYSV_ABI *)(void *, uint64_t, uint64_t, uint32_t,
void *, uint64_t, uint64_t);
@@ -685,6 +824,8 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
const auto set_buffer = reinterpret_cast<SetBuffer>(find("N-FSPA4S3nI"));
const auto offset = reinterpret_cast<Offset>(find("GnxKOHEawhk"));
const auto wait_address = reinterpret_cast<WaitAddress>(find("DLfoNxTFNVk"));
const auto wait_counter = reinterpret_cast<WaitCounter>(find("cQb8Zr8Q0Y0"));
const auto write_counter = reinterpret_cast<WriteCounter>(find("jK+yuYCI7MA"));
const auto write_address = reinterpret_cast<WriteAddress>(find("sJXyWHjP-F8"));
const auto read_file = reinterpret_cast<ReadFile>(find("mQ16-QdKv7k"));
const auto write_event = reinterpret_cast<WriteKernelEvent>(find("H896Pt-yB4I"));
@@ -758,6 +899,59 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
"submission wait observes the completed result");
}
}
constexpr std::array counter_comparisons {
Comparison{1, 0, 0, 1}, Comparison{1, 0, 0, 1},
Comparison{4, 0x7ffffffe, 0x7fffffff, 0x80000000},
Comparison{5, 0xffffffff, 0, 1}};
for (const auto &comparison : counter_comparisons) {
reset(1);
auto *producer = buffers[0].header.data();
auto *consumer = buffers[1].header.data();
auto *lower = buffers[2].header.data();
constexpr uint8_t Counter = 127;
constexpr auto Invalid = Libs::LibKernel::KERNEL_ERROR_EINVAL;
Check(write_counter(producer, 128, 1, 1, 0, 0) == Invalid &&
write_counter(producer, Counter, 8, 1, 0, 0) == Invalid &&
write_counter(producer, Counter, 1, 1, 5, 0) == Invalid &&
write_counter(producer, Counter, 1, 1, 0, 2) == Invalid &&
wait_counter(consumer, Counter, 8, 0, 1, 0, 0, 0) == Invalid &&
wait_counter(consumer, Counter, 1, 0, 7, 0, 0, 0) == Invalid &&
wait_counter(consumer, Counter, 1, 0, 1, 2, 0, 0) == Invalid &&
wait_counter(consumer, Counter, 1, 0, 1, 0, 0, 2) == Invalid &&
offset(producer) == 0 && offset(consumer) == 0,
"invalid counter operations fail without appending a command");
uint64_t retired = 0, lower_done = 0;
std::array<char, 3> output {};
Result result {1234, 5678};
std::array<uint32_t, 4> ids {};
if (comparison.blocked != 0) {
auto *initializer = buffers[3].header.data();
Check(write_counter(initializer, Counter, 1, comparison.blocked, 0, 0) == OK &&
submit_amm(buffers[3].data.data(), static_cast<uint32_t>(offset(initializer)),
0, &ids[3]) == OK && wait_amm(ids[3]) == OK,
"initialize shared counter before dependent submissions");
}
Check(read_file(producer, reinterpret_cast<uint64_t>(&buffers[0].header[3]),
reinterpret_cast<uint64_t>(&buffers[0].header[4]), file_id,
output.data(), output.size(), 0) == OK &&
write_counter(producer, Counter, 1, comparison.released, 0, 0) == OK &&
wait_counter(consumer, Counter, 1, comparison.reference, comparison.compare,
0, 0, 0) == OK &&
write_counter(consumer, Counter, 1, 0, 0, 0) == OK &&
write_address(consumer, &retired, 1) == OK &&
write_address(lower, &lower_done, 1) == OK,
"build APR completion and dependent AMM counter reset");
Check(submit_amm(buffers[1].data.data(), static_cast<uint32_t>(offset(consumer)),
0, &ids[1]) == OK &&
submit_amm(buffers[2].data.data(), static_cast<uint32_t>(offset(lower)),
1, &ids[2]) == OK && wait_amm(ids[2]) == OK &&
lower_done == 1 && retired == 0,
"counter wait blocks AMM retirement while its lower priority progresses");
Check(submit_apr(producer, 3, &result, &ids[0]) == OK && wait_amm(ids[1]) == OK &&
wait_apr(ids[0]) == OK && result.result == OK && retired == 1 &&
std::memcmp(output.data(), "APR", 3) == 0,
"shared counter completion releases AMM only after APR read and resets for reuse");
}
reset(1);
namespace EventQueue = Libs::LibKernel::EventQueue;
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
@@ -1118,6 +1312,38 @@ void CheckSocketReceiveBuffer(int reader, int writer) {
}
#endif
void CheckEtherAddressFormatting() {
Loader::SymbolDatabase symbols;
Libs::LibNet::InitNet_1_Net(&symbols);
const auto *format_symbol = symbols.FindByNid("v6M4txecCuo", Loader::SymbolType::Func);
const auto *errno_symbol = symbols.FindByNid("HQOwnfMGipQ", Loader::SymbolType::Func);
Check(format_symbol && errno_symbol, "Ethernet formatting and errno exports resolve");
using Format = int (KYTY_SYSV_ABI *)(const Libs::Network::Net::NetEtherAddr *, char *, size_t);
using Errno = int *(KYTY_SYSV_ABI *)();
const auto format = reinterpret_cast<Format>(format_symbol->vaddr);
auto *net_errno = reinterpret_cast<Errno>(errno_symbol->vaddr)();
for (const auto address : {Libs::Network::Net::NetEtherAddr{},
Libs::Network::Net::NetEtherAddr{{0x01, 0x23, 0x45, 0xab, 0xcd, 0xef}}}) {
const auto *expected = address.data[0] == 0 ? "00:00:00:00:00:00" : "01:23:45:ab:cd:ef";
for (const size_t size : {18u, 127u}) {
std::array<char, 128> text;
text.fill('!');
Check(format(&address, text.data(), size) == OK &&
std::strcmp(text.data(), expected) == 0 && text[18] == '!',
"Ethernet formatting accepts exact and larger buffers");
}
}
const Libs::Network::Net::NetEtherAddr address{};
std::array<char, 18> text;
text.fill('!');
Check(format(&address, text.data(), 17) == Libs::Network::NET_ERROR_EINVAL &&
*net_errno == Libs::Posix::POSIX_EINVAL &&
std::all_of(text.begin(), text.end(), [](char c) { return c == '!'; }) &&
format(nullptr, text.data(), text.size()) == Libs::Network::NET_ERROR_EINVAL &&
format(&address, nullptr, text.size()) == Libs::Network::NET_ERROR_EINVAL,
"Ethernet formatting rejects invalid arguments without writing output");
}
void CheckSocketWakeup() {
namespace Net = Libs::Network::Net;
Loader::SymbolDatabase symbols;
@@ -1398,6 +1624,10 @@ int main(int, char**) {
TempDirectory temporary;
FileSystem::Initialize();
TestSysmoduleReferences();
TestSystemServiceEntitlementEvents();
TestRandomDevices();
TestFileDescriptorFlags();
CheckMountRoot(temporary.Path());
CheckUnmappedPaths(temporary.Path());
CheckArchiveMount(temporary.Path());
@@ -1412,6 +1642,7 @@ int main(int, char**) {
CheckSaveRename(temporary.Path(), "replacement-save");
FileSystem::Shutdown();
CheckSocketWakeup();
CheckEtherAddressFormatting();
TestNpWebApi2Memory();
graphics.reset();
subsystems.Destroy();
+299 -27
View File
@@ -50,6 +50,7 @@
#include "libs/agc.h"
#include "libs/dialog.h"
#include "libs/errno.h"
#include "loader/symbolDatabase.h"
#include "spirv-tools/libspirv.hpp"
#if __has_include("graphics/host_gpu/renderer/renderTargetBarriers.h")
@@ -104,6 +105,10 @@
#undef max
#endif
namespace Libs {
void InitAgcDriver_1(Loader::SymbolDatabase *symbols);
}
namespace Libs::Graphics {
template <typename Cache>
@@ -386,6 +391,11 @@ struct TextureCacheTestAccess {
};
struct RenderExecutorTestAccess {
static void DrawIndex(RenderExecutor &executor, CommandBuffer &command,
const DrawIndexArgs &args) {
executor.DrawIndex(0, command, args);
}
static void DrawAuto(RenderExecutor &executor, CommandBuffer &command,
const DrawAutoArgs &args) {
executor.DrawAuto(0, command, args);
@@ -15755,6 +15765,7 @@ public:
constexpr uintptr_t depth_address = 0x0000000204400000ull;
constexpr uint64_t allocation_size = 0x40000;
constexpr uint64_t rect_address = depth_address + 0x8000;
constexpr uint64_t index_address = rect_address + 0x1000;
EnsureRuntimeContext();
RenderContext context(m_runtime_context);
auto &scheduler = context.GetCommandScheduler();
@@ -15798,6 +15809,8 @@ public:
-0.75f, 0.75f, 0, 0, 0, 1, 0, 0};
std::memcpy(reinterpret_cast<void *>(rect_address), rect_vertices.data(),
sizeof(rect_vertices));
constexpr std::array<uint32_t, 3> indices{0, 1, 2};
std::memcpy(reinterpret_cast<void *>(index_address), indices.data(), sizeof(indices));
}
resources.MapMemory(depth_address, allocation_size);
if (depth_feedback) {
@@ -16652,6 +16665,56 @@ public:
}
registers.SetTargetOutputMode(0, 4);
// Indirect arguments also reach s8/s9. Native VS prologs must add them
// exactly once, for both indexed and automatically generated indices.
static const auto indirect_vertex = [&] {
auto code = native_vertex;
for (auto &word : code) {
if (word == EncodeVopc(0xc2, InlineU32(1), 5))
word = EncodeVopc(0xc2, InlineU32(8), 5);
else if (word == EncodeVopc(0xc2, InlineU32(2), 5))
word = EncodeVopc(0xc2, InlineU32(9), 5);
}
code.insert(code.begin(), {EncodeVop2(0x25, 5, 8, 5),
EncodeVop2(0x25, 8, 9, 8)});
const auto position = std::ranges::find(code, EncodeExp0(0x0c, 0xf));
code.insert(position, {EncodeVopc(0xc2, InlineU32(5), 8),
EncodeVop2(0x01, 3, Vgpr(1), 3),
EncodeVop2(0x01, 4, Vgpr(1), 4)});
return code;
}();
const auto indirect_vs = reinterpret_cast<uint64_t>(indirect_vertex.data());
ShaderMapUserData(indirect_vs,
{.type = Prospero::ShaderBinaryType::kGs, .user_data = &native_user_data,
.code_size_bytes = static_cast<uint32_t>(indirect_vertex.size() * sizeof(u32))});
shaders.SetEsShaderBase(indirect_vs);
shaders.SetPsShaderBase(pixel_address);
shaders.SetGsShaderResource2({.user_sgpr = 2});
shaders.SetGsUserSgpr(0, 7, HW::UserSgprType::Unknown);
shaders.SetGsUserSgpr(1, 5, HW::UserSgprType::Unknown);
user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriList);
for (const bool indexed : {false, true}) {
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), sparse_color.image_id,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, {});
if (indexed) {
RenderExecutorTestAccess::DrawIndex(executor, scheduler.Current(),
{.index_count = 3, .index_addr = reinterpret_cast<const void *>(index_address),
.instance_count = 1, .index_type_and_size = 1, .base_vertex = 7, .first_instance = 5,
.offset_source = DrawOffsetSource::IndirectArgs});
} else {
RenderExecutorTestAccess::DrawAuto(executor, scheduler.Current(),
{.vertex_count = 3, .instance_count = 1, .first_vertex = 7, .first_instance = 5,
.offset_source = DrawOffsetSource::IndirectArgs});
}
const auto pixels = ReadCachedTexel(name, context, sparse_color.image_id,
{}, {extent, extent, 1});
for (size_t component = 0; component < pixels.size(); component++) {
Require("NativeIndirectDrawOffsets", indexed ? "indexed" : "auto",
pixels[component] == (component % 4 == 3 ? 0x3f800000u : 0x3e800000u),
"native vertex or instance offset was applied twice");
}
}
// A fourth VS invocation reads beyond the descriptor instead of reconstructing the corner.
const ShaderBufferResource rect_buffer{{
static_cast<u32>(rect_address),
@@ -23545,6 +23608,48 @@ TestCase Vop3pIntegerNegationCapturedAndSelectedHalves() {
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
}
TestCase Vop3pInt16Ops() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovLiteral(&code, 1, 0x80007fffu);
AppendVMovLiteral(&code, 2, 0x00010001u);
AppendVMovLiteral(&code, 3, 0xfffe0003u);
AppendVMovLiteral(&code, 4, 0x00130004u);
AppendVMovLiteral(&code, 5, 0x8010f00fu);
AppendVMovLiteral(&code, 6, 0x00050007u);
AppendVop3p(&code, 0x00u, 10, Vgpr(1), Vgpr(3), Vgpr(6), 0x7u);
AppendVop3p(&code, 0x09u, 11, Vgpr(1), Vgpr(3), Vgpr(6), 0x7u);
AppendVop3p(&code, 0x01u, 12, Vgpr(1), Vgpr(3), 0, 0x3u);
AppendVop3p(&code, 0x02u, 13, Vgpr(1), Vgpr(2), 0, 0x3u);
AppendVop3p(&code, 0x0bu, 14, Vgpr(2), Vgpr(3), 0, 0x3u);
AppendVop3p(&code, 0x04u, 15, Vgpr(4), Vgpr(5), 0, 0x3u);
AppendVop3p(&code, 0x05u, 16, Vgpr(4), Vgpr(5), 0, 0x3u);
AppendVop3p(&code, 0x06u, 17, Vgpr(4), Vgpr(5), 0, 0x3u);
AppendVop3p(&code, 0x08u, 18, Vgpr(1), Vgpr(2), 0, 0x3u);
AppendVop3p(&code, 0x0cu, 19, Vgpr(1), Vgpr(2), 0, 0x3u);
AppendVop3p(&code, 0x0du, 20, Vgpr(1), Vgpr(2), 0, 0x3u);
for (u32 i = 0; i < 11; i++) {
AppendStoreVgpr(&code, 10 + i, i);
}
AppendEnd(&code);
TestCase test;
test.name = "Vop3pInt16Ops";
test.code = std::move(code);
test.expected = {0x00058004u, 0x00058004u, 0x00007ffdu, 0x80018000u,
0x0003fffeu, 0x008000f0u, 0x10020f00u, 0xf002ff00u,
0x80000001u, 0x80007fffu, 0x00010001u};
test.opcodes = {O::V_MOV_B32, O::V_PK_MAD_I16, O::V_PK_MAD_U16,
O::V_PK_MUL_LO_U16, O::V_PK_ADD_I16, O::V_PK_SUB_U16,
O::V_PK_LSHLREV_B16, O::V_PK_LSHRREV_B16, O::V_PK_ASHRREV_I16,
O::V_PK_MIN_I16, O::V_PK_MAX_U16, O::V_PK_MIN_U16,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
return test;
}
TestCase CvtPkU8F32PacksSelectedByte() {
using O = ShaderOpcode;
@@ -24842,6 +24947,46 @@ TestCase FloatInlineConstantF16UsesNumericValue() {
return test;
}
TestCase Vop3Int16Ops() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovLiteral(&code, 1, 0xabcd0001u);
AppendVMovLiteral(&code, 2, 0x12340002u);
AppendVMovLiteral(&code, 3, 0x55557fffu);
AppendVMovLiteral(&code, 4, 0x66668000u);
AppendVMovLiteral(&code, 5, 0x77770013u);
AppendVMovLiteral(&code, 6, 0x88888010u);
AppendVMovLiteral(&code, 10, 0xaaaa0000u);
AppendVMovLiteral(&code, 11, 0xbbbb0000u);
AppendVMovLiteral(&code, 12, 0xcccc0000u);
AppendVMovLiteral(&code, 13, 0xdddd0000u);
AppendVMovLiteral(&code, 14, 0xeeee0000u);
AppendVMovLiteral(&code, 15, 0x11110000u);
AppendVop3(&code, 0x304u, 10, Vgpr(1), Vgpr(2));
AppendVop3(&code, 0x30du, 11, Vgpr(3), Vgpr(1));
AppendVop3(&code, 0x308u, 12, Vgpr(5), Vgpr(6));
AppendVop3(&code, 0x309u, 13, Vgpr(3), Vgpr(4));
AppendVop3(&code, 0x30bu, 14, Vgpr(3), Vgpr(4));
AppendVop3(&code, 0x308u, 15, Vgpr(5), Vgpr(3));
for (u32 i = 0; i < 6; i++) {
AppendStoreVgpr(&code, 10 + i, i);
}
AppendEnd(&code);
TestCase test;
test.name = "Vop3Int16Ops";
test.code = std::move(code);
test.expected = {0xaaaaffffu, 0xbbbb8000u, 0xccccf002u,
0xdddd8000u, 0xeeee7fffu, 0x11110fffu};
test.opcodes = {O::V_MOV_B32, O::V_SUB_NC_U16, O::V_ADD_NC_I16,
O::V_ASHRREV_I16, O::V_MAX_U16, O::V_MIN_U16,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
return test;
}
TestCase VectorFloatControlContractPreservesInfNan() {
using O = ShaderOpcode;
@@ -26862,6 +27007,61 @@ TestCase VectorCompareF16Ops() {
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
}
TestCase VectorCompareI16U16Ops() {
using O = ShaderOpcode;
std::vector<u32> code;
std::vector<u32> expected;
AppendVMovU32(&code, 1, 1);
AppendVMovLiteral(&code, 2, 0xabcd8000u);
AppendVMovLiteral(&code, 3, 0x00007fffu);
AppendVMovLiteral(&code, 4, 0x12348000u);
AppendVMovLiteral(&code, 5, 0xffff0001u);
AppendVMovLiteral(&code, 6, 0x00000001u);
u32 dst = 10;
u32 out = 0;
auto append_compare = [&](u32 opcode, u32 src0, u32 src1, bool value) {
code.push_back(EncodeVopc(opcode, src0, src1));
code.push_back(EncodeVop2(0x01, dst, InlineU32(0), 1));
AppendStoreVgpr(&code, dst, out++);
expected.push_back(value ? 1u : 0u);
dst++;
};
append_compare(0x8a, Vgpr(2), 4, true);
append_compare(0x8a, Vgpr(2), 3, false);
append_compare(0x8b, Vgpr(2), 3, true);
append_compare(0x8b, Vgpr(3), 2, false);
append_compare(0x8b, Vgpr(5), 6, true);
append_compare(0x8c, Vgpr(3), 2, true);
append_compare(0x8c, Vgpr(2), 3, false);
append_compare(0x8d, Vgpr(2), 3, true);
append_compare(0x8d, Vgpr(5), 6, false);
append_compare(0xaa, Vgpr(2), 4, true);
append_compare(0xaa, Vgpr(2), 3, false);
append_compare(0xab, Vgpr(3), 2, true);
append_compare(0xab, Vgpr(2), 3, false);
append_compare(0xac, Vgpr(2), 3, true);
append_compare(0xac, Vgpr(3), 2, false);
append_compare(0xad, Vgpr(2), 3, true);
append_compare(0xad, Vgpr(5), 6, false);
append_compare(0xae, Vgpr(2), 3, true);
append_compare(0xae, Vgpr(3), 2, false);
append_compare(0xae, Vgpr(5), 6, true);
AppendEnd(&code);
return {"VectorCompareI16U16Ops",
code,
{},
expected,
{O::V_MOV_B32, O::V_CMP_EQ_I16, O::V_CMP_LE_I16, O::V_CMP_GT_I16,
O::V_CMP_NE_I16, O::V_CMP_EQ_U16, O::V_CMP_LE_U16, O::V_CMP_GT_U16,
O::V_CMP_NE_U16, O::V_CMP_GE_U16, O::V_CNDMASK_B32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
}
TestCase Vop2SdwaCndmaskSourceModifier() {
using O = ShaderOpcode;
@@ -36120,6 +36320,7 @@ std::vector<TestCase> MakeCases() {
AddCase(Vop2PkFmacF16DppNegatesBothPackedHalves);
AddCase(Vop3pOpselHiUsesArchitecturalSourceBits);
AddCase(Vop3pIntegerNegationCapturedAndSelectedHalves);
AddCase(Vop3pInt16Ops);
AddCase(CvtPkU8F32PacksSelectedByte);
AddCase(CvtPkrtzF16F32SubnormalRoundsTowardZero);
AddCase(CvtPkrtzF16F32SdwaAndOutputModifiers);
@@ -36159,6 +36360,7 @@ std::vector<TestCase> MakeCases() {
AddCase(MadMixF16NegHiIsAbsAndNegIsIndependent);
AddCase(VectorVop3FmaF16UsesRdna2Opcode34b);
AddCase(FloatInlineConstantF16UsesNumericValue);
AddCase(Vop3Int16Ops);
AddCase(VectorFloatControlContractPreservesInfNan);
AddCase(VectorFloatArithmeticOps);
AddCase(VectorMinMaxF32NanAndSignedZeroEdges);
@@ -36203,6 +36405,7 @@ std::vector<TestCase> MakeCases() {
cases.push_back(VectorCmpxClassF16(32));
cases.push_back(VectorCmpxClassF16(64));
AddCase(VectorCompareF16Ops);
AddCase(VectorCompareI16U16Ops);
AddCase(Wave32VccMasksPreserveOtherHalf);
AddCase(Vop2SdwaCndmaskSourceModifier);
AddCase(Vop2SdwaCndmaskCapturedByte3Source);
@@ -41447,35 +41650,97 @@ void CheckPm4WaitResume(RenderContext &renderer) {
}
void CheckPm4RewindResume(RenderContext &renderer) {
constexpr const char *name = "Pm4RewindResume";
Loader::SymbolDatabase symbols;
Libs::InitAgcDriver_1(&symbols);
const auto *emit_symbol = symbols.FindByNid("DwICrVxerkY", Loader::SymbolType::Func);
const auto *patch_symbol = symbols.FindByNid("eWaWyFegzgQ", Loader::SymbolType::Func);
Require(name, "async exports",
emit_symbol != nullptr && patch_symbol != nullptr &&
emit_symbol->vaddr == reinterpret_cast<uint64_t>(&Gen5::AgcAcbRewind) &&
patch_symbol->vaddr ==
reinterpret_cast<uint64_t>(&Gen5::AgcRewindPatchSetRewindState),
"async rewind imports did not resolve to the emitter and state patch");
const auto async_patch = reinterpret_cast<decltype(&Gen5::AgcRewindPatchSetRewindState)>(
patch_symbol->vaddr);
constexpr uint32_t sentinel = 0x12345678u;
std::array<uint32_t, 4> packet{sentinel, sentinel, sentinel, sentinel};
CommandBufferLayout acb{packet.data(), packet.data() + packet.size(),
packet.data() + 1, packet.data() + 3, nullptr, nullptr, 0};
auto *buffer = reinterpret_cast<Gen5::CommandBuffer *>(&acb);
for (const uint8_t state : {0, 1}) {
for (const uint8_t offload : {0, 1}) {
acb.cursor_up = packet.data() + 1;
Require(name, "async packet",
Gen5::AgcAcbRewind(buffer, state, offload) == packet.data() + 1 &&
acb.cursor_up == packet.data() + 3 &&
packet[0] == sentinel && packet[3] == sentinel &&
packet[1] == 0xc0005900u &&
packet[2] == ((uint32_t{state} << 31u) | (uint32_t{offload} << 24u)),
"async rewind packet or two-DWORD allocation differs from the native command");
}
}
const auto before = packet;
acb.cursor_up = packet.data() + 1;
acb.reserved_dw = 1;
Require(name, "bounded allocation",
Gen5::AgcAcbRewind(buffer, 1, 1) == nullptr &&
Gen5::AgcAcbRewind(nullptr, 1, 1) == nullptr &&
acb.cursor_up == packet.data() + 1 && packet == before,
"failed rewind allocation changed the cursor or command storage");
// Native patchers inspect only the opcode, preserving the header and low 31 bits.
std::array<uint32_t, 2> command{0xa5ff591fu, 0x0155aa55u};
Require(name, "patch preserves fields",
async_patch(command.data(), 1) == 0 && command[0] == 0xa5ff591fu &&
command[1] == 0x8155aa55u && async_patch(command.data(), 0) == 0 &&
command[0] == 0xa5ff591fu && command[1] == 0x0155aa55u,
"rewind patch changed the offload bit, header, or unrelated payload bits");
command[0] = 0xc0001000u;
const auto invalid = command;
Require(name, "wrong opcode",
async_patch(command.data(), 1) == static_cast<int>(0x8a6c000cu) &&
command == invalid,
"rewind patch accepted or modified a packet with the wrong opcode");
GraphicsInitJmpTables();
CommandProcessor processor(renderer, 0);
uint32_t suffix = 0;
const auto address = reinterpret_cast<uint64_t>(&suffix);
std::array<uint32_t, 7> commands{};
commands[0] = KYTY_PM4(2, Pm4::IT_REWIND, 0);
commands[1] = 0;
commands[2] = KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0);
commands[3] = 0;
commands[4] = static_cast<uint32_t>(address);
commands[5] = static_cast<uint32_t>(address >> 32u);
commands[6] = 33;
Pm4Execution execution;
Require("Pm4RewindResume", "pending",
processor.Process(execution, commands) == Pm4ProcessResult::Blocked &&
suffix == 0,
"pending rewind did not preserve its command position");
Require("Pm4RewindResume", "patch valid",
Gen5::AgcRewindPatchSetRewindState(commands.data(), 1) == 0 &&
commands[1] == 0x80000000u,
"rewind state patch did not set the valid bit");
Require("Pm4RewindResume", "resume",
processor.Process(execution, commands) ==
Pm4ProcessResult::Complete &&
suffix == 33,
"valid rewind did not resume at the following packet");
std::printf("[host] %-32s ok\n", "Pm4RewindResume");
for (const bool async : {false, true}) {
for (const uint8_t offload : {0, 1}) {
if (!async && offload != 0) {
continue;
}
uint32_t suffix = 0;
const auto address = reinterpret_cast<uint64_t>(&suffix);
std::array<uint32_t, 7> commands{
0, 0, KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0), 0,
static_cast<uint32_t>(address), static_cast<uint32_t>(address >> 32u), 33};
CommandBufferLayout cb{commands.data(), commands.data() + 2,
commands.data(), commands.data() + 2, nullptr, nullptr, 0};
auto *command_buffer = reinterpret_cast<Gen5::CommandBuffer *>(&cb);
const auto *emitted = async ? Gen5::AgcAcbRewind(command_buffer, 0, offload)
: Gen5::AgcDcbRewind(command_buffer, 0);
const auto patch = async ? async_patch : &Gen5::AgcRewindPatchSetRewindState;
Pm4Execution execution;
Require(name, "pending",
emitted == commands.data() &&
processor.Process(execution, commands) == Pm4ProcessResult::Blocked &&
suffix == 0,
"pending rewind did not preserve its command position");
Require(name, "still pending",
processor.Process(execution, commands) == Pm4ProcessResult::Blocked &&
!execution.MadeProgress() && suffix == 0,
"pending rewind advanced before its state was patched");
Require(name, "patch and resume",
patch(commands.data(), 1) == 0 &&
commands[1] == (0x80000000u | (uint32_t{offload} << 24u)) &&
processor.Process(execution, commands) == Pm4ProcessResult::Complete &&
suffix == 33,
"patched rewind lost the offload bit or failed to resume");
}
}
std::printf("[host] %-32s ok\n", name);
}
void CheckPm4CeCompletion(RenderContext &renderer) {
@@ -41626,6 +41891,13 @@ int main(int argc, char **argv) {
RunCase(&vulkan, VectorCompareF16Ops());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--int16-alu-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, VectorCompareI16U16Ops());
RunCase(&vulkan, Vop3Int16Ops());
RunCase(&vulkan, Vop3pInt16Ops());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--buffer-format-store-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, BufferStoreFormatXyzwPackedUnormSkinningVectors());