Compare commits

...

26 Commits

Author SHA1 Message Date
Mac 2f4b8a335d shader: allow SDWA byte destinations for bitwise VOP2 ops 2026-10-08 22:12:05 +04:00
nmzik b38b7454a6 Preserve vertex export locations for mixed pixel interpolation 2026-10-08 22:05:53 +04:00
nmzik b65dc12208 Track pad port ownership and reject duplicate opens 2026-10-08 21:58:53 +04:00
Devran Cosmo Uenal 58978d154a font: rasterize glyphs larger than 128 pixels (#1214)
Glyph images were rendered into a fixed 128x128 buffer and their width and
height were clamped to 128. Since glyphs are rasterized at the requested
scale (f96a14a0), large text such as title cards lost every row and column
past 128 pixels, so letters were drawn cut into bands.

The glyph buffers now grow to the glyph's bitmap box and width_byte is the
glyph's width. The VGA fallback font keeps its 128-pixel limit; scalable
glyphs are limited to 4096 pixels only to reject absurd scales.
2026-10-08 20:03:13 +04:00
popcoder565 1f3adf5109 cmake: accept glslang's current tool name and document Python 3 (#1161)
* cmake: accept glslang's current tool name and document Python 3

Official glslang release archives for Windows ship the compiler only as
glslang.exe, so configuring with one of them on PATH could not find
glslangValidator. Fall back to glslang; glslangValidator is still
preferred where both exist.

The bundled SPIRV-Tools needs Python 3 at configure time, which the
Windows build requirements did not mention.

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

* docs: list Python 3 in the Linux and macOS prerequisites

The bundled SPIRV-Tools calls find_host_package(Python3 REQUIRED) on every platform, so the apt and Homebrew package lists need it too, not only the Windows requirements.

* cmake: remove glslang naming comment

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-08 19:46:09 +04:00
NeySlim 107d5f750a libNet: finish NpCommerce dialogs at open (#1166)
* libNet: finish NpCommerce dialogs at open

* libNet: complete NpCommerce dialog cancellation

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-08 19:31:47 +04:00
Dimitar Vagalinski f40755173f ampr: keep resolved host paths in native form (#1175)
* ampr: keep resolved host paths in native form

ResolveOnePath stored each resolved file as a UTF-8 std::string
(Common::PathToString) and the read, stat and size paths turned that
string back into a std::filesystem::path. On POSIX this round trip is a
no-op, but on Windows path(std::string) decodes the bytes with the ANSI
code page, not UTF-8 (/utf-8 only sets the source and execution
charsets). Any non-ASCII character in the game folder therefore produced
a different path: the resolve step found the file through the native
path, then every APR read of it failed with ENOENT.

Tokyo Xtreme Racer (#1156) installed under "D:/Инсталляторы/[ps5]/..."
hits this on its first pak read:

  APR read missing host file: .../tokyoxtremeracer/content/paks/global.utoc
  Apr read failure 80020002 at CB offset 0   (LowLevelFatalError, abort)

even though KernelStat and the directory listing see the same file.

Store std::filesystem::path in ResolvedPathInfo and the file-id table
and pass it straight to Common::File. Convert to UTF-8 only for the log
lines.

* tests: cover APR stat and size under a non-ASCII host folder

Resolve a file inside a Cyrillic directory through APR and check that
the stat and size exports find it by ID, matching the Tokyo Xtreme
Racer install layout from #1156. On POSIX the old string round trip
also passed, so this guards the Windows path.

* tests: read the non-ASCII APR fixture through an APR command

Review on #1175 noted that the Unicode fixture only checked the stat
and size exports. A submitted APR read opens the stored host path in
ExecuteCommand/ReadHostFileToGuest, which the fixture did not reach.

Add AprReadMatches, which submits one APR read command for a file ID
and compares the bytes, and use it on the file under the Cyrillic
folder. As with the stat and size checks, POSIX hosts run this path but
only Windows would show an ANSI code page round trip.

* Remove APR path tests from this change

* Remove unrelated formatting changes

* Remove native path explanatory comments

---------

Co-authored-by: overlordxrz-source <overlordxrz-source@users.noreply.github.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-08 19:27:10 +04:00
Romain Hedouin 4e14eed770 kernel: start the semaphore wait timer after the early returns (#1173)
Wait() read the clock and looked up the thread id before checking for an available token. Both are only needed once the thread has to block.
2026-10-08 19:21:36 +04:00
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
nmzik c86c8b4d93 audio: mix AudioOut2 objects and apply port gains
Co-authored-by: chenxiao07 <chenxiao07@gmail.com>
2026-10-08 01:47:34 +04:00
chenxiao07 e43aadf75f ampr: assign unique IDs to resolved file paths 2026-10-08 01:18:40 +04:00
shadowbeat070 9f25e3c23e shader: support partial SDWA multiply and byte-source moves 2026-10-08 01:10:49 +04:00
shadowbeat070 7881c69a54 shader: decode v_cmp_class_f16 2026-10-08 01:10:49 +04:00
shadowbeat070 9e666e4dd2 shader: support scalar byte sign extension 2026-10-08 01:10:49 +04:00
Nawid3333 c3a12397b0 tests: use function pointers in the concurrent backing read pairs (#1163)
TestConcurrentBackingReads (7e011fa5) builds its reader pairs with
std::pair {TryReadBacking, TryReadSparseBacking}. Class template argument
deduction then also considers pair's const T1& / const T2& constructors, which
deduce function types, and libstdc++ and libc++ reject a pair member of
function type ("data member instantiated with function type"), so
virtual_memory_allocation_tests no longer compiles on Linux and macOS. MSVC's
library accepts it, which is why Windows still builds.

Take the functions' addresses so the pairs hold function pointers.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 00:56:44 +04:00
47 changed files with 2415 additions and 414 deletions
+1 -1
View File
@@ -244,7 +244,7 @@ file(GLOB kyty_emulator_src CONFIGURE_DEPENDS
"${KYTY_SOURCE_DIR}/loader/*.h"
)
find_program(KYTY_GLSLANG_VALIDATOR glslangValidator REQUIRED)
find_program(KYTY_GLSLANG_VALIDATOR NAMES glslangValidator glslang REQUIRED)
set(gpu_tiler_shader_dir "${KYTY_SOURCE_DIR}/graphics/host_gpu/shaders")
set(gpu_tiler_generated_dir "${PROJECT_BINARY_DIR}/gpu_tiler_shaders")
set(gpu_tiler_shader_names
+4 -3
View File
@@ -141,7 +141,8 @@ the Vulkan/SPIR-V validation rules.
- Visual Studio 2022 or Build Tools 2022 with the **Desktop development with C++** workload and
**C++ Clang tools for Windows** component
- Qt 6 for MSVC 2022 64-bit, including Concurrent, Network, and Widgets
- [glslang](https://github.com/KhronosGroup/glslang/releases) (`glslangValidator`) on `PATH`
- [glslang](https://github.com/KhronosGroup/glslang/releases) (`glslang` or `glslangValidator`) on `PATH`
- Python 3 on `PATH`; the bundled SPIRV-Tools runs it at configure time
The Microsoft C++ compiler (`cl.exe`) is not supported; use `clang-cl`.
@@ -176,7 +177,7 @@ build has no working sound and no gamepad hotplug:
```bash
sudo apt-get install --no-install-recommends \
clang lld ninja-build cmake git glslang-tools pkg-config \
clang lld ninja-build cmake git glslang-tools python3 pkg-config \
libgl1-mesa-dev libx11-dev libxcursor-dev libxext-dev libxfixes-dev \
libxi-dev libxrandr-dev libxss-dev libxtst-dev libxkbcommon-dev \
libasound2-dev libpulse-dev libudev-dev libdbus-1-dev libwayland-dev wayland-protocols
@@ -251,7 +252,7 @@ Requirements:
- An Apple Silicon Mac with Rosetta 2 installed (`softwareupdate --install-rosetta`)
- Xcode (or the Command Line Tools)
- Homebrew packages: `brew install cmake ninja glslang`
- Homebrew packages: `brew install cmake ninja glslang python`
- Qt 6 (Concurrent, Network, Widgets) with x86-64 support. The official Qt installation is
universal and works; Homebrew's Qt is arm64-only and will not link
+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;
@@ -617,6 +617,13 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms(
ShaderParams pixel_params;
if (pixel_active) {
pixel_params = PrepareProgram(pixel_regs, sh, target_export_mapping, pixel_info);
if (!mesh_active && !tess_active && Prospero::IsRectList(user_config.GetPrimType())) {
pixel_info.parameter_mode = ShaderPixelParameterMode::Rectangle;
} else if (context.GetModeControl().provoking_vtx_last) {
pixel_info.parameter_mode = ShaderPixelParameterMode::LastVertex;
} else {
pixel_info.parameter_mode = ShaderPixelParameterMode::FirstVertex;
}
const auto& blend = context.GetBlendControl(0);
pixel_info.dual_source_blending =
blend.enable && !context.GetRenderTarget(0).info.blend_bypass &&
@@ -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);
@@ -177,6 +177,13 @@ uint32_t EmitAttribute(EmitterState& state, uint32_t attr, uint32_t chan) {
return value;
};
if (input->per_vertex) {
if (PixelParameterIsFlat(state, attr)) {
const auto value = load_per_vertex(
state.input_info.pixel->parameter_mode == ShaderPixelParameterMode::LastVertex ? 2u : 0u);
const auto bits = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, value);
return bits;
}
const auto barycentric_kind = state.input_info.pixel->ps_no_perspective
? IR::StageInputKind::BaryCoordNoPerspective
: IR::StageInputKind::BaryCoordSmooth;
@@ -231,8 +238,12 @@ uint32_t EmitInterpolationParameter(ValueEmitContext& ctx, uint32_t attr, uint32
return value;
};
const auto selected_vertex = (mode + 1u) % 3u;
uint32_t value = load_vertex(selected_vertex);
uint32_t selected_vertex = (mode + 1u) % 3u;
if (mode == 2u && PixelParameterIsFlat(state, attr)) {
selected_vertex =
state.input_info.pixel->parameter_mode == ShaderPixelParameterMode::LastVertex ? 2u : 0u;
}
uint32_t value = load_vertex(selected_vertex);
if (!PixelParameterIsCustom(state, attr) && mode < 2u) {
const auto delta = state.builder.AllocateId();
state.builder.AddFunction(spv::OpFSub, TypeF32(state), delta, value, load_vertex(0));
@@ -53,6 +53,7 @@ constexpr OpcodeMap SOP1_OPCODE_LIST[] = {
{0x14u, Opcode::S_FF1_I32_B64},
{0x15u, Opcode::S_FLBIT_I32_B32},
{0x16u, Opcode::S_FLBIT_I32_B64},
{0x19u, Opcode::S_SEXT_I32_I8},
{0x1au, Opcode::S_SEXT_I32_I16},
{0x1bu, Opcode::S_BITSET0_B32},
{0x1cu, Opcode::S_BITSET0_B64},
@@ -503,6 +503,7 @@ std::string InstructionToString(const Instruction& inst) {
OperandToString(inst.dst).c_str(),
OperandToString(inst.src0).c_str()));
case Opcode::S_ABS_I32:
case Opcode::S_SEXT_I32_I8:
case Opcode::S_SEXT_I32_I16:
case Opcode::S_BREV_B32:
case Opcode::S_BREV_B64:
@@ -45,6 +45,7 @@ enum class Opcode {
S_CMOV_B64,
S_MOVK_I32,
S_ABS_I32,
S_SEXT_I32_I8,
S_SEXT_I32_I16,
S_ABSDIFF_I32,
S_BREV_B32,
@@ -400,6 +401,7 @@ enum class Opcode {
V_CMP_T_I32,
V_CMP_CLASS_F32,
V_CMPX_CLASS_F32,
V_CMP_CLASS_F16,
V_CMPX_CLASS_F16,
V_CMP_LT_I16,
V_CMP_EQ_I16,
@@ -36,7 +36,7 @@ constexpr Vop2OpcodeInfo VOP2_OPCODE_LIST[] = {
{0x05u, Opcode::V_SUBREV_F32},
{0x08u, Opcode::V_MUL_F32, Vop2SdwaProfile::Float32},
{0x09u, Opcode::V_MUL_I32_I24, Vop2SdwaProfile::IntegerPartialDestination},
{0x0bu, Opcode::V_MUL_U32_U24, Vop2SdwaProfile::IntegerFullDestination},
{0x0bu, Opcode::V_MUL_U32_U24, Vop2SdwaProfile::IntegerPartialDestination},
{0x0fu, Opcode::V_MIN_F32},
{0x10u, Opcode::V_MAX_F32},
{0x11u, Opcode::V_MIN_I32},
@@ -245,7 +245,8 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0x88u, Opcode::V_CMP_CLASS_F32}, {0x89u, Opcode::V_CMP_LT_I16},
{0x8au, Opcode::V_CMP_EQ_I16}, {0x8bu, Opcode::V_CMP_LE_I16},
{0x8cu, Opcode::V_CMP_GT_I16}, {0x8du, Opcode::V_CMP_NE_I16},
{0x8eu, Opcode::V_CMP_GE_I16}, {0x91u, Opcode::V_CMPX_LT_I32},
{0x8eu, Opcode::V_CMP_GE_I16}, {0x8fu, Opcode::V_CMP_CLASS_F16, false},
{0x91u, Opcode::V_CMPX_LT_I32},
{0x92u, Opcode::V_CMPX_EQ_I32}, {0x93u, Opcode::V_CMPX_LE_I32},
{0x94u, Opcode::V_CMPX_GT_I32}, {0x95u, Opcode::V_CMPX_NE_I32},
{0x96u, Opcode::V_CMPX_GE_I32}, {0x98u, Opcode::V_CMPX_CLASS_F32},
@@ -578,7 +579,7 @@ struct Vop1SdwaRule {
constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = {
{Opcode::V_MOV_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(),
SdwaSelWords() | SdwaSelFull(), false},
SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), false},
{Opcode::V_CVT_F32_U32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_I32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_UBYTE0, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
@@ -929,6 +930,7 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
case Opcode::V_CMPX_NEQ_F16:
case Opcode::V_CMPX_NLT_F16:
case Opcode::V_CMP_CLASS_F32:
case Opcode::V_CMP_CLASS_F16:
case Opcode::V_CMPX_CLASS_F16:
case Opcode::V_CMPX_CLASS_F32: return true;
default: return false;
@@ -995,7 +997,8 @@ constexpr Vop2SdwaRule VOP2_SDWA_RULES[] = {
{SdwaSelAll(), SdwaSelAll(), SdwaSelAll(), true, false},
{SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), false, false},
{SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), false, false},
{SdwaSelWords() | SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), true, false},
// Bitwise: WriteRawU32 inserts a byte or word destination like any other partial write.
{SdwaSelAll(), SdwaSelAll(), SdwaSelAll(), true, false},
};
static_assert(sizeof(VOP2_SDWA_RULES) / sizeof(VOP2_SDWA_RULES[0]) ==
static_cast<size_t>(Vop2SdwaProfile::Count));
@@ -134,10 +134,14 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_ABS_I32:
return SimpleInteger(inst, IR::ValueOpcode::IAbs32, IR::Type::U32, false, false, true);
case O::S_SEXT_I32_I16:
case O::S_SEXT_I32_I8:
case O::S_SEXT_I32_I16: {
// Sign extension leaves SCC unchanged.
const auto bits = inst.opcode == O::S_SEXT_I32_I8 ? 8u : 16u;
WriteOperand(inst.dst, IR::U32(ir.Emit(IR::ValueOpcode::BitFieldSExtract,
{ReadU32(inst.src0), IR::Value(0u), IR::Value(16u)})));
{ReadU32(inst.src0), IR::Value(0u), IR::Value(bits)})));
return;
}
case O::S_MUL_I32:
case O::S_MULK_I32:
return SimpleInteger(inst, IR::ValueOpcode::IMul32, IR::Type::U32, false, false, false);
@@ -374,6 +374,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false, true); return;
case O::V_CMP_CLASS_F32: EmitFloatClassCompare(inst, false); return;
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst, true); return;
case O::V_CMP_CLASS_F16: EmitFloatClassCompare(inst, false, true); return;
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true, true); return;
case O::V_CVT_F32_UBYTE0: V_CVT_F32_UBYTE(inst, 0); return;
@@ -227,15 +227,16 @@ void CollectPixelInputs(const Program& program, const ShaderPixelInputInfo* pixe
}
}
}
// Aliases of a vertex output share one SPIR-V interface variable. If any
// alias reads raw vertices, interpolate the other aliases from those too.
// Mixed interpolation modes share raw vertices at their guest export slot.
// Rectangle expansion alone supplies separate flat and smooth outputs.
for (uint32_t input = 0; input < pixel->input_num; input++) {
for (uint32_t alias = 0; alias < pixel->input_num; alias++) {
const bool same_mode = ShaderPixelParameterIsFlat(*pixel, input) ==
ShaderPixelParameterIsFlat(*pixel, alias);
if (ShaderPixelParameterMappedLocation(*pixel, input) ==
ShaderPixelParameterMappedLocation(*pixel, alias) &&
ShaderPixelParameterIsFlat(*pixel, input) ==
ShaderPixelParameterIsFlat(*pixel, alias)) {
per_vertex[input] = per_vertex[input] || per_vertex[alias];
(pixel->parameter_mode != ShaderPixelParameterMode::Rectangle || same_mode)) {
per_vertex[input] = per_vertex[input] || per_vertex[alias] || !same_mode;
}
}
}
@@ -244,7 +245,8 @@ void CollectPixelInputs(const Program& program, const ShaderPixelInputInfo* pixe
per_vertex[input]);
}
for (uint32_t input = 0; input < pixel->input_num; input++) {
if (interpolated[input] && per_vertex[input]) {
if (interpolated[input] && per_vertex[input] &&
!ShaderPixelParameterIsFlat(*pixel, input)) {
const auto kind = pixel->ps_no_perspective ? StageInputKind::BaryCoordNoPerspective
: StageInputKind::BaryCoordSmooth;
AddInput(info, kind, 0, 3,
+1
View File
@@ -663,6 +663,7 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
key.push_back(static_cast<uint32_t>(info.ps_front_face));
key.push_back(static_cast<uint32_t>(info.ps_ancillary));
key.push_back(static_cast<uint32_t>(info.ps_no_perspective));
key.push_back(static_cast<uint32_t>(info.parameter_mode));
key.push_back(static_cast<uint32_t>(info.ps_pixel_kill_enable));
key.push_back(static_cast<uint32_t>(info.ps_depth_export_enable));
key.push_back(static_cast<uint32_t>(info.ps_sample_mask_export_enable));
+2
View File
@@ -159,6 +159,7 @@ struct ShaderComputeInputInfo: ShaderWorkgroupInputInfo {
};
enum class ShaderAlphaBlendSource : uint8_t { None, SourceAlpha, SourceAlphaOne, SourceAlphaZero };
enum class ShaderPixelParameterMode : uint8_t { FirstVertex, LastVertex, Rectangle };
struct ShaderPixelInputInfo {
uint32_t interpolator_settings[32] = {0};
@@ -180,6 +181,7 @@ struct ShaderPixelInputInfo {
bool ps_front_face = false;
bool ps_ancillary = false;
bool ps_no_perspective = false;
ShaderPixelParameterMode parameter_mode = ShaderPixelParameterMode::FirstVertex;
bool ps_pixel_kill_enable = false;
bool ps_depth_export_enable = false;
bool ps_sample_mask_export_enable = false;
+4 -2
View File
@@ -30,6 +30,9 @@ uint32_t ShaderPixelParameterMappedLocation(const ShaderPixelInputInfo& info, ui
uint32_t ShaderPixelParameterLocation(const ShaderPixelInputInfo& info,
std::span<const uint32_t> active_inputs, uint32_t input) {
if (info.parameter_mode != ShaderPixelParameterMode::Rectangle) {
return ShaderPixelParameterMappedLocation(info, input);
}
std::array<bool, 32> used_locations {};
for (const auto active_input: active_inputs) {
used_locations[ShaderPixelParameterMappedLocation(info, active_input)] = true;
@@ -42,8 +45,7 @@ uint32_t ShaderPixelParameterLocation(const ShaderPixelInputInfo& info,
auto& location = group_locations[group];
if (location == UINT32_MAX) {
location = mapped;
// Smooth and custom interpolation read the same vertex output. Only
// differing flat/smooth rectangle outputs need separate locations.
// Rectangle expansion broadcasts flat aliases through separate outputs.
if (group_locations[group ^ 1u] != UINT32_MAX) {
location = 0;
while (location < used_locations.size() && used_locations[location]) {
+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);
+6 -6
View File
@@ -198,12 +198,6 @@ KernelSemaPrivate::Result KernelSemaPrivate::Wait(int need_count, uint32_t* ptr_
infinitely = false;
}
uint32_t elapsed = 0;
Common::Timer t;
t.Start();
int id = Common::Thread::GetThreadIdUnique();
if (m_count >= need_count) {
m_count -= need_count;
if (ptr_micros != nullptr) {
@@ -216,6 +210,12 @@ KernelSemaPrivate::Result KernelSemaPrivate::Wait(int need_count, uint32_t* ptr_
return Result::TimedOut;
}
uint32_t elapsed = 0;
Common::Timer t;
t.Start();
int id = Common::Thread::GetThreadIdUnique();
WaitingThread waiter {};
waiter.id = id;
waiter.priority = (m_fifo_order ? 0 : Libs::LibKernel::PthreadGetCurrentPriorityForKernel());
+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);
+25 -29
View File
@@ -15,7 +15,6 @@
#include <algorithm>
#include <array>
#include <cstring>
#include <limits>
#include <magic_enum.hpp>
#include <vector>
@@ -90,8 +89,9 @@ public:
};
struct OutputParam {
Id handle;
const void* data = nullptr;
Id handle;
const void* data = nullptr;
const float* gains = nullptr;
};
Audio() = default;
@@ -159,8 +159,10 @@ private:
static void OpenSdlDevice(PortIn* port, Format format);
static void CloseSdlDevice(PortIn* port);
static const void* PrepareOutputBuffer(const PortOut& port, const void* data,
std::vector<uint8_t>* buffer, float gain);
static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain);
std::vector<uint8_t>* buffer, float gain,
const float* gains);
static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain,
const float* gains);
};
static Audio* g_audio = nullptr;
@@ -196,7 +198,7 @@ uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking)
for (uint32_t i = 0; i < num; i++) {
if (params[i].handle > 0 && params[i].data != nullptr) {
output_params.push_back(
Audio::OutputParam {Audio::Id(params[i].handle), params[i].data});
Audio::OutputParam {Audio::Id(params[i].handle), params[i].data, params[i].gains});
}
}
@@ -302,7 +304,8 @@ void Audio::CloseSdlDevice(PortOut* port) {
}
const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
std::vector<uint8_t>* buffer, float gain) {
std::vector<uint8_t>* buffer, float gain,
const float* gains) {
EXIT_IF(data == nullptr);
EXIT_IF(buffer == nullptr);
@@ -312,12 +315,12 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
const auto bytes_per_sample = BytesPerSample(port.format);
const bool reorder = channels >= 8 && !FormatIsStd(port.format);
bool volume_changed = gain != 1.0f;
std::array<double, 12> scales {};
bool volume_changed = false;
for (uint32_t ch = 0; ch < channels; ch++) {
if (port.volume[ch] != 32768) {
volume_changed = true;
break;
}
scales[ch] = (static_cast<double>(port.volume[ch]) / 32768.0) * gain *
(gains != nullptr ? gains[ch] : 1.0f);
volume_changed |= scales[ch] != 1.0;
}
if (!volume_changed && !reorder) {
@@ -337,8 +340,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
for (uint32_t ch = 0; ch < output_channels; ch++) {
const auto src_ch = reorder ? SDL_8CH_MAP[ch] : ch;
dst[frame * output_channels + ch] =
src[frame * channels + src_ch] *
(static_cast<float>(port.volume[src_ch]) / 32768.0f) * gain;
static_cast<float>(src[frame * channels + src_ch] * scales[src_ch]);
}
if (channels == 12) {
// Add the four top channels to their front/back channels, turning 12 into 8.
@@ -346,8 +348,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
static constexpr uint32_t HEIGHT_DST[4] = {0, 1, 4, 5};
for (uint32_t ch = 0; ch < 4; ch++) {
dst[frame * output_channels + HEIGHT_DST[ch]] +=
src[frame * channels + 8 + ch] *
(static_cast<float>(port.volume[8 + ch]) / 32768.0f) * gain;
static_cast<float>(src[frame * channels + 8 + ch] * scales[8 + ch]);
}
}
}
@@ -358,15 +359,9 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
for (uint32_t frame = 0; frame < frames; frame++) {
for (uint32_t ch = 0; ch < output_channels; ch++) {
const auto src_ch = reorder ? SDL_8CH_MAP[ch] : ch;
int64_t sample =
static_cast<int64_t>(src[frame * channels + src_ch]) * port.volume[src_ch] / 32768;
sample = static_cast<int64_t>(sample * static_cast<double>(gain));
if (sample > std::numeric_limits<int16_t>::max()) {
sample = std::numeric_limits<int16_t>::max();
} else if (sample < std::numeric_limits<int16_t>::min()) {
sample = std::numeric_limits<int16_t>::min();
}
dst[frame * output_channels + ch] = static_cast<int16_t>(sample);
const double sample = src[frame * channels + src_ch] * scales[src_ch];
dst[frame * output_channels + ch] = static_cast<int16_t>(
std::clamp(sample, -32768.0, 32767.0));
}
}
}
@@ -374,7 +369,8 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
return buffer->data();
}
bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain) {
bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain,
const float* gains) {
EXIT_IF(port == nullptr);
if (port->stream == nullptr || data == nullptr) {
@@ -382,7 +378,7 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float
}
std::vector<uint8_t> prepared_buffer;
const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer, gain);
const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer, gain, gains);
const auto output_channels = OutputChannels(*port);
const auto prepared_size =
BytesPerSample(port->format) * output_channels * port->samples_num;
@@ -570,14 +566,14 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
controller_queued_us = Controller::DualSenseHaptics::Queue(
port.haptics, Controller::GetActiveControllerId(), params[i].data, port.samples_num,
static_cast<uint32_t>(port.channels_num), FormatIsFloat(port.format), port.volume,
gain);
gain, params[i].gains);
controller_uses_bluetooth =
Controller::DualSenseHaptics::UsesBluetooth(port.haptics);
}
if (controller_queued_us == 0) {
// No DualSense took it (e.g. it was unplugged); a pad speaker port plays on the main
// output instead, and a vibration port has none.
if (QueueSdlAudio(&port, params[i].data, blocking, gain)) {
if (QueueSdlAudio(&port, params[i].data, blocking, gain, params[i].gains)) {
any_device = true;
paced[i] = port.queue_primed;
}
+3 -2
View File
@@ -19,8 +19,9 @@ enum class Format {
};
struct OutputParam {
int handle = 0;
const void* data = nullptr;
int handle = 0;
const void* data = nullptr;
const float* gains = nullptr;
};
static constexpr int OUT_PORTS_MAX = 32;
+199 -55
View File
@@ -24,8 +24,12 @@ namespace Libs::Controller {
LIB_NAME("Pad", "Pad");
constexpr int PAD_ERROR_INVALID_ARG = -2137915391; /* 0x80920001 */
constexpr int PAD_ERROR_INVALID_HANDLE = -2137915389; /* 0x80920003 */
constexpr int PAD_ERROR_INVALID_ARG = -2137915391; /* 0x80920001 */
constexpr int PAD_ERROR_INVALID_HANDLE = -2137915389; /* 0x80920003 */
constexpr int PAD_ERROR_ALREADY_OPENED = -2137915388; /* 0x80920004 */
constexpr int PAD_ERROR_NOT_INITIALIZED = -2137915387; /* 0x80920005 */
constexpr int PAD_ERROR_NO_HANDLE = -2137915384; /* 0x80920008 */
constexpr int PAD_HANDLE = 1;
constexpr uint32_t RUMBLE_DURATION_MS = 0xffff;
constexpr uint32_t RELEASE_FLUSH_MS = 50;
@@ -67,8 +71,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;
@@ -102,6 +111,12 @@ public:
KYTY_CLASS_NO_COPY(GameController);
int InitPort();
int OpenPort(int user_id, int type, int index);
int GetPortHandle(int user_id, int type, int index);
int ClosePort(int handle);
int CheckPortHandle(int handle);
void Connect(int id);
void Disconnect(int id);
void Button(int id, uint32_t button, bool down);
@@ -118,6 +133,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);
@@ -125,14 +141,19 @@ public:
private:
static constexpr uint32_t STATES_MAX = 64;
enum class PortState { Uninitialized, Closed, Open };
int CheckPortHandleLocked(int handle) const;
void CheckActive();
void AddState();
// 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;
PortState m_port_state = PortState::Uninitialized;
std::vector<int> m_connected_ids;
int m_active_id = -1;
bool m_connected = false;
@@ -152,6 +173,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 +352,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 +486,7 @@ void GameController::CheckActive() {
m_vibration = {};
m_vibration_until = 0;
m_trigger_effect = {};
m_trigger_state = {};
}
void GameController::AddState() {
@@ -466,6 +534,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 +691,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 +800,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;
@@ -853,21 +939,71 @@ int GetActiveControllerId() {
return g_controller != nullptr ? g_controller->GetActiveControllerId() : -1;
}
int KYTY_SYSV_ABI PadInit() {
PRINT_NAME();
static bool PadOpenArgsAreValid(int user_id, int type, int index) {
// The host keyboard and gamepad feed one standard port for the configured user.
return user_id == Config::GetUserId() && type == 0 && index == 0;
}
int GameController::InitPort() {
Common::LockGuard lock(m_mutex);
if (m_port_state == PortState::Uninitialized) {
m_port_state = PortState::Closed;
}
return OK;
}
static bool PadOpenArgsAreValid(int user_id, int type, int index) {
constexpr int user_id_system = 0xff;
constexpr int port_type_standard = 0;
constexpr int port_type_special = 2;
constexpr int port_type_remote = 16;
const bool personal_port =
user_id == Config::GetUserId() && (type == port_type_standard || type == port_type_special);
const bool system_remote_control = user_id == user_id_system && type == port_type_remote;
return index == 0 && (personal_port || system_remote_control);
int GameController::OpenPort(int user_id, int type, int index) {
Common::LockGuard lock(m_mutex);
if (m_port_state == PortState::Uninitialized) {
return PAD_ERROR_NOT_INITIALIZED;
}
if (!PadOpenArgsAreValid(user_id, type, index)) {
return PAD_ERROR_INVALID_ARG;
}
if (m_port_state == PortState::Open) {
return PAD_ERROR_ALREADY_OPENED;
}
m_port_state = PortState::Open;
return PAD_HANDLE;
}
int GameController::GetPortHandle(int user_id, int type, int index) {
Common::LockGuard lock(m_mutex);
if (m_port_state == PortState::Uninitialized) {
return PAD_ERROR_NOT_INITIALIZED;
}
return m_port_state == PortState::Open && PadOpenArgsAreValid(user_id, type, index) ? PAD_HANDLE
: PAD_ERROR_NO_HANDLE;
}
int GameController::CheckPortHandleLocked(int handle) const {
if (m_port_state == PortState::Uninitialized) {
return PAD_ERROR_NOT_INITIALIZED;
}
return m_port_state == PortState::Open && handle == PAD_HANDLE ? OK : PAD_ERROR_INVALID_HANDLE;
}
int GameController::CheckPortHandle(int handle) {
Common::LockGuard lock(m_mutex);
return CheckPortHandleLocked(handle);
}
int GameController::ClosePort(int handle) {
Common::LockGuard lock(m_mutex);
if (const auto error = CheckPortHandleLocked(handle); error != OK) {
return error;
}
m_port_state = PortState::Closed;
return OK;
}
int PadCheckHandle(int handle) {
return g_controller->CheckPortHandle(handle);
}
int KYTY_SYSV_ABI PadInit() {
PRINT_NAME();
return g_controller->InitPort();
}
int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param) {
@@ -879,15 +1015,7 @@ int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param) {
"\t param = 0x%016" PRIx64 "\n",
user_id, type, index, reinterpret_cast<uint64_t>(param));
constexpr int pad_error_invalid_arg = -2137915391; /* 0x80920001 */
if (!PadOpenArgsAreValid(user_id, type, index)) {
return pad_error_invalid_arg;
}
int handle = 1;
return handle;
return g_controller->OpenPort(user_id, type, index);
}
int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index) {
@@ -898,20 +1026,20 @@ int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index) {
"\t index = %d\n",
user_id, type, index);
constexpr int pad_error_device_no_handle = -2137915384; /* 0x80920008 */
return g_controller->GetPortHandle(user_id, type, index);
}
if (!PadOpenArgsAreValid(user_id, type, index)) {
return pad_error_device_no_handle;
}
return 1;
int KYTY_SYSV_ABI PadClose(int handle) {
PRINT_NAME();
LOGF("\t handle = %d\n", handle);
return g_controller->ClosePort(handle);
}
int KYTY_SYSV_ABI PadSetMotionSensorState(int handle, bool enable) {
PRINT_NAME();
if (handle != 1) {
return PAD_ERROR_INVALID_HANDLE;
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
}
LOGF("\t enable = %s\n", (enable ? "true" : "false"));
@@ -923,8 +1051,8 @@ int KYTY_SYSV_ABI PadSetMotionSensorState(int handle, bool enable) {
int KYTY_SYSV_ABI PadSetAngularVelocityDeadbandState(int handle, bool enable) {
PRINT_NAME();
if (handle != 1) {
return PAD_ERROR_INVALID_HANDLE;
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
}
LOGF("\t enable = %s\n", (enable ? "true" : "false"));
@@ -935,8 +1063,8 @@ int KYTY_SYSV_ABI PadSetAngularVelocityDeadbandState(int handle, bool enable) {
int KYTY_SYSV_ABI PadResetOrientation(int handle) {
PRINT_NAME();
if (handle != 1) {
return PAD_ERROR_INVALID_HANDLE;
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
}
g_controller->ResetOrientation();
@@ -946,18 +1074,17 @@ int KYTY_SYSV_ABI PadResetOrientation(int handle) {
int KYTY_SYSV_ABI PadGetControllerInformation(int handle, PadControllerInformation* info) {
PRINT_NAME();
int connected_count = 0;
bool connected = false;
g_controller->GetConnectionInfo(&connected, &connected_count);
if (handle != 1) {
return PAD_ERROR_INVALID_HANDLE;
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
}
if (info == nullptr) {
return PAD_ERROR_INVALID_ARG;
}
int connected_count = 0;
bool connected = false;
g_controller->GetConnectionInfo(&connected, &connected_count);
std::memset(info, 0, sizeof(*info));
info->touch_pixel_density = 44.86f;
@@ -976,8 +1103,8 @@ int KYTY_SYSV_ABI PadGetControllerInformation(int handle, PadControllerInformati
int KYTY_SYSV_ABI PadIsRemoteController(int handle, bool* is_remote) {
PRINT_NAME();
if (handle != 1) {
return PAD_ERROR_INVALID_HANDLE;
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
}
if (is_remote == nullptr) {
return PAD_ERROR_INVALID_ARG;
@@ -990,8 +1117,8 @@ int KYTY_SYSV_ABI PadIsRemoteController(int handle, bool* is_remote) {
int KYTY_SYSV_ABI PadReadState(int handle, PadData* data) {
PRINT_NAME();
if (handle != 1) {
return PAD_ERROR_INVALID_HANDLE;
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
}
if (data == nullptr) {
return PAD_ERROR_INVALID_ARG;
@@ -1012,8 +1139,8 @@ int KYTY_SYSV_ABI PadRead(int handle, PadData* data, int num) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(num < 1 || num > 64);
if (handle != 1) {
return PAD_ERROR_INVALID_HANDLE;
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
}
if (data == nullptr) {
return PAD_ERROR_INVALID_ARG;
@@ -1044,8 +1171,8 @@ int KYTY_SYSV_ABI PadRead(int handle, PadData* data, int num) {
int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param) {
PRINT_NAME();
if (handle != 1) {
return PAD_ERROR_INVALID_HANDLE;
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
}
if (param == nullptr) {
return PAD_ERROR_INVALID_ARG;
@@ -1063,8 +1190,8 @@ int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param) {
int KYTY_SYSV_ABI PadResetLightBar(int handle) {
PRINT_NAME();
if (handle != 1) {
return PAD_ERROR_INVALID_HANDLE;
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
}
return OK;
@@ -1073,8 +1200,8 @@ int KYTY_SYSV_ABI PadResetLightBar(int handle) {
int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param) {
PRINT_NAME();
if (handle != 1) {
return PAD_ERROR_INVALID_HANDLE;
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
}
if (param == nullptr) {
return PAD_ERROR_INVALID_ARG;
@@ -1088,8 +1215,8 @@ int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param) {
int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* param) {
PRINT_NAME();
if (handle != 1) {
return PAD_ERROR_INVALID_HANDLE;
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
}
if (param == nullptr) {
return PAD_ERROR_INVALID_ARG;
@@ -1098,4 +1225,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 (const auto error = PadCheckHandle(handle); error != OK) {
return error;
}
if (info == nullptr) {
return PAD_ERROR_INVALID_ARG;
}
g_controller->GetTriggerEffectState(info);
return OK;
}
} // namespace Libs::Controller
+5
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);
@@ -80,9 +81,12 @@ enum class Setting { SpeakerVolume, VibrationIntensity, TriggerEffectIntensity }
void CycleSetting(Setting setting);
float GetSettingScale(Setting setting);
int PadCheckHandle(int handle);
int KYTY_SYSV_ABI PadInit();
int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param);
int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index);
int KYTY_SYSV_ABI PadClose(int handle);
int KYTY_SYSV_ABI PadSetMotionSensorState(int handle, bool enable);
int KYTY_SYSV_ABI PadSetAngularVelocityDeadbandState(int handle, bool enable);
int KYTY_SYSV_ABI PadResetOrientation(int handle);
@@ -92,6 +96,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 -2
View File
@@ -315,7 +315,7 @@ bool UsesBluetooth(const Stream* stream) {
}
uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume, float gain) {
bool is_float, const int* volume, float gain, const float* gains) {
if (stream == nullptr || data == nullptr || frames == 0 || channels == 0 || volume == nullptr) {
return 0;
}
@@ -357,7 +357,7 @@ uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames
const auto index = (static_cast<size_t>(first_frame) + frame) * channels + src_ch;
float value = is_float ? static_cast<const float*>(data)[index]
: static_cast<const int16_t*>(data)[index] / 32768.0f;
value *= volume[src_ch] / 32768.0f * gain;
value *= volume[src_ch] / 32768.0f * gain * (gains != nullptr ? gains[src_ch] : 1.0f);
// USB places the speaker in front and the actuators in back; Bluetooth takes stereo.
stream->buffer[static_cast<size_t>(frame) * output_channels + ch + channel_offset] =
value;
+2 -1
View File
@@ -17,7 +17,8 @@ bool UsesBluetooth(const Stream* stream);
// (the active pad has no usable controller audio device or its speaker cannot be routed).
// Ambiguous USB or Bluetooth endpoints are not selected.
uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume, float gain = 1.0f);
bool is_float, const int* volume, float gain = 1.0f,
const float* gains = nullptr);
// Returns false for other gamepad types, which retain the normal rumble path.
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor,
uint32_t duration_ms = 0xffff);
+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);
}
+74 -56
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"
@@ -18,6 +19,7 @@
#include <cstdint>
#include <cstring>
#include <deque>
#include <filesystem>
#include <limits>
#include <mutex>
#include <string>
@@ -47,14 +49,15 @@ struct ResolvedPathInfo {
uint32_t file_id = 0xffffffffu;
uint64_t file_size = 0;
bool is_dir = false;
std::string host_path;
std::filesystem::path host_path;
};
static std::mutex g_mutex;
static uint32_t g_next_submission_id = 1;
static uint32_t g_next_file_id = 1;
static std::unordered_map<uint32_t, bool> g_submissions;
static std::condition_variable g_submission_cv;
static std::unordered_map<uint32_t, std::string> g_files;
static std::unordered_map<uint32_t, std::filesystem::path> g_files;
static std::unordered_map<uint32_t, uint64_t> g_file_sizes;
static std::unordered_map<std::string, ResolvedPathInfo> g_resolved_paths;
@@ -92,15 +95,6 @@ static bool WriteGuest(uint64_t addr, const T& value) {
return WriteGuestBytes(addr, &value, sizeof(T));
}
static uint32_t ComputeFileId(const char* guest_path) {
uint32_t hash = 2166136261u;
for (auto* p = reinterpret_cast<const uint8_t*>(guest_path); p != nullptr && *p != 0; ++p) {
hash ^= *p;
hash *= 16777619u;
}
return hash & static_cast<uint32_t>(std::numeric_limits<int32_t>::max());
}
static int ReadGuestCString(uint64_t addr, char* out, size_t out_size) {
if (addr == 0) {
return LibKernel::KERNEL_ERROR_EFAULT;
@@ -120,19 +114,7 @@ static int ReadGuestCString(uint64_t addr, char* out, size_t out_size) {
return LibKernel::KERNEL_ERROR_ENAMETOOLONG;
}
static void RegisterHostPathLocked(uint32_t file_id, const std::string& host_path,
uint64_t file_size, bool is_dir) {
g_files[file_id] = host_path;
g_file_sizes[file_id] = is_dir ? 0 : file_size;
}
static void RegisterHostPath(uint32_t file_id, const std::string& host_path, uint64_t file_size,
bool is_dir) {
std::scoped_lock lock(g_mutex);
RegisterHostPathLocked(file_id, host_path, file_size, is_dir);
}
static bool TryGetHostPath(uint32_t file_id, std::string* out) {
static bool TryGetHostPath(uint32_t file_id, std::filesystem::path* out) {
std::scoped_lock lock(g_mutex);
const auto it = g_files.find(file_id);
if (it == g_files.end()) {
@@ -152,7 +134,7 @@ static bool TryGetHostFileSize(uint32_t file_id, uint64_t* out) {
return true;
}
static int GetHostPathStat(const std::string& host_path, LibKernel::FileSystem::FileStat* st) {
static int GetHostPathStat(const std::filesystem::path& host_path, LibKernel::FileSystem::FileStat* st) {
if (st == nullptr) {
return LibKernel::KERNEL_ERROR_EINVAL;
}
@@ -213,8 +195,7 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
if (!found) {
const auto real_path = LibKernel::FileSystem::GetRealFilename(path);
info.file_id = AprShared::ComputeFileId(guest_path);
info.host_path = Common::PathToString(real_path);
info.host_path = real_path;
if (Common::File::IsDirectoryExisting(real_path)) {
info.is_dir = true;
@@ -232,16 +213,21 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
if (!inserted) {
info = it->second;
} else if (info.result == OK) {
RegisterHostPathLocked(info.file_id, info.host_path, info.file_size, info.is_dir);
// Allocate once under the cache lock; distinct paths can share a hash.
if (g_next_file_id == 0xffffffffu) {
info.result = it->second.result = LibKernel::KERNEL_ERROR_ENFILE;
} else {
info.file_id = it->second.file_id = g_next_file_id++;
g_files.emplace(info.file_id, info.host_path);
g_file_sizes.emplace(info.file_id, info.is_dir ? 0 : info.file_size);
}
} else if (info.result == LibKernel::KERNEL_ERROR_ENOENT) {
log_missing = true;
}
}
if (log_missing) {
LOGF("\tAPR resolve missing path: %s -> %s\n", guest_path, info.host_path.c_str());
LOGF("\tAPR resolve missing path: %s -> %s\n", guest_path, Common::PathToString(info.host_path).c_str());
}
} else if (info.result == OK) {
AprShared::RegisterHostPath(info.file_id, info.host_path, info.file_size, info.is_dir);
}
if (info.result != OK) {
@@ -423,7 +409,7 @@ static int KYTY_SYSV_ABI GetFileStat(uint32_t file_id, LibKernel::FileSystem::Fi
return KernelSyscallResult(LibKernel::KERNEL_ERROR_EINVAL);
}
std::string host_path;
std::filesystem::path host_path;
if (!AprShared::TryGetHostPath(file_id, &host_path)) {
LOGF("\tAPR stat failed for unknown file id: 0x%08" PRIx32 "\n", file_id);
return KernelSyscallResult(LibKernel::KERNEL_ERROR_ENOENT);
@@ -445,7 +431,7 @@ static int KYTY_SYSV_ABI GetFileSize(uint32_t file_id, uint64_t* size) {
return KernelSyscallResult(LibKernel::KERNEL_ERROR_EFAULT);
}
std::string host_path;
std::filesystem::path host_path;
if (!AprShared::TryGetHostPath(file_id, &host_path)) {
LOGF("\tAPR size failed for unknown file id: 0x%08" PRIx32 "\n", file_id);
return KernelSyscallResult(LibKernel::KERNEL_ERROR_ENOENT);
@@ -665,10 +651,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;
@@ -680,7 +671,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;
@@ -705,6 +696,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) {
@@ -1169,13 +1162,13 @@ static bool AppendCommandRecord(uint64_t command_buffer, CommandBufferState::Com
return CommitCommandBufferRecord(command_buffer, &state, record_size);
}
static int ReadHostFileToGuest(const std::string& host_path, uint64_t file_offset,
static int ReadHostFileToGuest(const std::filesystem::path& host_path, uint64_t file_offset,
uint64_t destination, uint64_t size, uint64_t* bytes_read);
static int ExecuteCommand(const CommandBufferState::Command& entry) {
if (const auto* payload = std::get_if<CommandBufferState::ReadFileCommand>(&entry.data)) {
const auto& command = *payload;
std::string host_path;
std::filesystem::path host_path;
if (!AprShared::TryGetHostPath(command.file_id, &host_path)) {
LOGF("\tAPR submit failed for unknown file id: 0x%08" PRIx32 "\n", command.file_id);
return LibKernel::KERNEL_ERROR_ENOENT;
@@ -1186,7 +1179,7 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) {
command.size, &bytes_read);
if (result != OK) {
LOGF("\tAPR submit read failed: id=0x%08" PRIx32 ", result=0x%08" PRIx32 ", path=%s\n",
command.file_id, static_cast<uint32_t>(result), host_path.c_str());
command.file_id, static_cast<uint32_t>(result), Common::PathToString(host_path).c_str());
return result;
}
} else if (const auto* payload =
@@ -1207,6 +1200,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;
@@ -1235,17 +1231,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);
}
}
@@ -1279,7 +1278,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;
@@ -1382,7 +1381,7 @@ static bool AdvanceCommandBuffer(uint64_t command_buffer, uint64_t record_size)
return CommitCommandBufferRecord(command_buffer, &state, record_size);
}
static int ReadHostFileToGuest(const std::string& host_path, uint64_t file_offset,
static int ReadHostFileToGuest(const std::filesystem::path& host_path, uint64_t file_offset,
uint64_t destination, uint64_t size, uint64_t* bytes_read) {
if (bytes_read == nullptr) {
return LibKernel::KERNEL_ERROR_EINVAL;
@@ -1398,7 +1397,7 @@ static int ReadHostFileToGuest(const std::string& host_path, uint64_t file_offse
Common::File file;
if (!file.Open(host_path, Common::File::Mode::Read)) {
LOGF("\tAPR read missing host file: %s\n", host_path.c_str());
LOGF("\tAPR read missing host file: %s\n", Common::PathToString(host_path).c_str());
return LibKernel::KERNEL_ERROR_ENOENT;
}
const auto file_size = file.Size();
@@ -1927,18 +1926,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,
@@ -1970,11 +1978,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;
}
+219 -100
View File
@@ -11,6 +11,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstring>
#include <vector>
@@ -170,12 +171,14 @@ static std::atomic_uint64_t g_audioout2_next_port {1};
static AudioOut2UserHandle g_audioout2_next_user = 1;
struct AudioOut2ContextState {
bool used = false;
AudioOut2ContextHandle handle = 0;
uint32_t queue_depth = 4;
uint32_t queued = 0;
uint32_t num_grains = 512;
uint64_t last_update = 0;
bool used = false;
AudioOut2ContextHandle handle = 0;
uint32_t queue_depth = 4;
uint32_t queued = 0;
uint32_t num_grains = 512;
uint64_t last_update = 0;
int object_audio_handle = 0;
std::vector<float> object_mix;
};
struct AudioOut2PortStateEntry {
@@ -189,9 +192,11 @@ struct AudioOut2PortStateEntry {
uint32_t samples_num = 512;
AudioInternal::Format audio_format = AudioInternal::Format::Unknown;
int audio_handle = 0;
std::array<float, 16> gains {};
uint32_t ambisonics = UINT32_MAX;
bool attributes_locked = false;
std::vector<uint8_t> pcm_data;
};
struct AudioOut2SpeakerArrayState {
bool used = false;
uint32_t num_speakers = 0;
@@ -207,9 +212,8 @@ struct AudioOut2LatencyState {
uint32_t latency_us = 0;
};
static Common::Mutex g_audioout2_context_mutex;
static Common::Mutex g_audioout2_state_mutex;
static std::array<AudioOut2ContextState, 16> g_audioout2_contexts;
static Common::Mutex g_audioout2_port_mutex;
static std::array<AudioOut2PortStateEntry, 256> g_audioout2_ports;
static std::vector<AudioOut2UserHandle> g_audioout2_users;
static Common::Mutex g_audioout2_speaker_array_mutex;
@@ -223,7 +227,11 @@ static constexpr int AUDIO_OUT2_ERROR_INVALID_PARAM = -2144960511;
static constexpr int AUDIO_OUT2_ERROR_BUSY = -2144960505; /* 0x80268007 */
static constexpr int AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM = -2144959999; /* 0x80268201 */
static constexpr int AUDIO_OUT2_ERROR_MASTERING_INVALID_STATES_ID = -2144959996; /* 0x80268204 */
static constexpr int AUDIO_OUT2_ERROR_OUT_OF_RESOURCE = -2144960510; /* 0x80268002 */
static constexpr int AUDIO_OUT2_ERROR_INVALID_PORT = -2144960503; /* 0x80268009 */
static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM = 0;
static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN = 1;
static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS = 8;
static constexpr uint32_t AUDIO_OUT2_MASTERING_OUTPUT_RECORDING = 2;
static constexpr uint32_t AUDIO_OUT2_MASTERING_STATES_ID_DEFAULT = 1;
static constexpr uint32_t AUDIO_OUT2_MASTERING_STATES_ID_V2 = 2;
@@ -350,37 +358,80 @@ static size_t audioout2_pcm_size(const AudioOut2PortStateEntry& state) {
audioout2_data_format_channels(state.data_format) * bytes_per_sample;
}
static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) {
Common::LockGuard lock(g_audioout2_port_mutex);
static bool audioout2_context_has_queueable_device(const AudioOut2ContextState& context) {
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
!state.pcm_data.empty() && AudioInternal::AudioOutHasDevice(state.audio_handle)) {
return true;
if (state.used && state.context == context.handle && !state.pcm_data.empty()) {
const int handle = audioout2_port_type_is_object(state.port_type)
? context.object_audio_handle
: state.audio_handle;
if (AudioInternal::AudioOutHasDevice(handle)) {
return true;
}
}
}
return false;
}
static void audioout2_mix_object(AudioOut2ContextState& context,
const AudioOut2PortStateEntry& object) {
auto& mix = context.object_mix;
if (mix.empty()) {
mix.resize(static_cast<size_t>(context.num_grains) * 2, 0.0f);
}
// Center ordinary objects. First-order stereo ambisonics uses left/right-facing
// cardioids; FuMa W has a different normalization from ACN/SN3D W.
constexpr float half_power = 0.70710678f;
float left = half_power, right = half_power;
switch (object.ambisonics) {
case UINT32_MAX:
case 64: break;
case 0: left = right = 1.0f; break;
case 2:
case 65: right = -half_power; break;
default: return;
}
left *= object.gains[0];
right *= object.gains[0];
for (uint32_t frame = 0; frame < context.num_grains; frame++) {
const float sample =
object.audio_format == AudioInternal::Format::FloatMono
? reinterpret_cast<const float*>(object.pcm_data.data())[frame]
: reinterpret_cast<const int16_t*>(object.pcm_data.data())[frame] / 32768.0f;
mix[frame * 2] += sample * left;
mix[frame * 2 + 1] += sample * right;
}
}
static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, bool blocking) {
// The backend consumes the buffers synchronously, but can block while pacing SDL's queue.
// Keep both PCM storage and port handles alive until it returns.
Common::LockGuard lock(g_audioout2_state_mutex);
auto* context = audioout2_find_context_locked(ctx);
if (context == nullptr) {
return;
}
context->object_mix.clear();
std::vector<AudioInternal::OutputParam> params;
params.reserve(AudioInternal::OUT_PORTS_MAX);
Common::LockGuard lock(g_audioout2_port_mutex);
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
!state.pcm_data.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data.data()});
if (!state.used || state.context != ctx || state.pcm_data.empty()) {
continue;
}
if (audioout2_port_type_is_object(state.port_type)) {
audioout2_mix_object(*context, state);
} else if (state.audio_handle > 0 && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back({state.audio_handle, state.pcm_data.data(), state.gains.data()});
}
}
if (!context->object_mix.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back({context->object_audio_handle, context->object_mix.data()});
}
if (!params.empty()) {
(void)AudioInternal::AudioOutOutputs(params.data(), static_cast<uint32_t>(params.size()),
blocking);
}
}
static void audioout2_close_audio_handle(int audio_handle) {
if (audio_handle > 0) {
AudioInternal::AudioOutClose(audio_handle);
@@ -431,52 +482,58 @@ int KYTY_SYSV_ABI AudioOut2ContextCreate(const AudioOut2ContextParam* params, vo
EXIT_NOT_IMPLEMENTED(params == nullptr);
EXIT_NOT_IMPLEMENTED(ctx == nullptr);
*ctx = g_audioout2_next_context.fetch_add(1, std::memory_order_relaxed);
g_audioout2_context_mutex.Lock();
auto* state = audioout2_find_context_locked(0);
if (state == nullptr) {
for (auto& candidate: g_audioout2_contexts) {
if (!candidate.used) {
state = &candidate;
break;
}
}
const auto num_grains = params->num_grains == 0 ? 512u : params->num_grains;
const int object_audio_handle =
params->max_object_ports == 0
? 0
: AudioInternal::AudioOutOpen(AUDIO_OUT_PORT_TYPE_MAIN, num_grains, 48000,
AudioInternal::Format::FloatStereo);
if (params->max_object_ports != 0 && object_audio_handle == 0) {
return AUDIO_OUT2_ERROR_OUT_OF_RESOURCE;
}
EXIT_NOT_IMPLEMENTED(state == nullptr);
*state = AudioOut2ContextState {};
state->used = true;
state->handle = *ctx;
state->queue_depth = (params->queue_depth == 0 ? 4u : params->queue_depth);
state->queued = 0;
state->num_grains = (params->num_grains == 0 ? 512u : params->num_grains);
state->last_update = LibKernel::KernelGetProcessTime();
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Lock();
auto it = std::find_if(g_audioout2_contexts.begin(), g_audioout2_contexts.end(),
[](const auto& state) { return !state.used; });
if (it == g_audioout2_contexts.end()) {
g_audioout2_state_mutex.Unlock();
audioout2_close_audio_handle(object_audio_handle);
return AUDIO_OUT2_ERROR_OUT_OF_RESOURCE;
}
*ctx = g_audioout2_next_context.fetch_add(1, std::memory_order_relaxed);
auto* state = &*it;
*state = AudioOut2ContextState {};
state->used = true;
state->handle = *ctx;
state->queue_depth = params->queue_depth == 0 ? 4u : params->queue_depth;
state->num_grains = num_grains;
state->last_update = LibKernel::KernelGetProcessTime();
state->object_audio_handle = object_audio_handle;
g_audioout2_state_mutex.Unlock();
LOGF("\t buffer = 0x%016" PRIx64 "\n"
"\t buffer_size = 0x%016" PRIx64 "\n"
"\t ctx = 0x%016" PRIx64 "\n"
"\t queue_depth = %" PRIu32 ", num_grains = %" PRIu32 "\n",
reinterpret_cast<uint64_t>(buffer), static_cast<uint64_t>(buffer_size), *ctx,
state->queue_depth, state->num_grains);
(params->queue_depth == 0 ? 4u : params->queue_depth), num_grains);
return OK;
}
int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
PRINT_NAME();
LOGF("\t ctx = 0x%016" PRIx64 "\n", ctx);
g_audioout2_context_mutex.Lock();
std::array<int, 257> audio_handles {};
size_t audio_handles_num = 0;
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
if (state->object_audio_handle > 0) {
audio_handles[audio_handles_num++] = state->object_audio_handle;
}
*state = AudioOut2ContextState {};
}
g_audioout2_context_mutex.Unlock();
std::array<int, 256> audio_handles {};
size_t audio_handles_num = 0;
g_audioout2_port_mutex.Lock();
for (auto& port_state: g_audioout2_ports) {
if (port_state.used && port_state.context == ctx) {
if (port_state.audio_handle > 0 && audio_handles_num < audio_handles.size()) {
@@ -485,7 +542,7 @@ int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
port_state = AudioOut2PortStateEntry {};
}
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
for (size_t i = 0; i < audio_handles_num; i++) {
audioout2_close_audio_handle(audio_handles[i]);
@@ -504,11 +561,16 @@ int KYTY_SYSV_ABI AudioOut2ContextSetAttributes(AudioOut2ContextHandle ctx,
}
int KYTY_SYSV_ABI AudioOut2ContextAdvance(AudioOut2ContextHandle ctx) {
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
audioout2_update_context_locked(state);
}
g_audioout2_context_mutex.Unlock();
for (auto& port: g_audioout2_ports) {
if (port.used && port.context == ctx) {
port.attributes_locked = false;
}
}
g_audioout2_state_mutex.Unlock();
return OK;
}
@@ -520,11 +582,10 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
// Only a synchronous submission carrying PCM to a real device can rely on the SDL queue for
// pacing. Async pushes must retain queue-depth backpressure, and a handle without PCM (or a
// vibration/failed-open handle) has no downstream operation that can block this call.
const bool use_device_clock =
blocking != 0 && audioout2_context_has_queueable_device(ctx);
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
const bool use_device_clock =
blocking != 0 && audioout2_context_has_queueable_device(*state);
audioout2_update_context_locked(state);
sleep_micros = audioout2_grain_micros(state->num_grains);
if (state->queued < state->queue_depth || use_device_clock) {
@@ -534,12 +595,12 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
if (state->queued < state->queue_depth) {
state->queued++;
}
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
audioout2_queue_context_audio(ctx, blocking != 0);
return OK;
}
}
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
if (blocking == 0) {
return AUDIO_OUT2_ERROR_NOT_READY;
@@ -558,7 +619,7 @@ int KYTY_SYSV_ABI AudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint
*available_queues = 4;
}
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
audioout2_update_context_locked(state);
if (queue_level != nullptr) {
@@ -569,7 +630,7 @@ int KYTY_SYSV_ABI AudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint
(state->queued < state->queue_depth ? state->queue_depth - state->queued : 0);
}
}
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
return OK;
}
@@ -583,19 +644,25 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
const auto audio_format = audioout2_data_format_to_audio_format(params->data_format);
const auto audio_type = audioout2_port_type_to_audio_out_type(params->port_type);
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
const auto* context_state = audioout2_find_context_locked(ctx);
if (context_state == nullptr) {
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
const auto samples_num = context_state->num_grains == 0 ? 512u : context_state->num_grains;
if (audioout2_port_type_is_object(params->port_type) &&
(params->port_type != 0x100 || context_state->object_audio_handle == 0 ||
params->sampling_freq != 48000 ||
(audio_format != AudioInternal::Format::FloatMono &&
audio_format != AudioInternal::Format::Signed16bitMono))) {
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
const auto samples_num = context_state->num_grains;
g_audioout2_port_mutex.Lock();
if (std::find(g_audioout2_users.begin(), g_audioout2_users.end(), params->user_handle) ==
g_audioout2_users.end()) {
g_audioout2_port_mutex.Unlock();
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
AudioOut2PortStateEntry* port_state = nullptr;
@@ -616,9 +683,9 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
port_state->sampling_freq = params->sampling_freq;
port_state->samples_num = samples_num;
port_state->audio_format = audio_format;
port_state->gains.fill(1.0f);
}
g_audioout2_port_mutex.Unlock();
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
if (port_state == nullptr) {
return AUDIO_OUT2_ERROR_PORT_FULL;
@@ -632,12 +699,12 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
audio_format);
}
g_audioout2_port_mutex.Lock();
g_audioout2_state_mutex.Lock();
const bool reserved = port_state->used && port_state->handle == next_port;
if (reserved) {
port_state->audio_handle = audio_handle;
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
if (!reserved) {
audioout2_close_audio_handle(audio_handle);
return AUDIO_OUT2_ERROR_INVALID_PARAM;
@@ -663,12 +730,12 @@ int KYTY_SYSV_ABI AudioOut2PortDestroy(AudioOut2PortHandle port) {
LOGF("\t port = 0x%016" PRIx64 "\n", port);
int audio_handle = 0;
g_audioout2_port_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_port_locked(port); state != nullptr) {
audio_handle = state->audio_handle;
*state = AudioOut2PortStateEntry {};
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
audioout2_close_audio_handle(audio_handle);
@@ -677,36 +744,86 @@ int KYTY_SYSV_ABI AudioOut2PortDestroy(AudioOut2PortHandle port) {
int KYTY_SYSV_ABI AudioOut2PortSetAttributes(AudioOut2PortHandle port,
const AudioOut2Attribute* attributes, uint32_t num) {
EXIT_NOT_IMPLEMENTED(num != 0 && attributes == nullptr);
if (num != 0 && attributes == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
Common::LockGuard lock(g_audioout2_state_mutex);
auto* state = audioout2_find_port_locked(port);
if (state == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PORT;
}
const void* pcm_data = nullptr;
bool has_pcm = false;
const void* pcm_data = nullptr;
auto gains = state->gains;
auto ambisonics = state->ambisonics;
uint32_t seen = 0;
for (uint32_t i = 0; i < num; i++) {
if (attributes[i].attribute_id == AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM &&
attributes[i].value != nullptr && attributes[i].value_size >= sizeof(AudioOut2Pcm)) {
AudioOut2Pcm pcm {};
std::memcpy(&pcm, attributes[i].value, sizeof(AudioOut2Pcm));
pcm_data = pcm.data;
has_pcm = true;
const auto& attribute = attributes[i];
const auto id = attribute.attribute_id;
if (id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM && id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN &&
id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS) {
continue;
}
}
if (has_pcm) {
g_audioout2_port_mutex.Lock();
if (auto* state = audioout2_find_port_locked(port); state != nullptr) {
if (pcm_data != nullptr && state->audio_format != AudioInternal::Format::Unknown) {
const auto* bytes = static_cast<const uint8_t*>(pcm_data);
state->pcm_data.assign(bytes, bytes + audioout2_pcm_size(*state));
} else {
state->pcm_data.clear();
if ((seen & (1u << id)) != 0) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
seen |= 1u << id;
if (id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM && state->attributes_locked) {
continue;
}
if (attribute.value == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
switch (id) {
case AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM: {
if (attribute.value_size < sizeof(AudioOut2Pcm)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
AudioOut2Pcm pcm {};
std::memcpy(&pcm, attribute.value, sizeof(pcm));
pcm_data = pcm.data;
break;
}
case AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN: {
const auto channels = audioout2_data_format_channels(state->data_format);
if (attribute.value_size != channels * sizeof(float)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
std::memcpy(gains.data(), attribute.value, attribute.value_size);
for (uint32_t ch = 0; ch < channels; ch++) {
if (!std::isfinite(gains[ch]) || gains[ch] < 0.0f) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
}
break;
}
case AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS:
if (!audioout2_port_type_is_object(state->port_type) ||
attribute.value_size != sizeof(ambisonics)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
std::memcpy(&ambisonics, attribute.value, sizeof(ambisonics));
if (ambisonics != UINT32_MAX && ambisonics > 15 &&
(ambisonics < 64 || ambisonics > 99)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
break;
}
g_audioout2_port_mutex.Unlock();
}
state->gains = gains;
state->ambisonics = ambisonics;
if ((seen & (1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM)) != 0) {
if (pcm_data != nullptr && state->audio_format != AudioInternal::Format::Unknown) {
const auto* bytes = static_cast<const uint8_t*>(pcm_data);
state->pcm_data.assign(bytes, bytes + audioout2_pcm_size(*state));
} else {
state->pcm_data.clear();
}
state->attributes_locked = audioout2_port_type_is_object(state->port_type);
}
return OK;
}
int KYTY_SYSV_ABI AudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) {
PRINT_NAME();
@@ -719,11 +836,11 @@ int KYTY_SYSV_ABI AudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortS
state->reroute_counter = 0;
state->flags = 0;
g_audioout2_port_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* port_state = audioout2_find_port_locked(port); port_state != nullptr) {
state->num_channels = audioout2_data_format_channels(port_state->data_format);
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
// LOGF("\t port = 0x%016" PRIx64 "\n", port);
// LOGF("\t num_channels = %" PRIu8 "\n", state->num_channels);
@@ -743,7 +860,7 @@ int KYTY_SYSV_ABI AudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* han
if (handle == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
Common::LockGuard lock(g_audioout2_port_mutex);
Common::LockGuard lock(g_audioout2_state_mutex);
*handle = g_audioout2_next_user++;
g_audioout2_users.push_back(*handle);
LOGF("\t user_id = %" PRIu32 ", handle = 0x%016" PRIx64 "\n", user_id,
@@ -754,7 +871,7 @@ int KYTY_SYSV_ABI AudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* han
int KYTY_SYSV_ABI AudioOut2UserDestroy(AudioOut2UserHandle handle) {
PRINT_NAME();
LOGF("\t handle = 0x%016" PRIx64 "\n", static_cast<uint64_t>(handle));
Common::LockGuard lock(g_audioout2_port_mutex);
Common::LockGuard lock(g_audioout2_state_mutex);
const auto it = std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle);
if (it == g_audioout2_users.end()) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
@@ -773,13 +890,15 @@ int KYTY_SYSV_ABI AudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle
if (context_attributes == nullptr || port_attributes == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
Common::LockGuard lock(g_audioout2_port_mutex);
Common::LockGuard lock(g_audioout2_state_mutex);
if (std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle) ==
g_audioout2_users.end()) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
*context_attributes = 0;
*port_attributes = 1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM;
*port_attributes = (1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM) |
(1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN) |
(1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS);
return OK;
}
+17 -15
View File
@@ -78,6 +78,8 @@ constexpr int SCE_FONT_TEXT_PARSER_RESULT_TERMINATE = 0;
constexpr int SCE_FONT_TEXT_PARSER_RESULT_ERROR = -1;
constexpr int SCE_FONT_WRITING_FORM_HORIZONTAL = 0x10;
constexpr int FONT_BITMAP_MAX_DIM = 128;
// TrueType glyphs are rasterized at the requested size; the limit only rejects absurd scales.
constexpr int FONT_GLYPH_MAX_DIM = 4096;
constexpr int FONT_ERROR_INVALID_PARAMETER = static_cast<int>(0x80460002u);
constexpr int FONT_ERROR_INVALID_FONT_HANDLE = static_cast<int>(0x80460005u);
constexpr int FONT_ERROR_NO_SUPPORT_CODE = static_cast<int>(0x80460041u);
@@ -206,7 +208,7 @@ struct FontState {
const uint8_t* ttf_data;
uint32_t ttf_size;
bool has_ttf;
std::array<uint8_t, FONT_BITMAP_MAX_DIM * FONT_BITMAP_MAX_DIM> fallback_image;
std::vector<uint8_t> fallback_image;
FontTransImage trans_image;
};
@@ -227,7 +229,7 @@ struct GlyphState {
uint32_t code;
int attribute;
FontGlyphMetrics metrics;
std::array<uint8_t, FONT_BITMAP_MAX_DIM * FONT_BITMAP_MAX_DIM> image;
std::vector<uint8_t> image;
FontTransImage trans_image;
};
@@ -851,8 +853,8 @@ static bool get_stb_metrics(FontState* font, uint32_t code, FontGlyphMetrics* me
return true;
}
static bool init_stb_image(std::array<uint8_t, FONT_BITMAP_MAX_DIM * FONT_BITMAP_MAX_DIM>* image,
FontTransImage* trans_image, uint32_t code, FontState* font) {
static bool init_stb_image(std::vector<uint8_t>* image, FontTransImage* trans_image, uint32_t code,
FontState* font) {
if (image == nullptr || trans_image == nullptr || !ensure_stb_font(font)) {
return false;
}
@@ -868,12 +870,12 @@ static bool init_stb_image(std::array<uint8_t, FONT_BITMAP_MAX_DIM * FONT_BITMAP
stbtt_GetCodepointBitmapBox(&font->font_info, static_cast<int>(code), scale_x, scale_y, &x0, &y0,
&x1, &y1);
image->fill(0);
const auto width = static_cast<uint32_t>(std::clamp(x1 - x0, 0, FONT_BITMAP_MAX_DIM));
const auto height = static_cast<uint32_t>(std::clamp(y1 - y0, 0, FONT_BITMAP_MAX_DIM));
const auto width = static_cast<uint32_t>(std::clamp(x1 - x0, 0, FONT_GLYPH_MAX_DIM));
const auto height = static_cast<uint32_t>(std::clamp(y1 - y0, 0, FONT_GLYPH_MAX_DIM));
// Keep at least one byte so that blank glyphs still get a valid address.
image->assign(std::max<size_t>(static_cast<size_t>(width) * height, 1), 0);
trans_image->address = image->data();
trans_image->width_byte = FONT_BITMAP_MAX_DIM;
trans_image->width_byte = width;
trans_image->image_width = width;
trans_image->image_height = height;
@@ -882,7 +884,7 @@ static bool init_stb_image(std::array<uint8_t, FONT_BITMAP_MAX_DIM * FONT_BITMAP
}
stbtt_MakeCodepointBitmap(&font->font_info, image->data(), static_cast<int>(width),
static_cast<int>(height), FONT_BITMAP_MAX_DIM, scale_x, scale_y,
static_cast<int>(height), static_cast<int>(width), scale_x, scale_y,
static_cast<int>(code));
return true;
@@ -939,8 +941,8 @@ static uint32_t normalize_bitmap_font_code(uint32_t code) {
return static_cast<uint32_t>('?');
}
static void init_image(std::array<uint8_t, FONT_BITMAP_MAX_DIM * FONT_BITMAP_MAX_DIM>* image,
FontTransImage* trans_image, uint32_t code, const FontState* font) {
static void init_image(std::vector<uint8_t>* image, FontTransImage* trans_image, uint32_t code,
const FontState* font) {
EXIT_NOT_IMPLEMENTED(image == nullptr);
EXIT_NOT_IMPLEMENTED(trans_image == nullptr);
@@ -948,11 +950,11 @@ static void init_image(std::array<uint8_t, FONT_BITMAP_MAX_DIM * FONT_BITMAP_MAX
return;
}
image->fill(0);
code = normalize_bitmap_font_code(code);
const auto width = static_cast<uint32_t>(scaled_font_width(font));
const auto height = static_cast<uint32_t>(scaled_font_height(font));
image->assign(static_cast<size_t>(width) * height, 0);
#if LIBAVUTIL_VERSION_MAJOR < 61
const auto* vga_font = avpriv_vga16_font;
#else
@@ -965,13 +967,13 @@ static void init_image(std::array<uint8_t, FONT_BITMAP_MAX_DIM * FONT_BITMAP_MAX
for (uint32_t x = 0; x < width; x++) {
const uint32_t src_x = std::min<uint32_t>((x * 8u) / width, 7u);
if ((row & (0x80u >> src_x)) != 0) {
(*image)[y * FONT_BITMAP_MAX_DIM + x] = 0xff;
(*image)[y * width + x] = 0xff;
}
}
}
trans_image->address = image->data();
trans_image->width_byte = FONT_BITMAP_MAX_DIM;
trans_image->width_byte = width;
trans_image->image_width = width;
trans_image->image_height = height;
}
+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);
+74 -3
View File
@@ -1497,13 +1497,43 @@ namespace LibNpCommerce {
LIB_VERSION("NpCommerce", 1, "NpCommerce", 1, 1);
constexpr int COMMERCE_STATUS_NONE = 0;
constexpr int COMMERCE_STATUS_INITIALIZED = 1;
constexpr int COMMERCE_STATUS_NONE = 0;
constexpr int COMMERCE_STATUS_INITIALIZED = 1;
constexpr int COMMERCE_STATUS_FINISHED = 3;
constexpr int COMMERCE_RESULT_USER_CANCELED = 1;
constexpr int COMMERCE_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u);
constexpr int COMMERCE_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u);
constexpr int COMMERCE_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u);
constexpr int COMMERCE_ERROR_PARAM_INVALID = static_cast<int>(0x80B8000Au);
constexpr int COMMERCE_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du);
static int g_commerce_status = COMMERCE_STATUS_NONE;
struct CommerceDialogParam {
uint8_t base_param[48];
int32_t size;
int32_t user_id;
int32_t mode;
uint32_t service_label;
const char* const* targets;
uint32_t num_targets;
uint32_t padding;
uint64_t features;
void* user_data;
uint8_t reserved[32];
};
struct CommerceDialogResult {
int32_t result;
bool authorized;
void* user_data;
uint8_t reserved[32];
};
static_assert(sizeof(CommerceDialogParam) == 128 && offsetof(CommerceDialogParam, user_data) == 88);
static_assert(sizeof(CommerceDialogResult) == 48 && offsetof(CommerceDialogResult, user_data) == 8);
static int g_commerce_status = COMMERCE_STATUS_NONE;
static CommerceDialogResult g_commerce_result {};
static int KYTY_SYSV_ABI NpCommerceDialogInitialize() {
PRINT_NAME();
@@ -1520,9 +1550,47 @@ static int KYTY_SYSV_ABI NpCommerceDialogTerminate() {
return COMMERCE_ERROR_NOT_INITIALIZED;
}
g_commerce_status = COMMERCE_STATUS_NONE;
g_commerce_result = {};
return OK;
}
static int KYTY_SYSV_ABI NpCommerceDialogOpen(const CommerceDialogParam* param) {
PRINT_NAME();
if (g_commerce_status == COMMERCE_STATUS_NONE) {
return COMMERCE_ERROR_NOT_INITIALIZED;
}
if (param == nullptr) {
return COMMERCE_ERROR_ARG_NULL;
}
g_commerce_result = {};
if (param->size == sizeof(CommerceDialogParam)) {
g_commerce_result.user_data = param->user_data;
}
g_commerce_status = COMMERCE_STATUS_FINISHED;
if (param->size != sizeof(CommerceDialogParam) || param->mode < 0 || param->mode > 5 ||
std::ranges::any_of(param->reserved, [](uint8_t value) { return value != 0; })) {
g_commerce_result.result = COMMERCE_ERROR_PARAM_INVALID;
return COMMERCE_ERROR_PARAM_INVALID;
}
g_commerce_result.result = COMMERCE_RESULT_USER_CANCELED;
return OK;
}
static int KYTY_SYSV_ABI NpCommerceDialogGetResult(CommerceDialogResult* result) {
PRINT_NAME();
if (g_commerce_status == COMMERCE_STATUS_NONE) {
return COMMERCE_ERROR_NOT_INITIALIZED;
}
if (result == nullptr) {
return COMMERCE_ERROR_ARG_NULL;
}
if (g_commerce_status != COMMERCE_STATUS_FINISHED) {
return COMMERCE_ERROR_NOT_FINISHED;
}
*result = g_commerce_result;
return result->result;
}
static int KYTY_SYSV_ABI NpCommerceDialogUpdateStatus() {
PRINT_NAME();
@@ -1532,6 +1600,8 @@ static int KYTY_SYSV_ABI NpCommerceDialogUpdateStatus() {
LIB_DEFINE(InitNet_1_NpCommerce) {
LIB_FUNC("0aR2aWmQal4", NpCommerceDialogInitialize);
LIB_FUNC("m-I92Ab50W8", NpCommerceDialogTerminate);
LIB_FUNC("DfSCDRA3EjY", NpCommerceDialogOpen);
LIB_FUNC("r42bWcQbtZY", NpCommerceDialogGetResult);
LIB_FUNC("LR5cwFMMCVE", NpCommerceDialogUpdateStatus);
}
@@ -1559,6 +1629,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);
+11 -41
View File
@@ -19,27 +19,7 @@ static int KYTY_SYSV_ABI PadSetVibrationMode(int handle, int mode) {
"\t mode = %d\n",
handle, 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;
return Controller::PadCheckHandle(handle);
}
static int KYTY_SYSV_ABI PadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool enabled) {
@@ -86,11 +66,10 @@ static int KYTY_SYSV_ABI
PadDeviceClassGetExtendedInformation(int handle, PadDeviceClassExtendedInformation* info) {
PRINT_NAME();
constexpr int pad_error_invalid_handle = -2137915389; /* 0x80920003 */
constexpr int pad_error_invalid_arg = -2137915391; /* 0x80920001 */
constexpr int pad_error_invalid_arg = -2137915391; /* 0x80920001 */
if (handle != 1) {
return pad_error_invalid_handle;
if (const auto error = Controller::PadCheckHandle(handle); error != OK) {
return error;
}
if (info == nullptr) {
return pad_error_invalid_arg;
@@ -105,16 +84,15 @@ static int KYTY_SYSV_ABI PadDeviceClassParseData(int handle, const Controller::P
PadDeviceClassData* class_data) {
PRINT_NAME();
constexpr int pad_error_invalid_handle = -2137915389; /* 0x80920003 */
constexpr int pad_error_invalid_arg = -2137915391; /* 0x80920001 */
constexpr int pad_error_invalid_arg = -2137915391; /* 0x80920001 */
LOGF("\t handle = %d\n"
"\t data = 0x%016" PRIx64 "\n"
"\t class_data = 0x%016" PRIx64 "\n",
handle, reinterpret_cast<uint64_t>(data), reinterpret_cast<uint64_t>(class_data));
if (handle != 1) {
return pad_error_invalid_handle;
if (const auto error = Controller::PadCheckHandle(handle); error != OK) {
return error;
}
if (data == nullptr || class_data == nullptr) {
return pad_error_invalid_arg;
@@ -142,15 +120,7 @@ static int KYTY_SYSV_ABI PadSetTiltCorrectionState(int handle, bool enabled) {
"\t enabled = %d\n",
handle, enabled ? 1 : 0);
return 0;
}
static int KYTY_SYSV_ABI PadClose(int handle) {
PRINT_NAME();
LOGF("\t handle = %d\n", handle);
return OK;
return Controller::PadCheckHandle(handle);
}
// PPSA02385
@@ -174,7 +144,7 @@ static int KYTY_SYSV_ABI PadGetExtControllerInformationStub(int handle, void* in
"\t info = 0x%016" PRIx64 "\n",
handle, reinterpret_cast<uint64_t>(info));
return OK;
return Controller::PadCheckHandle(handle);
}
static int KYTY_SYSV_ABI PadUnknownN3kSX62fgNo(uint64_t arg0, uint64_t arg1, uint64_t arg2,
@@ -214,10 +184,10 @@ 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);
LIB_FUNC("6ncge5+l5Qs", Controller::PadClose);
LIB_FUNC("n3kSX62fgNo", PadUnknownN3kSX62fgNo);
}
+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
+187 -3
View File
@@ -3,12 +3,15 @@
#include "libs/errno.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <condition_variable>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <mutex>
#include <thread>
#include <vector>
@@ -24,6 +27,7 @@ std::vector<int> g_live_devices;
std::vector<int> g_device_backed_handles;
std::vector<bool> g_output_blocking;
std::vector<std::vector<uint8_t>> g_output_pcm;
std::vector<std::array<float, 2>> g_output_gains;
size_t g_capture_bytes = 0;
int g_next_device = 1;
int g_open_waiters = 0;
@@ -102,8 +106,9 @@ PortParam MakeParam(uint32_t data_format = 0x200) {
return param;
}
AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4) {
AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4, uint32_t max_object_ports = 0) {
ContextParam param {};
param.max_object_ports = max_object_ports;
param.queue_depth = queue_depth;
param.num_grains = 512;
AudioOut2::AudioOut2ContextHandle context = 0;
@@ -112,6 +117,12 @@ AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4) {
return context;
}
AudioOut2::AudioOut2PortHandle CreatePort(AudioOut2::AudioOut2ContextHandle context, const PortParam& param) {
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK, "port create failed");
return port;
}
void TestUserSupportedAttributes() {
constexpr int invalid_param = static_cast<int32_t>(0x80268001u);
constexpr int busy = static_cast<int32_t>(0x80268007u);
@@ -123,8 +134,8 @@ void TestUserSupportedAttributes() {
uint32_t port_attributes = UINT32_MAX;
Check(AudioOut2::AudioOut2UserGetSupportedAttributes(user, &context_attributes,
&port_attributes) == OK &&
context_attributes == 0 && port_attributes == 1,
"user capabilities do not match implemented PCM support");
context_attributes == 0 && port_attributes == 0x103,
"user capabilities do not match implemented attributes");
context_attributes = port_attributes = UINT32_MAX;
Check(AudioOut2::AudioOut2UserGetSupportedAttributes(user, nullptr, &port_attributes) ==
@@ -191,6 +202,13 @@ void SetPcm(AudioOut2::AudioOut2PortHandle port, const void* data) {
"setting PCM failed");
}
template <typename T>
void SetAttribute(AudioOut2::AudioOut2PortHandle port, uint32_t id, const T& value) {
const Attribute attribute {id, 0, &value, sizeof(value)};
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) == OK,
"setting attribute failed");
}
void ResetOutputCalls() {
std::lock_guard lock(g_device_mutex);
g_output_blocking.clear();
@@ -212,6 +230,23 @@ std::vector<std::vector<uint8_t>> OutputPcm() {
return g_output_pcm;
}
std::vector<std::vector<uint8_t>> PushPcm(AudioOut2::AudioOut2ContextHandle context, size_t bytes) {
CaptureOutputPcm(bytes);
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "PCM push failed");
auto output = OutputPcm();
CaptureOutputPcm(0);
return output;
}
void CheckSamples(const std::vector<uint8_t>& pcm, std::initializer_list<float> frame, const char* message) {
Check(!pcm.empty() && pcm.size() % (frame.size() * sizeof(float)) == 0, "incorrect PCM size");
for (size_t i = 0; i < pcm.size() / sizeof(float); i++) {
float sample;
std::memcpy(&sample, pcm.data() + i * sizeof(float), sizeof(float));
Check(std::abs(sample - *(frame.begin() + i % frame.size())) < 0.000001f, message);
}
}
void TestSlotReuse() {
const auto context = CreateContext();
const auto param = MakeParam();
@@ -428,6 +463,148 @@ void TestPcmCopiedBeforeScratchBufferReuse() {
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestPortGainAndValidation() {
const auto context = CreateContext();
const auto port = CreatePort(context, MakeParam());
std::vector<float> pcm(512 * 2, 0.25f);
SetPcm(port, pcm.data());
auto check_output = [&](const std::array<float, 2>& gain, float expected_pcm) {
CheckSamples(PushPcm(context, pcm.size() * sizeof(float)).at(0), {expected_pcm},
"gain modified stored PCM");
std::lock_guard lock(g_device_mutex);
Check(g_output_gains.size() == 1 && g_output_gains[0] == gain,
"incorrect per-channel gain forwarded to backend");
};
check_output({1.0f, 1.0f}, 0.25f);
for (const std::array<float, 2> gain: {std::array {2.0f, 0.5f}, {0.0f, 0.0f}, {1.0f, 1.0f}}) {
SetAttribute(port, 1, gain);
check_output(gain, 0.25f);
check_output(gain, 0.25f);
}
const std::array gain {0.5f, 0.25f};
SetAttribute(port, 1, gain);
std::fill(pcm.begin(), pcm.end(), 0.75f);
SetPcm(port, pcm.data());
check_output(gain, 0.75f);
std::fill(pcm.begin(), pcm.end(), -0.25f);
const Pcm replacement {pcm.data()};
for (const float invalid: {-1.0f, std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::quiet_NaN()}) {
const std::array invalid_gain {1.0f, invalid};
const Attribute attributes[] {{0, 0, &replacement, sizeof(replacement)},
{1, 0, &invalid_gain, sizeof(invalid_gain)}};
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(attributes), 2) ==
static_cast<int32_t>(0x80268001u), "invalid gain was accepted");
check_output(gain, 0.75f);
}
for (const Attribute attribute: {Attribute {1, 0, nullptr, sizeof(gain)},
Attribute {1, 0, &gain, sizeof(float)}}) {
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) ==
static_cast<int32_t>(0x80268001u), "malformed gain was accepted");
}
const Attribute duplicates[] {{0, 0, &replacement, sizeof(replacement)},
{1, 0, &gain, sizeof(gain)}, {1, 0, &gain, sizeof(gain)}};
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(duplicates), 3) ==
static_cast<int32_t>(0x80268001u), "duplicate gain attributes were accepted");
check_output(gain, 0.75f);
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestObjectGainAndAmbisonics() {
constexpr float half_power = 0.70710678f;
struct Coefficients { uint32_t channel; float left; float right; };
constexpr Coefficients cases[] {{UINT32_MAX, half_power, half_power}, {0, 1.0f, 1.0f},
{1, 0.0f, 0.0f}, {2, half_power, -half_power}, {3, 0.0f, 0.0f},
{64, half_power, half_power}, {65, half_power, -half_power},
{66, 0.0f, 0.0f}, {99, 0.0f, 0.0f}};
for (const auto format: {0x100u, 0x101u}) {
const auto context = CreateContext(1, 1);
auto param = MakeParam(format);
param.port_type = 0x100;
const auto object = CreatePort(context, param);
Check(LiveDeviceCount() == 1, "object-only context did not create one output device");
std::vector<float> floating(512, 0.5f);
std::vector<int16_t> integer(512, 16384);
const void* pcm = format == 0x100 ? static_cast<const void*>(floating.data()) : integer.data();
constexpr auto output_bytes = 512 * 2 * sizeof(float);
SetPcm(object, pcm);
std::fill(floating.begin(), floating.end(), 0.0f);
std::fill(integer.begin(), integer.end(), 0);
SetAttribute(object, 1, 0.0f);
SetAttribute(object, 8, 65u);
for (int push = 0; push < 2; push++) {
const auto output = PushPcm(context, output_bytes);
Check(output.size() == 1, "object-only push did not output exactly once");
CheckSamples(output[0], {0.5f * half_power, 0.5f * half_power},
"object PCM ownership, default gain, or attribute freeze failed");
}
std::fill(floating.begin(), floating.end(), 0.5f);
std::fill(integer.begin(), integer.end(), 16384);
for (const auto& test: cases) {
Check(AudioOut2::AudioOut2ContextAdvance(context) == OK, "context advance failed");
const Pcm input {pcm};
const float gain = 0.5f;
const Attribute attributes[] {{0, 0, &input, sizeof(input)}, {1, 0, &gain, sizeof(gain)},
{8, 0, &test.channel, sizeof(test.channel)}};
Check(AudioOut2::AudioOut2PortSetAttributes(object, AsAttribute(attributes), 3) == OK,
"object attributes could not be set atomically with PCM first");
for (int push = 0; push < 2; push++) {
CheckSamples(PushPcm(context, output_bytes).at(0), {0.25f * test.left, 0.25f * test.right},
"object gain or ambisonics decoded incorrectly or accumulated across pushes");
}
}
AudioOut2::AudioOut2ContextAdvance(context);
SetAttribute(object, 8, UINT32_MAX);
SetAttribute(object, 1, 0.0f);
SetPcm(object, pcm);
CheckSamples(PushPcm(context, output_bytes).at(0), {0.0f}, "zero gain did not mute object");
AudioOut2::AudioOut2ContextAdvance(context);
SetPcm(object, pcm);
CheckSamples(PushPcm(context, output_bytes).at(0), {0.0f}, "object gain did not persist");
AudioOut2::AudioOut2ContextDestroy(context);
Check(LiveDeviceCount() == 0, "object context leaked its output device");
}
}
void TestObjectBusRouting() {
const auto context = CreateContext(1, 2);
const auto other_context = CreateContext(1, 1);
AudioOut2::AudioOut2UserHandle other_user = 0;
Check(AudioOut2::AudioOut2UserCreate(2000, &other_user) == OK, "second user create failed");
const auto first = CreatePort(context, MakeParam());
const auto second = CreatePort(context, MakeParam());
std::vector<float> bed(512 * 2, 0.125f);
SetPcm(first, bed.data());
SetPcm(second, bed.data());
auto param = MakeParam(0x100);
param.port_type = 0x100;
std::vector<float> object_pcm(512, 0.25f);
SetPcm(CreatePort(context, param), object_pcm.data());
param.user_handle = other_user;
SetPcm(CreatePort(context, param), object_pcm.data());
std::fill(object_pcm.begin(), object_pcm.end(), 1.0f);
SetPcm(CreatePort(other_context, param), object_pcm.data());
constexpr auto bytes = 512 * 2 * sizeof(float);
for (int push = 0; push < 2; push++) {
const auto output = PushPcm(context, bytes);
Check(output.size() == 3, "object bus was missing or queued more than once");
CheckSamples(output[0], {0.125f}, "objects modified first bed");
CheckSamples(output[1], {0.125f}, "objects modified second bed");
CheckSamples(output[2], {0.35355339f}, "MAIN objects failed to mix across users or leaked contexts");
}
SetPcm(first, nullptr);
SetPcm(second, nullptr);
CheckSamples(PushPcm(context, bytes).at(0), {0.35355339f}, "object output depended on bed PCM");
AudioOut2::AudioOut2ContextDestroy(context);
Check(LiveDeviceCount() == 1, "context destroy closed another context's object device");
CheckSamples(PushPcm(other_context, bytes).at(0), {0.70710678f}, "other context lost its object PCM");
AudioOut2::AudioOut2ContextDestroy(other_context);
Check(AudioOut2::AudioOut2UserDestroy(other_user) == OK, "second user destroy failed");
Check(LiveDeviceCount() == 0, "object bus device leaked");
}
} // namespace
namespace Libs::Audio::AudioInternal {
@@ -467,10 +644,14 @@ bool AudioOutHasDevice(int handle) {
uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking) {
std::lock_guard lock(g_device_mutex);
g_output_blocking.push_back(blocking);
g_output_gains.clear();
if (g_capture_bytes != 0) {
for (uint32_t i = 0; i < num; i++) {
const auto* bytes = static_cast<const uint8_t*>(params[i].data);
g_output_pcm.emplace_back(bytes, bytes + g_capture_bytes);
g_output_gains.push_back(params[i].gains != nullptr
? std::array {params[i].gains[0], params[i].gains[1]}
: std::array {1.0f, 1.0f});
}
}
return 0;
@@ -499,6 +680,9 @@ int main() {
TestAsynchronousDevicePushKeepsQueueBounded();
TestHandleWithoutPcmDoesNotBypassQueue();
TestPcmCopiedBeforeScratchBufferReuse();
TestPortGainAndValidation();
TestObjectGainAndAmbisonics();
TestObjectBusRouting();
Check(AudioOut2::AudioOut2UserDestroy(g_user_handle) == OK, "test user destroy failed");
std::printf("AudioOut2PortTests: all cases passed\n");
return 0;
+74 -3
View File
@@ -7,6 +7,8 @@
#include <array>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <memory>
#include <vector>
@@ -22,6 +24,7 @@ struct Stream {
SDL_AudioSpec spec;
uint64_t frames = 0; // Millionths of a frame retain exact simulated consumption.
uint64_t updated;
std::vector<uint8_t> pcm;
};
uint64_t now = 1000000;
@@ -31,6 +34,7 @@ std::vector<std::unique_ptr<Stream>> streams;
bool fail_open = false, fail_put = false, stalled = false;
bool pad_connected = false, pad_bluetooth = false;
uint64_t pad_queue_us = 0;
const float* queued_pad_gains = nullptr;
int clears = 0;
Stream& GetStream(SDL_AudioStream* stream) {
@@ -61,7 +65,7 @@ SDL_AudioStream* OpenAudioDeviceStream(SDL_AudioDeviceID, const SDL_AudioSpec* s
return nullptr;
}
streams.push_back(std::make_unique<Stream>(Stream {*spec, 0, now}));
streams.push_back(std::make_unique<Stream>(Stream {*spec, 0, now, {}}));
return reinterpret_cast<SDL_AudioStream*>(streams.back().get());
}
@@ -80,12 +84,14 @@ int GetAudioStreamQueued(SDL_AudioStream* stream) {
state.spec.channels);
}
bool PutAudioStreamData(SDL_AudioStream* stream, const void*, int bytes) {
bool PutAudioStreamData(SDL_AudioStream* stream, const void* data, int bytes) {
now += processing;
if (fail_put) {
return false;
}
auto& state = GetStream(stream);
const auto* pcm = static_cast<const uint8_t*>(data);
state.pcm.assign(pcm, pcm + bytes);
Drain(state);
state.frames += static_cast<uint64_t>(bytes) * 1000000 /
(SDL_AUDIO_BYTESIZE(state.spec.format) * state.spec.channels);
@@ -148,7 +154,8 @@ void Close(Stream*) {}
bool UsesBluetooth(const Stream* stream) {
return stream != nullptr && pad_bluetooth;
}
uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*, float) {
uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*, float, const float* gains) {
queued_pad_gains = gains;
return stream != nullptr ? pad_queue_us : 0;
}
} // namespace DualSenseHaptics
@@ -181,6 +188,7 @@ struct Fixture {
fail_open = fail_put = stalled = false;
pad_connected = pad_bluetooth = false;
pad_queue_us = 0;
queued_pad_gains = nullptr;
sleeps.clear();
}
@@ -386,6 +394,68 @@ void TestZeroOutputFrequency() {
"internal output open accepted zero frequency");
Check(f.Open().ToInt() == 1, "rejected output port occupied a handle");
}
template <typename T>
void CheckOutputSamples(std::initializer_list<T> expected, const char* message) {
const auto& pcm = streams.back()->pcm;
Check(pcm.size() == expected.size() * sizeof(T) &&
std::memcmp(pcm.data(), expected.begin(), pcm.size()) == 0, message);
}
void TestChannelGains() {
Fixture f;
const std::array pcm {0.25f, -0.5f, 0.75f, -1.0f};
const auto stereo = f.audio.AudioOutOpen(0, 2, 48000, Audio::Format::FloatStereo);
std::array gains {2.0f, 0.5f};
Audio::OutputParam output {stereo, pcm.data(), gains.data()};
for (int push = 0; push < 2; push++) {
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples({0.5f, -0.25f, 1.5f, -0.5f}, "float channel gains were not applied independently");
}
const int volume[] {16384, 32768};
f.audio.AudioOutSetVolume(stereo, 3, volume);
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples({0.25f, -0.25f, 0.75f, -0.5f}, "channel gain did not combine with port volume");
gains = {0.0f, 0.0f};
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples({0.0f, -0.0f, 0.0f, -0.0f}, "zero channel gains did not mute");
output.gains = nullptr;
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples({0.125f, -0.5f, 0.375f, -1.0f}, "channel scaling changed original float PCM");
const std::array<int16_t, 2> integer_pcm {20000, -20000};
const auto integer = f.audio.AudioOutOpen(0, 1, 48000, Audio::Format::Signed16bitStereo);
output = {integer, integer_pcm.data(), gains.data()};
for (const auto gain: {2.0f, std::numeric_limits<float>::max()}) {
gains = {gain, gain};
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples<int16_t>({32767, -32768}, "integer gain overflowed instead of saturating");
}
gains = {0.5f, 0.25f};
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples<int16_t>({10000, -5000}, "integer channel gains changed original PCM");
const std::array surround_pcm {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f};
for (const auto format: {Audio::Format::Float8Ch, Audio::Format::Float12Ch}) {
const auto surround = f.audio.AudioOutOpen(0, 1, 48000, format);
output = {surround, surround_pcm.data(), surround_pcm.data()};
f.audio.AudioOutOutputs(&output, 1, false);
if (format == Audio::Format::Float8Ch) {
CheckOutputSamples({1.0f, 4.0f, 9.0f, 16.0f, 49.0f, 64.0f, 25.0f, 36.0f},
"channel reordering used destination channel gains");
} else {
CheckOutputSamples({82.0f, 104.0f, 9.0f, 16.0f, 170.0f, 208.0f, 25.0f, 36.0f},
"height downmix lost per-channel gains");
}
}
pad_connected = true;
const auto speaker = f.Open(2, 4);
gains = {0.75f, 0.25f};
output = {speaker, pcm.data(), gains.data()};
f.audio.AudioOutOutputs(&output, 1, false);
Check(queued_pad_gains != nullptr && queued_pad_gains[0] == gains[0] && queued_pad_gains[1] == gains[1],
"connected controller did not receive channel gains");
}
} // namespace
int main() {
@@ -399,6 +469,7 @@ int main() {
TestControllerSpeakerPacing();
TestInvalidBatchSize();
TestZeroOutputFrequency();
TestChannelGains();
Check(streams.empty(), "output stream leaked");
std::puts("AudioOutTimingTests: all cases passed");
}
+208
View File
@@ -110,6 +110,8 @@ struct Controller {
now = 1000;
haptics_handles_rumble = false;
Initialize();
Check(PadInit() == 0 && PadOpen(Config::GetUserId(), 0, 0, nullptr) == 1,
"the standard controller port did not open");
Connect(1);
Check(GetSettingScale(Setting::SpeakerVolume) ==
Config::GetControllerSpeakerVolume() / 50.0f &&
@@ -143,6 +145,54 @@ void SetRumble(uint8_t large, uint8_t small) {
Check(PadSetVibration(1, &param) == 0, "vibration request failed");
}
void TestPadPortOwnership() {
Initialize();
const int user = Config::GetUserId();
constexpr auto not_initialized = static_cast<int32_t>(0x80920005u);
constexpr auto already_opened = static_cast<int32_t>(0x80920004u);
constexpr auto invalid_handle = static_cast<int32_t>(0x80920003u);
constexpr auto no_handle = static_cast<int32_t>(0x80920008u);
PadData data {};
Check(PadOpen(user, 0, 0, nullptr) == not_initialized &&
PadGetHandle(user, 0, 0) == not_initialized &&
PadReadState(1, &data) == not_initialized,
"an uninitialized library exposed a controller port");
Check(PadInit() == 0 && PadGetHandle(user, 0, 0) == no_handle,
"initializing the library opened a controller port");
const int handle = PadOpen(user, 0, 0, nullptr);
Check(handle == 1 && PadInit() == 0 && PadGetHandle(user, 0, 0) == handle,
"reinitializing the library lost the open port");
for (int duplicate = 0; duplicate < 4; duplicate++) {
const int rejected = PadOpen(user, 0, 0, nullptr);
Check(rejected == already_opened,
"opening one pad five times exposed duplicate connected controllers");
Check(PadClose(rejected) == invalid_handle && PadGetHandle(user, 0, 0) == handle,
"closing a rejected duplicate released the original owner");
}
Check(PadGetHandle(user + 1, 0, 0) == no_handle &&
PadGetHandle(user, 2, 0) == no_handle && PadGetHandle(user, 0, 1) == no_handle,
"a different port tuple aliased the open standard pad");
SetButton(HOST_INPUT_CONTROLLER_ID, PAD_BUTTON_CROSS, true);
Check(PadReadState(handle, &data) == 0 && data.connected &&
data.buttons == PAD_BUTTON_CROSS,
"keyboard Cross did not reach the owned port");
SetButton(HOST_INPUT_CONTROLLER_ID, PAD_BUTTON_CROSS, false);
Connect(1);
SetButton(1, PAD_BUTTON_CROSS, true);
Check(PadReadState(handle, &data) == 0 && data.buttons == PAD_BUTTON_CROSS,
"the physical controller did not share the owned port");
Disconnect(1);
Check(PadGetHandle(user, 0, 0) == handle && PadOpen(user, 0, 0, nullptr) == already_opened,
"host disconnection released guest port ownership");
Check(PadClose(handle) == 0 && PadGetHandle(user, 0, 0) == no_handle &&
PadReadState(handle, &data) == invalid_handle &&
PadRead(handle, &data, 1) == invalid_handle && PadClose(handle) == invalid_handle,
"a closed port retained a usable handle");
Check(PadOpen(user, 0, 0, nullptr) == handle && PadReadState(handle, &data) == 0,
"a closed port could not reopen");
Shutdown();
}
void TestSettingCycles() {
Controller controller;
for (auto setting:
@@ -343,16 +393,174 @@ 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() {
Config::Initialize();
TestPadPortOwnership();
TestSettingCycles();
TestGlobalControllerLevels();
// Each following test initializes another controller and requires strong defaults.
TestVibrationLifetime();
TestMaskedTriggersAndValidation();
TestIndependentOutputsAndPadSwitch();
TestTriggerEffectState();
Config::Shutdown();
return 0;
}
+352 -1
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 {
@@ -59,6 +62,10 @@ namespace Libs::LibNpWebApi2 {
void InitNet_1_NpWebApi2(Loader::SymbolDatabase *symbols);
}
namespace Libs::LibNpCommerce {
void InitNet_1_NpCommerce(Loader::SymbolDatabase *symbols);
}
namespace {
namespace FileSystem = Libs::LibKernel::FileSystem;
@@ -121,6 +128,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";
@@ -250,6 +389,91 @@ void TestAioBatches() {
Check(FileSystem::KernelClose(fd) == OK, "close AIO read fixture");
}
void TestNpCommerceDialog() {
Loader::SymbolDatabase symbols;
Libs::LibNpCommerce::InitNet_1_NpCommerce(&symbols);
const auto find = [&](const char *nid) {
const auto *symbol = symbols.FindByNid(nid, Loader::SymbolType::Func);
Check(symbol != nullptr, "NpCommerce dialog exports resolve");
return symbol->vaddr;
};
using Call = int (KYTY_SYSV_ABI *)();
using Open = int (KYTY_SYSV_ABI *)(const void *);
using GetResult = int (KYTY_SYSV_ABI *)(void *);
const auto initialize = reinterpret_cast<Call>(find("0aR2aWmQal4"));
const auto terminate = reinterpret_cast<Call>(find("m-I92Ab50W8"));
const auto update = reinterpret_cast<Call>(find("LR5cwFMMCVE"));
const auto open = reinterpret_cast<Open>(find("DfSCDRA3EjY"));
const auto get_result = reinterpret_cast<GetResult>(find("r42bWcQbtZY"));
struct BaseParam {
uint64_t size;
uint8_t reserved[36];
uint32_t magic;
};
struct Param {
BaseParam base;
int32_t size, user_id, mode;
uint32_t service_label;
const char *const *targets;
uint32_t num_targets, padding;
uint64_t features;
void *user_data;
uint8_t reserved[32];
} param {};
struct Result {
int32_t result;
bool authorized;
void *user_data;
uint8_t reserved[32];
} result {};
static_assert(sizeof(Param) == 128 && offsetof(Param, user_data) == 88);
static_assert(sizeof(Result) == 48 && offsetof(Result, user_data) == 8);
param.base.size = sizeof(BaseParam);
param.base.magic = static_cast<uint32_t>(0xC0D1A109u + reinterpret_cast<uintptr_t>(&param.base));
param.size = sizeof(param);
param.user_id = 1;
param.user_data = &symbols;
Check(open(&param) == static_cast<int>(0x80B80003u) &&
get_result(&result) == static_cast<int>(0x80B80003u),
"NpCommerce dialog calls require initialization");
Check(initialize() == OK && update() == 1 &&
get_result(&result) == static_cast<int>(0x80B80005u),
"NpCommerce dialog result requires completion");
Check(open(nullptr) == static_cast<int>(0x80B8000Du) && update() == 1 &&
get_result(nullptr) == static_cast<int>(0x80B8000Du),
"NpCommerce dialog rejects null arguments");
Check(open(&param) == OK && update() == 3, "NpCommerce dialog finishes without a store");
param.user_data = nullptr;
result.authorized = true;
Check(get_result(&result) == 1 && result.result == 1 && !result.authorized &&
result.user_data == &symbols,
"NpCommerce cancellation returns the original user data");
param.mode = 5;
Check(open(&param) == OK && get_result(&result) == 1 && !result.authorized &&
result.user_data == nullptr,
"NpCommerce dialog can reopen with fresh result data");
const auto invalid_param = [&] {
Check(open(&param) == static_cast<int>(0x80B8000Au) && update() == 3 &&
get_result(&result) == static_cast<int>(0x80B8000Au) &&
result.result == static_cast<int>(0x80B8000Au) && !result.authorized,
"NpCommerce dialog reports invalid parameters in the completion result");
};
param.mode = 99;
invalid_param();
param.mode = -1;
invalid_param();
param.mode = 0;
param.size = 64;
invalid_param();
param.size = sizeof(param);
param.reserved[0] = 1;
invalid_param();
param.reserved[0] = 0;
Check(open(&param) == OK && get_result(&result) == 1 && terminate() == OK && update() == 0 &&
get_result(&result) == static_cast<int>(0x80B80003u),
"NpCommerce dialog recovers from invalid parameters and terminates");
}
void TestNpWebApi2Memory() {
Loader::SymbolDatabase symbols;
Libs::LibNpWebApi2::InitNet_1_NpWebApi2(&symbols);
@@ -669,6 +893,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 +913,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 +988,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;
@@ -857,6 +1140,36 @@ void CheckAprPaths(const std::filesystem::path &root) {
ids[0] == 0xffffffffu && ids[1] == expected_id && sizes[0] == 0 && sizes[1] == 3,
"APR foreach reports a missing path and continues to the valid file");
// These paths share the old 31-bit FNV-1a ID (0x122d1544).
const char *collision_paths[] = {"perf-audit/test_00015ddf.bin",
"perf-audit/test_000389b8.bin"};
Check(std::filesystem::create_directory(root / "perf-audit"), "create APR collision directory");
for (size_t i = 0; i < 2; ++i) {
Check(fixture.Create(root / collision_paths[i]), "create APR collision fixture");
fixture.Write(i == 0 ? "APR" : "OTHER", i == 0 ? 3 : 5);
fixture.Close();
}
uint32_t collision_ids[2] = {};
Check(resolve("/app0/", collision_paths, 2, collision_ids, sizes, &error_index) == OK &&
collision_ids[0] != collision_ids[1] && collision_ids[0] != 0xffffffffu &&
collision_ids[1] != 0xffffffffu && sizes[0] == 3 && sizes[1] == 5,
"APR assigns distinct valid IDs to colliding paths");
const auto *stat_symbol = symbols.FindByNid("ApkYaHb8Sek", Loader::SymbolType::Func);
const auto *size_symbol = symbols.FindByNid("WvEu7yl3Ivg", Loader::SymbolType::Func);
Check(stat_symbol && size_symbol, "APR file metadata exports are registered");
using Stat = int (KYTY_SYSV_ABI *)(uint32_t, FileSystem::FileStat *);
using Size = int (KYTY_SYSV_ABI *)(uint32_t, uint64_t *);
for (size_t i = 0; i < 2; ++i) {
FileSystem::FileStat stat {};
uint64_t size = 0;
Check(resolve("/app0/", &collision_paths[i], 1, ids, nullptr, &error_index) == OK &&
ids[0] == collision_ids[i] &&
reinterpret_cast<Stat>(stat_symbol->vaddr)(ids[0], &stat) == OK &&
reinterpret_cast<Size>(size_symbol->vaddr)(ids[0], &size) == OK &&
stat.st_size == static_cast<int64_t>(sizes[i]) && size == sizes[i],
"APR re-resolution preserves each file's ID, host path and cached size");
}
// PATH_MAX includes NUL; all components remain below NAME_MAX (255).
std::string longest = "/app0/";
for (int i = 0; i < 3; ++i) {
@@ -880,7 +1193,7 @@ void CheckAprPaths(const std::filesystem::path &root) {
Check(resolve(unterminated.data(), paths, 1, ids, sizes, &error_index) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_ENAMETOOLONG,
"APR rejects an unterminated prefix");
CheckAmprOrdering(symbols, expected_id);
CheckAmprOrdering(symbols, collision_ids[0]);
FileSystem::Umount("/app0");
}
@@ -1088,6 +1401,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;
@@ -1368,6 +1713,10 @@ int main(int, char**) {
TempDirectory temporary;
FileSystem::Initialize();
TestSysmoduleReferences();
TestSystemServiceEntitlementEvents();
TestRandomDevices();
TestFileDescriptorFlags();
CheckMountRoot(temporary.Path());
CheckUnmappedPaths(temporary.Path());
CheckArchiveMount(temporary.Path());
@@ -1382,7 +1731,9 @@ int main(int, char**) {
CheckSaveRename(temporary.Path(), "replacement-save");
FileSystem::Shutdown();
CheckSocketWakeup();
CheckEtherAddressFormatting();
TestNpWebApi2Memory();
TestNpCommerceDialog();
graphics.reset();
subsystems.Destroy();
+5
View File
@@ -414,11 +414,16 @@ void TestFormatsAndVolume() {
Check(opened_device == 10 && opened_spec.channels == 4 && opened_spec.format == SDL_AUDIO_F32,
"did not select quad DualSense PCM");
ExpectPcm({0, 0, 0.5f, -0.125f, 0, 0, 1.0f, 0});
const std::array gains {0.25f, 2.0f};
Haptics::Queue(port.get(), 1, stereo.data(), 2, 2, true, volume.data(), 1.0f, gains.data());
ExpectPcm({0, 0, 0.125f, -0.25f, 0, 0, 0.25f, 0});
const std::array<int16_t, 2> mono {16384, -32768};
Queue(port, mono.data(), 2, 1, false, volume.data() + 1);
ExpectPcm({0, 0, 0.25f, 0.25f, 0, 0, -0.5f, -0.5f});
Queue(port, mono.data(), 2, 1, false, volume.data() + 1, 0.5f);
ExpectPcm({0, 0, 0.125f, 0.125f, 0, 0, -0.25f, -0.25f});
Haptics::Queue(port.get(), 1, mono.data(), 2, 1, false, volume.data() + 1, 1.0f, gains.data());
ExpectPcm({0, 0, 0.0625f, 0.0625f, 0, 0, -0.125f, -0.125f});
std::array<float, 24> multichannel {};
multichannel.fill(0.9f);
multichannel[0] = 0.1f;
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -617,9 +617,9 @@ void TestConcurrentBackingReads() {
using namespace Libs::LibKernel::Memory;
const char* test = "ConcurrentBackingReads";
constexpr uint64_t original = 0x123456789abcdef0ull;
for (const auto& readers: {std::pair {TryReadBacking, TryReadBacking},
std::pair {TryReadBacking, TryReadSparseBacking},
std::pair {TryReadSparseBacking, TryReadSparseBacking}}) {
for (const auto& readers: {std::pair {&TryReadBacking, &TryReadBacking},
std::pair {&TryReadBacking, &TryReadSparseBacking},
std::pair {&TryReadSparseBacking, &TryReadSparseBacking}}) {
const auto base = MapNamedFlexible(test, SceKernelPageSize, SceKernelProtCpuRw,
"concurrent_backing_reads");
std::memcpy(reinterpret_cast<void*>(base), &original, sizeof(original));