Compare commits

...
Author SHA1 Message Date
nmzik aa3b2cd57f Implement AGC async rewind imports 2026-10-08 17:43:02 +04:00
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
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 Nevesandnmzik 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
Nawid3333andClaude Opus 5.5 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
nmzikandchenxiao07 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
Nawid3333andClaude Opus 5.5 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
nmzik 45b6d2c597 memory: copy sparse backing in one traversal 2026-10-07 23:17:53 +04:00
nmzik 7e011fa565 memory: allow concurrent backing reads 2026-10-07 23:10:14 +04:00
nmzik 4d10ea17a0 graphics: streamline staging-buffer writes 2026-10-07 22:48:42 +04:00
nmzik 006875ed4f graphics: pause memory-tracking spin loops 2026-10-07 22:47:50 +04:00
nmzik a87889f032 graphics: process upload regions in one traversal 2026-10-07 22:47:50 +04:00
nmzik 9c79d9d298 memory: flatten direct and batch mapping paths 2026-10-07 22:47:50 +04:00
nmzik c30ac7c904 memory: avoid redundant Windows view protection 2026-10-07 22:47:50 +04:00
nmzik 038d2257de tests: enable image aliasing coverage on Linux 2026-10-07 21:35:28 +04:00
nmzik 190f838ea4 shader: preserve workgroup bounds for scalar image descriptor tables 2026-10-07 21:25:53 +04:00
nmzik 470669c677 graphics: make raw image synchronization an opt-in setting 2026-10-07 21:17:35 +04:00
Claxten dd754065d8 graphics: preserve full tiled array backing in image-to-buffer copies 2026-10-07 21:17:35 +04:00
Claxten 743fdc6990 graphics: synchronize raw storage reads with GPU image aliases 2026-10-07 21:17:35 +04:00
Claxten 9f83fd5f0c graphics: apply indirect draw offsets to guest user SGPRs 2026-10-07 21:17:35 +04:00
Claxten 5297abb7ac shader: run non-amplifying NGG programs in mesh workgroups 2026-10-07 21:17:35 +04:00
Claxten a8beac190f shader: evaluate scalar branch decisions across the complete guest wave 2026-10-07 21:17:35 +04:00
Claxten b96871e2bf shader: unpack compressed SNORM16 color exports as normalized signed values 2026-10-07 21:17:35 +04:00
Claxten 21a1346e27 graphics: route compact color exports through CB_SHADER_MASK 2026-10-07 21:17:35 +04:00
Claxten 3ba6ebae81 shader: initialize perspective sample barycentrics at SampleId 2026-10-07 20:44:46 +04:00
nmzik fbf6ae400c graphics: preserve depth array layout during storage reinterpretation 2026-10-07 20:44:46 +04:00
Pipe530 d2413fc2eb trophies: add viewer inspector and wrap toast titles 2026-10-07 17:22:44 +04:00
nmzik 96eb681b90 shader: add V_MIN3_U16 opcode 2026-10-07 16:22:30 +04:00
nmzik 26cce9909a shader: add BUFFER_LOAD_FORMAT_D16_X opcode 2026-10-07 16:22:30 +04:00
nmzik 5af370c257 shader: add BUFFER_STORE_FORMAT_D16_X opcode 2026-10-07 16:22:30 +04:00
nmzik 67d7db685a agc: new abi 2026-10-07 16:22:30 +04:00
nmzik bb98f26433 shader: add missing I64/U64 compare variants 2026-10-07 05:03:09 +04:00
cc44a43ebc systemservice: add an option to report the notice screen as skipped (#1127)
* systemservice: add an option to report the notice screen as skipped

sceSystemServiceGetNoticeScreenSkipFlag always returned false. Return the
new skip_notice_screen option instead. Games that check this flag skip
their boot logo movies when it is set; Marvel's Wolverine, for example,
then does not queue movies/BootSequence/BootSequence.movie.

The option defaults to off, so behaviour is unchanged unless it is
enabled with --skip-notice-screen true or the launcher's "Skip boot
logos" checkbox. The launcher stores it with the other per-game settings,
so it is included in per-game config import and export.

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

* Shorten skip notice screen wording

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-07 04:21:27 +04:00
nmzik 2a18e66a4c gpu: validate depth copy state when drawing instead of decoding 2026-10-07 04:14:49 +04:00
Henrique Reis Nevesandnmzik e7cfe5a2f7 libkernel: implement sceKernelAioWaitRequests (#1109)
kernel: implement batch AIO completion waits

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-07 04:13:06 +04:00
nmzikandnetwarex 7415febd93 SDL title fix
Co-authored-by: netwarex <mail@pety.me>
2026-10-07 03:15:16 +04:00
NeySlimandnmzik 7492cb9577 shader: emulate DS_ORDERED_COUNT as a GDS atomic (#1117)
* feat(shader): emulate DS_ORDERED_COUNT as a GDS atomic

* fix(shader): issue one ordered-count atomic per wave

* shader: correct ordered-count addressing and wave results

Use the M0 base and pixel packer offset for the GDS counter. Broadcast the
previous value to every lane and preserve the counter when EXEC is empty.

Add GPU regressions for add/exchange with full, partial, and empty EXEC in
wave32 and wave64. The targeted DS and buffer atomic suite passes 63 cases.

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-07 01:45:37 +04:00
NeySlim 60e53a39cc decoder: accept clamp on V_MIN3/MAX3/MED3_F16 (#1116)
fix(decoder): accept clamp on V_MIN3/MAX3/MED3_F16
2026-10-07 01:24:18 +04:00
1b21161aac kernel: fix the macOS build of host thread naming (#1122)
* kernel: apply clang-format to pthread.cpp

Output of the repository's pre-commit clang-format hook (v22.1.3) on
this file; no code change.

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

* kernel: fix the macOS build of host thread naming

199d0a9d (#1106) names guest threads on the host with the two-argument
glibc pthread_setname_np(thread, name). KYTY_PLATFORM is
KYTY_PLATFORM_LINUX on macOS too, so the macOS job now fails with "no
matching function for call to 'pthread_setname_np'": Apple's version
takes only the name and renames the calling thread. This code already
runs on the new thread itself (it passes pthread_self()), so use the
one-argument form on Apple with Apple's 63-character limit and keep the
Linux call unchanged.

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

* Remove formatting

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-07 01:18:48 +04:00
NeySlim 1195af2510 decoder: add V_CMPX_U_F32 (#1120)
feat(decoder): add V_CMPX_U_F32
2026-10-07 01:16:23 +04:00
nmzik 9a5658c038 launcher: add per-game config import and export 2026-10-07 01:02:33 +04:00
Oberd 199d0a9d92 kernel: name guest threads on the host (#1106)
PthreadCreate carries a guest thread name, but RunThread never handed it to
the host, so every guest thread showed up as the same generic host thread
name in core dumps, perf samples and htop.

Call pthread_setname_np() on Linux so host-side diagnostics can attribute
work to the guest thread that caused it.
2026-10-06 23:43:36 +04:00
nmzik e679486ed3 net: translate native UDP socket options and microsecond timeouts 2026-10-06 23:41:06 +04:00
Henrique Reis NevesandClaude Opus 5.5 6ef406542c tests: make kyty_tests build and run every test without LTO (#1110)
avplayer_file_tests compiles libs/avPlayer.cpp into the test, and its
PRINT_NAME() calls Loader::Timer::GetTimeMs(). Add loader/timer.cpp to the
target, like the other tests that compile HLE library sources. CI links the
test with LTO, which drops the unused reference, so the failure only shows
in non-LTO builds such as the README's no-Qt instructions.

Clang with libstdc++ rejects optional<IndirectDescriptor>::emplace() with
no arguments in ShaderRecompilerComputeTests.cpp. Assign the descriptor the
same way the neighbouring test does.

trophy_system is registered with ctest but kyty_tests did not build its
executable, so ctest reported it as Not Run.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:56:48 +04:00
Bartłomiej Wołyńczykandnmzik 35c6e8fff6 loader: enable the AMD CPU instruction patcher on macOS (#1014)
* loader: enable AMD CPU instruction patching on macOS

The guest instruction patcher was compiled out on macOS and runtimeLinker forced emulate_amd off,
so under Rosetta every SSE4a EXTRQ/INSERTQ took the SIGILL path (about 18 us each). Build the
patcher on all hosts and let --amd-cpu enable it on macOS like on Windows and Linux. The host
feature probe already reports no SSE4a under Rosetta, so EXTRQ becomes a native trampoline and the
remaining fallbacks keep using the x64 instruction emulator through the host exception handler.

* tests: macos_sse4a covers the guest instruction patcher on macOS

Patch EXTRQ/INSERTQ blobs in an RWX area with a trampoline page behind it, with the host features of
Rosetta (no SSE4a/RDPID/CLWB), and compare against the SIGILL emulator: EXTRQ must become a native
trampoline without SIGILL, INSERTQ stays a trap handled by the emulator, and nothing is touched
without emulate_amd.

* loader: emulate the patcher's VRSQRTPS trap and MWAITX on macOS

The AMD CPU patcher marks VRSQRTPS it cannot relocate by clearing a reserved
VEX.vvvv bit, which raises #UD on every host including Rosetta. The macOS build
left TryEmulateReciprocalSquareRoot and TryEmulateMonitorxMwaitx out, so the
guest died with an unhandled SIGILL at the first such site.

Both are plain SSE2/libc, so enable them on macOS. The Darwin context also
gains ClearUpperYmm so the VEX.128 result zeroes YMM[255:128] when the thread
has the AVX context. macos_sse4a covers the trapped VRSQRTPS and MWAITX.

* tests: macos_sse4a runs a plain VEX.128 VRSQRTPS through the patcher

The existing case started from an instruction that was already marked as a trap. The
new cases feed the ordinary encoding to PatchGuestInstructions and compare the result
with the emulated reference, both through a native trampoline and, when the instruction
cannot be relocated, through the trap mark and the signal emulator.

* loader: compact macOS instruction patcher coverage

Share the emulator dispatch chain while keeping SHA emulation limited to non-Apple hosts and preserving handler order. Remove a duplicate VRSQRTPS signal test already covered by both native and trapped patcher cases, plus blank lines left by removed platform guards.

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-06 21:55:11 +04:00
thenotsoquietninja44-collab 4852721005 net: export connect, getsockopt, sendto, and recvfrom 2026-10-06 21:32:46 +04:00
91 changed files with 4956 additions and 958 deletions

No files matched your search

+13 -11
View File
@@ -419,7 +419,10 @@ add_executable(trophy_system_tests EXCLUDE_FROM_ALL
target_link_libraries(trophy_system_tests common)
target_include_directories(trophy_system_tests PRIVATE ${inc_headers})
add_executable(avplayer_file_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/AvPlayerFileTests.cpp")
add_executable(avplayer_file_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/AvPlayerFileTests.cpp"
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
)
target_link_libraries(avplayer_file_tests common FFmpeg::ffmpeg ZArchive::zarchive)
target_include_directories(avplayer_file_tests PRIVATE ${inc_headers})
@@ -671,15 +674,12 @@ if(BUILD_TESTING)
add_test(NAME color_render_target_1d
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --rt1d-only)
if(WIN32)
# These tests still depend on the Windows multisample-depth path.
add_test(NAME texture_cache_image_overlap
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --image-overlap-only)
add_test(NAME texture_cache_htile_clear
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --htile-clear-only)
add_test(NAME buffer_cache_ranges
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --buffer-cache-range-only)
endif()
add_test(NAME texture_cache_image_overlap
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --image-overlap-only)
add_test(NAME texture_cache_htile_clear
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --htile-clear-only)
add_test(NAME buffer_cache_ranges
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --buffer-cache-range-only)
add_custom_target(kyty_tests DEPENDS
ime_dialog_tests
@@ -689,6 +689,7 @@ if(BUILD_TESTING)
memory_tracker_tests
page_manager_tests
archive_file_tests
trophy_system_tests
avplayer_file_tests
bit_array_tests
lru_cache_tests
@@ -719,8 +720,9 @@ if(APPLE AND (CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64" OR
add_executable(macos_sse4a_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/macosSse4aTests.cpp"
"${KYTY_SOURCE_DIR}/loader/x64InstructionEmulator.cpp"
"${KYTY_SOURCE_DIR}/loader/guestInstructionPatcher.cpp"
)
target_link_libraries(macos_sse4a_tests common fmt::fmt Zydis::Zydis)
target_link_libraries(macos_sse4a_tests common fmt::fmt Zydis::Zydis xbyak::xbyak)
target_include_directories(macos_sse4a_tests PRIVATE ${inc_headers})
if(BUILD_TESTING)
add_test(NAME macos_sse4a COMMAND $<TARGET_FILE:macos_sse4a_tests>)
+8
View File
@@ -152,6 +152,10 @@ bool ReadbackLinearImagesEnabled() {
return g_config->readback_linear_images;
}
bool SyncRawImageBuffersEnabled() {
return g_config->sync_raw_image_buffers;
}
bool TessellationEnabled() {
return g_config->tessellation_enabled;
}
@@ -164,6 +168,10 @@ bool PlayGoHackEnabled() {
return g_config->playgo_hack_enabled;
}
bool SkipNoticeScreen() {
return g_config->skip_notice_screen;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool RedZoneProtectionEnabled() {
return g_config->red_zone_protection_enabled;
+4
View File
@@ -73,9 +73,11 @@ struct ConfigOptions {
bool gpu_assisted_validation_enabled = false;
bool renderdoc_enabled = false;
bool readback_linear_images = false;
bool sync_raw_image_buffers = false;
bool tessellation_enabled = false;
bool trophy_enabled = true;
bool playgo_hack_enabled = false;
bool skip_notice_screen = false;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool red_zone_protection_enabled = false;
#endif
@@ -123,9 +125,11 @@ bool GpuAssistedValidationEnabled();
bool RenderDocEnabled();
bool ReadbackLinearImagesEnabled();
bool SyncRawImageBuffersEnabled();
bool TessellationEnabled();
bool TrophyEnabled();
bool PlayGoHackEnabled();
bool SkipNoticeScreen();
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool RedZoneProtectionEnabled();
#endif
@@ -23,6 +23,12 @@ enum class ContextStateOperation : uint32_t {
PushClear = 3,
};
struct IndirectDrawRegisters {
uint32_t vertex_offset;
uint32_t instance_offset;
uint32_t index_offset;
};
class Pm4Execution {
public:
[[nodiscard]] bool MadeProgress() const noexcept { return m_made_progress; }
@@ -79,10 +85,11 @@ public:
void DrawIndex(DrawIndexArgs args);
void DrawIndexOffset(uint32_t index_offset, uint32_t index_count);
void DrawIndexAuto(DrawAutoArgs args);
void DrawIndirect(uint32_t data_offset, uint32_t draw_initiator, bool indexed);
void DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers,
uint32_t draw_initiator, bool indexed);
void DrawIndirectMulti(uint32_t data_offset, uint32_t max_count_or_count,
const volatile uint32_t* count_addr, uint32_t stride_in_bytes,
uint32_t draw_initiator, bool indexed);
IndirectDrawRegisters registers, uint32_t draw_initiator, bool indexed);
void WriteAtEndOfPipe32(uint32_t cache_policy, uint32_t event_write_dest,
uint32_t eop_event_type, uint32_t cache_action, uint32_t event_index,
uint32_t event_write_source, void* dst_gpu_addr, uint32_t value,
@@ -184,7 +184,6 @@ static HW::RenderControl DecodeRenderControl(uint32_t value) {
KYTY_PM4_GET(value, DB_RENDER_CONTROL, COPY_STENCIL_TO_COLOR) != 0;
r.copy_centroid = KYTY_PM4_GET(value, DB_RENDER_CONTROL, COPY_CENTROID) != 0;
r.copy_sample = KYTY_PM4_GET(value, DB_RENDER_CONTROL, COPY_SAMPLE);
EXIT_NOT_IMPLEMENTED(r.copy_depth_to_color || r.copy_stencil_to_color);
return r;
}
@@ -1642,8 +1641,12 @@ KYTY_CP_OP_PARSER(CpOpDrawIndirect) {
const auto data_offset = buffer[0];
const auto draw_initiator = buffer[3];
const bool indexed = (cmd_id == 0xc0032500);
// The native indexed packet enables the high-half first-index destination in word 3.
const IndirectDrawRegisters registers {
buffer[1] & 0xffffu, buffer[2] & 0xffffu,
indexed && (buffer[2] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP};
cp.DrawIndirect(data_offset, draw_initiator, indexed);
cp.DrawIndirect(data_offset, registers, draw_initiator, indexed);
return 4;
}
@@ -1661,13 +1664,17 @@ KYTY_CP_OP_PARSER(CpOpDrawIndirectMulti) {
const auto stride_in_bytes = buffer[7];
const auto draw_initiator = buffer[8];
const bool indexed = (cmd_id == 0xc0083800);
// Multi-draw moves the first-index enable to word 4; the destinations stay in 2/3.
const IndirectDrawRegisters registers {
buffer[1] & 0xffffu, buffer[2] & 0xffffu,
indexed && (buffer[3] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP};
if (count_indirect == 0) {
count_addr = nullptr;
}
cp.DrawIndirectMulti(data_offset, max_count_or_count, count_addr, stride_in_bytes,
draw_initiator, indexed);
registers, draw_initiator, indexed);
return 9;
}
+25 -4
View File
@@ -875,15 +875,32 @@ void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_cou
DrawIndex({.index_count = index_count, .index_addr = index_addr});
}
void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiator, bool indexed) {
static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters registers,
uint32_t vertex_offset, uint32_t instance_offset,
uint32_t first_index) {
const auto write = [&](uint32_t location, uint32_t value) {
if (location == Pm4::SH_NOP) {
return;
}
EXIT_NOT_IMPLEMENTED(location >= Pm4::SH_NUM ||
g_hw_sh_indirect_func[location] == nullptr);
g_hw_sh_indirect_func[location](cp, location, value);
};
write(registers.vertex_offset, vertex_offset);
write(registers.instance_offset, instance_offset);
write(registers.index_offset, first_index);
}
void CommandProcessor::DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers,
uint32_t draw_initiator, bool indexed) {
const auto args_size = indexed ? sizeof(DrawIndexedIndirectArgs) : sizeof(DrawIndirectArgs);
DrawIndirectMulti(data_offset, 1, nullptr, args_size, draw_initiator, indexed);
DrawIndirectMulti(data_offset, 1, nullptr, args_size, registers, draw_initiator, indexed);
}
void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_count_or_count,
const volatile uint32_t* count_addr,
uint32_t stride_in_bytes, uint32_t draw_initiator,
bool indexed) {
uint32_t stride_in_bytes, IndirectDrawRegisters registers,
uint32_t draw_initiator, bool indexed) {
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != (indexed ? 0u : 2u));
EXIT_NOT_IMPLEMENTED(m_draw_indirect_args_base_addr == 0);
@@ -918,6 +935,8 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
if (!indexed) {
auto* args = reinterpret_cast<const DrawIndirectArgs*>(args_addr);
PatchIndirectDrawOffsets(*this, registers, args->start_vertex_location,
args->start_instance_location, 0);
m_num_instances = args->instance_count;
DrawIndexAuto({.vertex_count = args->vertex_count_per_instance,
.instance_count = args->instance_count,
@@ -928,6 +947,8 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
}
auto* args = reinterpret_cast<const DrawIndexedIndirectArgs*>(args_addr);
PatchIndirectDrawOffsets(*this, registers, args->base_vertex_location,
args->start_instance_location, args->start_index_location);
auto* index_addr = reinterpret_cast<const void*>(
m_index_base_addr + static_cast<uint64_t>(args->start_index_location) * index_size);
+1 -2
View File
@@ -82,9 +82,8 @@ public:
static_assert(std::is_nothrow_invocable_v<RangeFunc&, uint64_t, uint64_t>);
static_assert(std::is_nothrow_invocable_v<UploadFunc&>);
CheckNotInUploadCallback();
Iterate<true>(vaddr, size, [](RegionManager*, uint64_t, uint64_t) {});
const auto* previous_upload_owner = std::exchange(s_upload_owner, this);
Iterate<false>(vaddr, size, [&](RegionManager* manager, uint64_t offset, uint64_t bytes) {
Iterate<true>(vaddr, size, [&](RegionManager* manager, uint64_t offset, uint64_t bytes) {
manager->lock.lock();
manager->ForEachModifiedRange<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset,
bytes, range_func);
+13 -1
View File
@@ -15,6 +15,12 @@
#include <mutex>
#include <vector>
#if defined(_MSC_VER)
#include <intrin.h>
#elif defined(__x86_64__)
#include <xmmintrin.h>
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifndef NOMINMAX
#define NOMINMAX
@@ -62,7 +68,13 @@ class SpinGuard final {
public:
explicit SpinGuard(std::atomic_flag& lock): m_lock(lock) {
while (m_lock.test_and_set(std::memory_order_acquire)) {
std::atomic_signal_fence(std::memory_order_seq_cst);
#if defined(__x86_64__) || defined(_M_X64)
_mm_pause();
#elif defined(_M_ARM64)
__yield();
#elif defined(__aarch64__)
asm volatile("yield");
#endif
}
}
~SpinGuard() { m_lock.clear(std::memory_order_release); }
+16 -2
View File
@@ -9,6 +9,12 @@
#include <mutex>
#include <utility>
#if defined(_MSC_VER)
#include <intrin.h>
#elif defined(__x86_64__)
#include <xmmintrin.h>
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifndef NOMINMAX
#define NOMINMAX
@@ -32,7 +38,13 @@ public:
const auto thread = CurrentThread();
while (m_lock.test_and_set(std::memory_order_acquire)) {
EXIT_NOT_IMPLEMENTED(m_owner.load(std::memory_order_relaxed) == thread);
std::atomic_signal_fence(std::memory_order_seq_cst);
#if defined(__x86_64__) || defined(_M_X64)
_mm_pause();
#elif defined(_M_ARM64)
__yield();
#elif defined(__aarch64__)
asm volatile("yield");
#endif
}
m_owner.store(thread, std::memory_order_relaxed);
}
@@ -48,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;
+29 -2
View File
@@ -34,9 +34,36 @@ void BufferCache::WriteDataBuffer(Buffer& buffer, uint64_t address, const void*
auto* bytes = static_cast<const uint8_t*>(source);
while (size != 0) {
const auto chunk = std::min(size, m_staging_buffer.Size());
// Copy waits for ring reuse and flushes the fresh host data before submission.
const auto offset = m_staging_buffer.Copy(bytes, chunk, 4);
buffer.CopyFrom(m_scheduler.Current(), m_staging_buffer, offset, buffer.Offset(address),
chunk, vk::AccessFlagBits::eHostWrite);
const auto destination_offset = buffer.Offset(address);
EXIT_IF(destination_offset > buffer.Size() || chunk > buffer.Size() - destination_offset);
m_scheduler.EndRendering();
const auto command = m_scheduler.Current().Handle();
vk::BufferMemoryBarrier2 before {};
before.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands;
before.srcAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite;
before.dstStageMask = vk::PipelineStageFlagBits2::eTransfer;
before.dstAccessMask = vk::AccessFlagBits2::eTransferWrite;
before.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
before.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
before.buffer = buffer.Handle();
before.offset = destination_offset;
before.size = chunk;
vk::DependencyInfo dependency {};
dependency.dependencyFlags = vk::DependencyFlagBits::eByRegion;
dependency.bufferMemoryBarrierCount = 1;
dependency.pBufferMemoryBarriers = &before;
command.pipelineBarrier2(dependency);
const vk::BufferCopy copy {offset, destination_offset, chunk};
command.copyBuffer(m_staging_buffer.Handle(), buffer.Handle(), 1, &copy);
auto after = before;
after.srcStageMask = vk::PipelineStageFlagBits2::eTransfer;
after.srcAccessMask = vk::AccessFlagBits2::eTransferWrite;
after.dstStageMask = vk::PipelineStageFlagBits2::eAllCommands;
after.dstAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite;
dependency.pBufferMemoryBarriers = &after;
command.pipelineBarrier2(dependency);
bytes += chunk;
address += chunk;
size -= chunk;
+9 -10
View File
@@ -694,7 +694,9 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
requested.type == cached.info.type && requested.pitch == cached.info.pitch &&
!requested.HasStencil() && !cached.info.HasStencil() && !requested.HasMetadata() &&
!cached.info.HasMetadata();
// PPSA04264
// PPSA04264, PPSA04288
// A partial view retains the entire matching array layout. HTile belongs to
// the depth source, independently of the data slices copied into color storage.
const bool retain_cached_layout =
requested.samples == 1 && cached.info.samples == 1 && cached.backing.samples == 1 &&
requested.bytes_per_block == cached.info.bytes_per_block &&
@@ -713,7 +715,8 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
requested.data.size / requested.resources.layers ==
cached.info.data.size / cached.info.resources.layers &&
!requested.HasStencil() && !cached.info.HasStencil() && !requested.HasMetadata() &&
!cached.info.HasMetadata();
(cached.info.metadata.kind == ImageMetadataKind::None ||
cached.info.metadata.kind == ImageMetadataKind::Htile);
bool recreate = cached.info.resources < requested.resources ||
requested.IsVolume() != cached.info.IsVolume();
switch (binding) {
@@ -738,8 +741,6 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
info.data = cached.info.data;
info.resources = cached.info.resources;
info.mip_layout = cached.info.mip_layout;
} else {
info.resources = std::max(requested.resources, cached.info.resources);
}
info.htile_clear_mask = 0;
const auto replacement_id = InsertImage(info);
@@ -1791,14 +1792,12 @@ bool BufferCache::SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uin
const auto available = buffer.Size() - buf_offset;
uint32_t levels = 0;
uint64_t copy_size = 0;
if (image.info.IsVolume()) {
// Volume mips contain strided block slices, so a mip's linear span cannot prove that
// every retained slice fits. Keep volume synchronization whole-image only.
if (!buffer.IsInBounds(image.info.data.address, image.info.data.size)) {
return false;
}
if (buffer.IsInBounds(image.info.data.address, image.info.data.size)) {
levels = image.info.resources.levels;
copy_size = image.info.data.size;
} else if (image.info.IsVolume() || image.info.resources.layers > 1) {
// Array and volume slices are strided across the full mip chain.
return false;
} else {
for (; levels < image.info.resources.levels; ++levels) {
const auto& mip = image.info.mip_layout[levels];
@@ -3,6 +3,7 @@
#include "common/alignment.h"
#include "common/assert.h"
#include "common/common.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "common/profiler.h"
@@ -126,8 +127,9 @@ NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource
if (size > graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange) {
EXIT("storage buffer range is unsupported\n");
}
auto [buffer, offset] = context.GetBufferCache().ObtainBuffer(address, size, resource.written,
resource.formatted, id);
auto [buffer, offset] = context.GetBufferCache().ObtainBuffer(
address, size, resource.written,
resource.formatted || (resource.read && Config::SyncRawImageBuffersEnabled()), id);
const auto aligned_offset = Common::AlignDown(offset, alignment);
const auto adjustment = offset - aligned_offset;
const auto max_range = graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange;
+15 -7
View File
@@ -589,8 +589,12 @@ static uint32_t DrawColorOutputMask(const HW::Context& ctx) {
const auto& sh_regs = ctx.GetShaderRegisters();
const auto write_mask = ctx.GetRenderTargetMask() & sh_regs.m_cbShaderMask;
uint32_t output_mask = 0;
for (uint32_t slot = 0; slot < RENDER_COLOR_ATTACHMENTS_MAX; slot++) {
if (sh_regs.target_output_mode[slot] != 0 &&
for (uint32_t index = 0; index < RENDER_COLOR_ATTACHMENTS_MAX; index++) {
const auto slot = ShaderPixelExportTarget(sh_regs.m_cbShaderMask, index);
if (slot >= RENDER_COLOR_ATTACHMENTS_MAX) {
break;
}
if (sh_regs.target_output_mode[index] != 0 &&
render_target_mask_slot(write_mask, slot) != 0) {
output_mask |= 1u << slot;
}
@@ -902,7 +906,7 @@ bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCal
if (state.ps_active) {
for (const auto& output: state.ps_input_info.stage.program->info.outputs) {
if (output.kind == ShaderRecompiler::IR::StageOutputKind::Mrt) {
mrt_mask |= 1u << output.index;
mrt_mask |= 1u << output.location;
}
}
}
@@ -1280,8 +1284,10 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
: ResolveDrawOffsets(ucfg.GetIndexOffset(), state.vertex_info[0]);
DrawEmitInfo emit {};
emit.vertex_offset = vertex_offset + args.base_vertex;
emit.first_instance = 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);
@@ -1369,8 +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 {};
emit.first_vertex = static_cast<uint32_t>(vertex_offset + static_cast<int32_t>(args.first_vertex));
emit.first_instance = instance_offset;
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 = native_indirect ? 0 : instance_offset;
DrawIndexBufferSource index_source {};
ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source, false);
+64 -18
View File
@@ -195,6 +195,37 @@ std::mutex g_trophy_notification_mutex;
std::deque<TrophyNotification> g_trophy_notifications;
std::atomic<bool> g_trophy_notification_active {false};
std::vector<std::string_view> WrapTrophyTitle(std::string_view text, float font_size, float width) {
std::vector<std::string_view> lines;
auto* const font = ImGui::GetFont();
for (;;) {
const auto newline = text.find('\n');
const auto paragraph = text.substr(0, newline);
auto line = paragraph.substr(0, 0);
for (auto start = paragraph.find_first_not_of(" \t\r"); start != std::string_view::npos;) {
const auto end = std::min(paragraph.find_first_of(" \t\r", start), paragraph.size());
const auto word = paragraph.substr(start, end - start);
const std::string_view candidate(line.empty() ? word.begin() : line.begin(),
word.end());
// Keep an oversized word intact; the toast's clip rectangle handles overflow.
if (!line.empty() && font->CalcTextSizeA(font_size, std::numeric_limits<float>::max(),
0.0f, candidate.data(),
candidate.data() + candidate.size()).x > width) {
lines.push_back(line);
line = word;
} else {
line = candidate;
}
start = paragraph.find_first_not_of(" \t\r", end);
}
lines.push_back(line);
if (newline == std::string_view::npos) {
return lines;
}
text.remove_prefix(newline + 1);
}
}
void ClearInputEvents() {
std::scoped_lock lock(g_input_mutex);
g_input_events.clear();
@@ -1101,7 +1132,26 @@ struct SystemOverlay::Impl {
0.65f, 1.25f);
const float width = std::max(
0.0f, std::min(520.0f * screen_scale, static_cast<float>(frame_extent.width) - 32.0f));
const float height = 104.0f * screen_scale;
constexpr std::array GRADE_COLORS {
IM_COL32(161, 225, 245, 255), IM_COL32(255, 207, 82, 255), IM_COL32(211, 220, 232, 255),
IM_COL32(211, 142, 91, 255)};
const int grade = trophy_notification->grade;
const bool has_grade = grade > 0 && grade <= static_cast<int>(GRADE_COLORS.size());
const float pad = 10.0f * screen_scale;
const float base_height = 104.0f * screen_scale;
const float icon_size = base_height - 2.0f * pad;
const float text_offset = trophy_notification_image != nullptr
? pad + icon_size + 18.0f * screen_scale
: 20.0f * screen_scale;
const float title_offset = text_offset + (has_grade ? 30.0f * screen_scale : 0.0f);
const float title_size = 26.0f * screen_scale;
const float line_height = 30.0f * screen_scale;
const float text_padding = 16.0f * screen_scale;
const auto title_lines = WrapTrophyTitle(trophy_notification->name, title_size,
width - title_offset - text_padding);
const float block_height =
static_cast<float>(title_lines.size()) * line_height + 28.0f * screen_scale;
const float height = std::max(base_height, block_height + 2.0f * text_padding);
const float rounding = 14.0f * screen_scale;
const float fade_in = std::clamp(age / 0.22f, 0.0f, 1.0f);
const float fade_out = std::clamp((TOAST_LIFETIME - age) / 0.4f, 0.0f, 1.0f);
@@ -1121,26 +1171,19 @@ struct SystemOverlay::Impl {
draw->AddRect(top_left, bottom_right, fade(255, 255, 255, 28), rounding,
ImDrawFlags_RoundCornersAll, 1.0f);
const float pad = 10.0f * screen_scale;
const float icon_size = height - 2.0f * pad;
float text_left = top_left.x + 20.0f * screen_scale;
const float text_left = top_left.x + text_offset;
if (trophy_notification_image != nullptr) {
const ImVec2 icon_min {top_left.x + pad, top_left.y + pad};
const ImVec2 icon_min {top_left.x + pad, top_left.y + (height - icon_size) * 0.5f};
const ImVec2 icon_max {icon_min.x + icon_size, icon_min.y + icon_size};
draw->AddImageRounded(trophy_notification_image->GetTexRef(), icon_min, icon_max,
{0, 0}, {1, 1}, fade(255, 255, 255, 255), 8.0f * screen_scale);
draw->AddRect(icon_min, icon_max, fade(255, 255, 255, 40), 8.0f * screen_scale,
ImDrawFlags_RoundCornersAll, 1.0f);
text_left = icon_max.x + 18.0f * screen_scale;
}
constexpr std::array GRADE_COLORS {IM_COL32(161, 225, 245, 255), IM_COL32(255, 207, 82, 255),
IM_COL32(211, 220, 232, 255),
IM_COL32(211, 142, 91, 255)};
const int grade = trophy_notification->grade;
const float title_y = top_left.y + height * 0.5f - 25.0f * screen_scale;
float title_x = text_left;
if (grade > 0 && grade <= static_cast<int>(GRADE_COLORS.size())) {
const float title_y = top_left.y + (height - block_height) * 0.5f;
const float title_x = top_left.x + title_offset;
if (has_grade) {
// Small cup glyph tinted with the trophy grade.
const ImU32 cup = ImGui::GetColorU32(GRADE_COLORS[grade - 1], alpha);
const float u = screen_scale;
@@ -1155,12 +1198,15 @@ struct SystemOverlay::Impl {
draw->AddRectFilled({cx - 1.5f * u, cy + 2.0f * u}, {cx + 1.5f * u, cy + 7.0f * u}, cup);
draw->AddRectFilled({cx - 5.0f * u, cy + 7.0f * u}, {cx + 5.0f * u, cy + 10.0f * u}, cup,
1.5f * u);
title_x += 30.0f * u;
}
draw->PushClipRect(top_left, {bottom_right.x - 16.0f * screen_scale, bottom_right.y}, true);
draw->AddText(nullptr, 26.0f * screen_scale, {title_x, title_y},
fade(255, 255, 255, 255), trophy_notification->name.c_str());
draw->AddText(nullptr, 20.0f * screen_scale, {text_left, title_y + 38.0f * screen_scale},
draw->PushClipRect(top_left, {bottom_right.x - text_padding, bottom_right.y}, true);
float line_y = title_y;
for (const auto line : title_lines) {
draw->AddText(nullptr, title_size, {title_x, line_y}, fade(255, 255, 255, 255),
line.data(), line.data() + line.size());
line_y += line_height;
}
draw->AddText(nullptr, 20.0f * screen_scale, {text_left, line_y + 8.0f * screen_scale},
fade(176, 180, 192, 255), "Trophy earned!");
draw->PopClipRect();
}
@@ -454,7 +454,7 @@ void HostInputToggleMouseToJoystick() {
LOGF("Mouse to right stick: enabled (F7 to release)\n");
}
bool HostInputWaitEvent(SDL_Event* event) {
bool HostInputWaitEvent(SDL_Event* event, int max_wait_ms) {
int timeout = -1;
if (!g_mouse.enabled || SDL_GetKeyboardFocus() != g_mouse_window) {
g_mouse.next_poll = 0;
@@ -473,6 +473,7 @@ bool HostInputWaitEvent(SDL_Event* event) {
now_ms < g_cursor_hide_at ? static_cast<int>(g_cursor_hide_at - now_ms) : 0;
timeout = timeout < 0 ? cursor_timeout : std::min(timeout, cursor_timeout);
}
timeout = timeout < 0 ? max_wait_ms : std::min(timeout, max_wait_ms);
const bool has_event = SDL_WaitEventTimeout(event, timeout);
if (Config::HideCursorEnabled()) {
+1 -1
View File
@@ -13,7 +13,7 @@ void HostInputShutdown();
void HostInputKey(int key_code, bool down);
void HostInputMouseButton(uint8_t mouse_button, bool down);
void HostInputToggleMouseToJoystick();
[[nodiscard]] bool HostInputWaitEvent(SDL_Event* event);
[[nodiscard]] bool HostInputWaitEvent(SDL_Event* event, int max_wait_ms);
} // namespace Libs::Graphics
@@ -998,7 +998,7 @@ void Presenter::Impl::Present() {
}
presented_overlay_revision.store(overlay_visual.revision, std::memory_order_release);
window.UpdateTitle();
window.loop.presented_frames.fetch_add(1, std::memory_order_relaxed);
return;
}
LOGF("Vulkan presentation retry exhausted; dropping frame\n");
+20 -27
View File
@@ -722,6 +722,10 @@ void WindowContext::Run() {
loop.need_exit = false;
loop.paused.store(false, std::memory_order_release);
constexpr uint64_t title_interval_ms = 1000;
auto title_time = SDL_GetTicks();
auto title_frames = loop.presented_frames.load(std::memory_order_relaxed);
while (!loop.need_exit) {
if (loop.paused.load(std::memory_order_acquire)) {
if (!timer.IsPaused()) {
@@ -731,7 +735,20 @@ void WindowContext::Run() {
timer.Resume();
}
if (!HostInputWaitEvent(&loop.event)) {
// Refresh the title on the main thread without making presentation wait for it.
const auto now = SDL_GetTicks();
const auto elapsed = now - title_time;
if (elapsed >= title_interval_ms) {
const auto frames = loop.presented_frames.load(std::memory_order_relaxed);
if (frames != 0) {
UpdateTitle(frames, static_cast<double>(frames - title_frames) * 1000.0 /
static_cast<double>(elapsed));
}
title_time = now;
title_frames = frames;
}
const auto wait_ms = static_cast<int>(title_interval_ms - (now - title_time));
if (!HostInputWaitEvent(&loop.event, wait_ms)) {
continue;
}
ProcessEvent(timer.GetTimeS());
@@ -913,7 +930,7 @@ void WindowContext::UpdateIcon() {
}
}
void WindowContext::UpdateTitle() {
void WindowContext::UpdateTitle(uint64_t frame_num, double current_fps) {
static char title[128];
static char title_id[12];
static char app_ver[12];
@@ -923,10 +940,6 @@ void WindowContext::UpdateTitle() {
static bool has_app_ver =
Loader::SystemContentParamSfoGetString("APP_VER", app_ver, sizeof(app_ver));
static const std::string processor_name = Common::GetSystemInfo().ProcessorName;
static uint64_t fps_start = Common::Timer::QueryPerformanceCounter();
static uint64_t frame_num = 0;
static uint64_t fps_frames = 0;
static double current_fps = 0.0;
#if KYTY_BUILD == KYTY_BUILD_DEBUG
static constexpr auto build_type = "Debug";
@@ -936,17 +949,6 @@ void WindowContext::UpdateTitle() {
static constexpr auto build_type = "Unknown";
#endif
const auto now = Common::Timer::QueryPerformanceCounter();
const auto frequency = Common::Timer::QueryPerformanceFrequency();
frame_num++;
fps_frames++;
if (now - fps_start >= frequency) {
current_fps = static_cast<double>(fps_frames) * static_cast<double>(frequency) /
static_cast<double>(now - fps_start);
fps_start = now;
fps_frames = 0;
}
const auto* device_name = graphic_ctx.GetPhysicalDeviceProperties().deviceName.data();
auto text = fmt::format(
"[{} | {}] {}{}{}{}{}{}[{}] [{}], frame: {}, fps: {:.0f}", KYTY_BUILD_LABEL, build_type,
@@ -954,16 +956,7 @@ void WindowContext::UpdateTitle() {
(has_title_id ? ", " : ""), (has_app_ver ? app_ver : ""), (has_app_ver ? " " : ""),
device_name, processor_name, frame_num, current_fps);
struct TitleUpdate {
SDL_Window* window;
std::string* text;
} update {window, &text};
EXIT_IF(!SDL_RunOnMainThread(
[](void* data) {
auto& title = *static_cast<TitleUpdate*>(data);
SDL_SetWindowTitle(title.window, title.text->c_str());
},
&update, true));
SDL_SetWindowTitle(window, text.c_str());
}
} // namespace Libs::Graphics
@@ -27,6 +27,7 @@ struct WindowLoopState {
SDL_Event event {};
bool need_exit = false;
std::atomic_bool paused = false;
std::atomic<uint64_t> presented_frames {0};
};
struct WindowContext {
@@ -42,7 +43,7 @@ struct WindowContext {
void RecreateSurface();
void RefreshSurfaceCapabilities();
void UpdateIcon();
void UpdateTitle();
void UpdateTitle(uint64_t frame_num, double current_fps);
void Resize(int width, int height);
void ProcessWindowEvent(const SDL_WindowEvent& event);
void ProcessDisplayEvent(const SDL_DisplayEvent& event);
@@ -280,6 +280,10 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
switch (inst.GetOpcode()) {
case IR::ValueOpcode::StoreCompletion: requirements.subgroup_barrier = true; break;
case IR::ValueOpcode::BvhIntersect: requirements.bvh = true; break;
case IR::ValueOpcode::ConditionRef:
requirements.subgroup_ballot |=
inst.Flags<CFG::BranchCondition>() != CFG::BranchCondition::ScalarInstruction;
break;
case IR::ValueOpcode::Ballot: requirements.subgroup_ballot = true; break;
case IR::ValueOpcode::DppMoveU32:
case IR::ValueOpcode::ReadFirstLane:
@@ -42,8 +42,9 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
EmitBinaryU32(state, spv::OpIMul, group, ConstantU32(state, size)));
}
const bool centroid = kind == IR::StageInputKind::BaryCoordSmoothCentroid;
const bool sample = kind == IR::StageInputKind::BaryCoordSmoothSample;
const auto variable = InputVariableForKind(
state, centroid ? IR::StageInputKind::BaryCoordSmooth : kind);
state, centroid || sample ? IR::StageInputKind::BaryCoordSmooth : kind);
if (variable == 0) {
return ConstantU32(state, 0);
}
@@ -76,15 +77,24 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, value);
return bits;
}
if (centroid || kind == IR::StageInputKind::BaryCoordSmooth ||
if (centroid || sample || kind == IR::StageInputKind::BaryCoordSmooth ||
kind == IR::StageInputKind::BaryCoordNoPerspective) {
const auto value = state.builder.AllocateId();
const auto bits = state.builder.AllocateId();
if (centroid) {
if (centroid || sample) {
const auto coordinates = state.builder.AllocateId();
state.builder.RequireCapability(spv::CapabilityInterpolationFunction);
state.builder.AddFunction(spv::OpExtInst, TypeF32Vector(state, 3), coordinates,
GlslStd450(state), GLSLstd450InterpolateAtCentroid, variable);
if (sample) {
const auto sample_id = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, TypeI32(state), sample_id,
InputVariableForKind(state, IR::StageInputKind::SampleId));
state.builder.AddFunction(spv::OpExtInst, TypeF32Vector(state, 3), coordinates,
GlslStd450(state), GLSLstd450InterpolateAtSample,
variable, sample_id);
} else {
state.builder.AddFunction(spv::OpExtInst, TypeF32Vector(state, 3), coordinates,
GlslStd450(state), GLSLstd450InterpolateAtCentroid, variable);
}
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), value,
coordinates, component + 1u);
} else {
@@ -252,8 +262,12 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
bool uint_output) {
auto& state = ctx.state;
if (exp.compr && !uint_output) {
const auto unpack =
MrtOutputMode(state, exp) == 5u ? GLSLstd450UnpackUnorm2x16 : GLSLstd450UnpackHalf2x16;
auto unpack = GLSLstd450UnpackHalf2x16;
switch (MrtOutputMode(state, exp)) {
case 5: unpack = GLSLstd450UnpackUnorm2x16; break;
case 6: unpack = GLSLstd450UnpackSnorm2x16; break;
default: break;
}
uint32_t f32[4] = {ConstantF32(state, 0), ConstantF32(state, 0), ConstantF32(state, 0),
ConstantF32(state, 0x3f800000u)};
for (uint32_t pair = 0; pair < 2u; pair++) {
@@ -699,26 +713,18 @@ uint32_t EmitDppUpdateU32(ValueEmitContext& ctx, const IR::Inst& inst) {
}
uint32_t EmitConditionRef(ValueEmitContext& ctx, const IR::Inst& inst) {
if (ctx.other_half == nullptr) return ctx.Arg(inst, 0);
// A native scalar branch makes one decision for both emulated wave halves.
if (ctx.half != 0) return ctx.other_half->Def(IR::Value(&inst));
// Scalar branches include lanes disabled by EXEC, which the branch body may restore.
if (ctx.other_half != nullptr && ctx.half != 0) return ctx.other_half->Def(IR::Value(&inst));
const auto kind = inst.Flags<CFG::BranchCondition>();
if (kind == CFG::BranchCondition::ScalarInstruction) return ctx.Arg(inst, 0);
const auto ballot = ctx.Ballot(inst.Arg(0));
const auto low = ctx.state.builder.AllocateId();
const auto high = ctx.state.builder.AllocateId();
const auto combined = ctx.state.builder.AllocateId();
const auto result = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), low, ballot, 0);
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), high, ballot, 1);
const bool zero = kind == CFG::BranchCondition::ExecZero ||
kind == CFG::BranchCondition::VccZero || kind == CFG::BranchCondition::SccZero;
ctx.state.builder.AddFunction(zero ? spv::OpBitwiseAnd : spv::OpBitwiseOr,
TypeU32(ctx.state), combined, low, high);
ctx.state.builder.AddFunction(zero ? spv::OpIEqual : spv::OpINotEqual,
TypeBool(ctx.state), result, combined,
ConstantU32(ctx.state, zero ? ~0u : 0u));
return result;
const auto ballot = ctx.Ballot(inst.Arg(0));
const auto reference = zero ? ctx.Ballot(IR::Value(true))
: ConstantU32CompositeZero(ctx.state, 4);
const auto compare = Binary(ctx.state, zero ? spv::OpIEqual : spv::OpINotEqual,
TypeBoolVector(ctx.state, 4), ballot, reference);
return Unary(ctx.state, zero ? spv::OpAll : spv::OpAny, TypeBool(ctx.state), compare);
}
uint32_t EmitBallot(ValueEmitContext& ctx, IR::Value predicate) {
@@ -716,18 +716,38 @@ uint32_t FormattedLoad(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Me
return LoadWordPrepared(ctx, inst, RebaseRawComponent(mem, 0u), resource);
}
const auto plan = PrepareFormattedMemory(ctx, inst, mem, resource, info);
return EmitValueOrDefaultIfCondition(
const auto value = EmitValueOrDefaultIfCondition(
ctx.state, plan.in_bounds, TypeU32(ctx.state),
FormattedOutOfBoundsValue(ctx, mem, plan, 1u),
[&]() { return LoadFormattedInBounds(ctx, mem, plan, 0u); });
if (mem.data_bits != 16u) {
return value;
}
if (info.type == Format::ComponentType::Uint) {
return EmitUMin32(ctx.state, value, ConstantU32(ctx.state, 0xffffu));
}
if (info.type == Format::ComponentType::Sint) {
const auto lower = EmitSMax32(ctx.state, value, ConstantU32(ctx.state, 0xffff8000u));
return EmitSMin32(ctx.state, lower, ConstantU32(ctx.state, 0x7fffu));
}
return EmitConvertF16F32(ctx.state, EmitBitCastF32U32(ctx.state, value));
});
}
uint32_t EncodeFormattedStoreComponent(ValueEmitContext& ctx,
const Format::BufferFormatInfo& info,
uint32_t component, uint32_t data) {
uint32_t component, uint32_t data, uint32_t data_bits) {
auto& state = ctx.state;
const auto bits = info.component_bits[component];
if (data_bits == 16u) {
if (info.type == Format::ComponentType::Uint) {
data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
} else if (info.type == Format::ComponentType::Sint) {
data = EmitBitFieldSExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
} else {
data = EmitBitCastU32F32(state, EmitF16BitsToF32(state, data));
}
}
const bool normalized = info.type == Format::ComponentType::Unorm ||
info.type == Format::ComponentType::Snorm;
const bool scaled = info.type == Format::ComponentType::Uscaled ||
@@ -779,7 +799,7 @@ void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst,
value, data, component);
}
}
value = EncodeFormattedStoreComponent(ctx, info, component, value);
value = EncodeFormattedStoreComponent(ctx, info, component, value, mem.data_bits);
const auto bits = info.component_bits[component];
if (info.packed_bitfield) {
packed = EmitBitFieldInsert(
@@ -63,6 +63,8 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
{0x50u, Opcode::BUFFER_ATOMIC_SWAP_X2, 2, 32},
{0x59u, Opcode::BUFFER_ATOMIC_AND_X2, 2, 32},
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
{0x80u, Opcode::BUFFER_LOAD_FORMAT_D16_X, 1, 16, false, false, true},
{0x84u, Opcode::BUFFER_STORE_FORMAT_D16_X, 1, 16, false, false, true},
};
constexpr MemoryOpcodeInfo MTBUF_OPCODE_LIST[] = {
@@ -109,6 +111,7 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
{0x39u, Opcode::DS_READ_I8, 1, 8, true}, {0x3au, Opcode::DS_READ_U8, 1, 8},
{0x3bu, Opcode::DS_READ_I16, 1, 16, true}, {0x3cu, Opcode::DS_READ_U16, 1, 16},
{0x3du, Opcode::DS_CONSUME, 1, 32}, {0x3eu, Opcode::DS_APPEND, 1, 32},
{0x3fu, Opcode::DS_ORDERED_COUNT, 1, 32},
{0x40u, Opcode::DS_ADD_U64, 2, 32}, {0x4au, Opcode::DS_OR_B64, 2, 32},
{0x4du, Opcode::DS_WRITE_B64, 2, 32}, {0x4eu, Opcode::DS_WRITE2_B64, 4, 32},
{0x4fu, Opcode::DS_WRITE2ST64_B64, 4, 32}, {0x76u, Opcode::DS_READ_B64, 2, 32},
@@ -214,6 +217,7 @@ uint32_t DsSourceCount(Opcode opcode) {
case Opcode::DS_READ_ADDTID_B32:
case Opcode::DS_CONSUME:
case Opcode::DS_APPEND: return 0u;
case Opcode::DS_ORDERED_COUNT: return 1u;
default: return IsDsWriteOpcode(opcode) || IsDsAtomicOpcode(opcode) ? 2u : 1u;
}
}
@@ -294,6 +298,9 @@ void DecodeMubuf(uint32_t pc, std::span<const uint32_t> code, uint32_t word_inde
}
DecodeVectorGpr(vdata, inst.dst);
if (inst.opcode == Opcode::BUFFER_LOAD_FORMAT_D16_X) {
inst.dst.sdwa_sel = 4u;
}
DecodeVectorGpr(vaddr, inst.src0);
DecodeScalarSource(srsrc * 4u, pc, inst.src1);
DecodeScalarSource(soffset, pc, inst.src2);
@@ -412,6 +419,14 @@ void DecodeDs(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
if (inst.opcode == Opcode::UNSUPPORTED) {
SetUnsupported(inst, Family::DS, opcode, "DS opcode is not implemented");
}
if (inst.opcode == Opcode::DS_ORDERED_COUNT) {
// Keep OFFSET1's wave type as well as its add/swap selector for GDS addressing.
inst.offset = offset0 & 0xfcu;
inst.secondary_offset = offset1;
if (!inst.gds || ((offset1 >> 4u) & 3u) > 1u || ((offset1 >> 6u) & 3u) != 0u) {
SetUnsupported(inst, Family::DS, opcode, "DS ordered count variant is not implemented");
}
}
if (inst.opcode == Opcode::DS_SWIZZLE_B32 && inst.offset >= 0xe000u) {
SetUnsupported(inst, Family::DS, opcode, "DS swizzle FFT mode is not implemented");
}
@@ -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:
@@ -618,10 +619,12 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::BUFFER_LOAD_FORMAT_XY:
case Opcode::BUFFER_LOAD_FORMAT_XYZ:
case Opcode::BUFFER_LOAD_FORMAT_XYZW:
case Opcode::BUFFER_LOAD_FORMAT_D16_X:
case Opcode::BUFFER_STORE_FORMAT_X:
case Opcode::BUFFER_STORE_FORMAT_XY:
case Opcode::BUFFER_STORE_FORMAT_XYZ:
case Opcode::BUFFER_STORE_FORMAT_XYZW:
case Opcode::BUFFER_STORE_FORMAT_D16_X:
case Opcode::BUFFER_LOAD_UBYTE:
case Opcode::BUFFER_LOAD_USHORT:
case Opcode::BUFFER_LOAD_DWORD:
@@ -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,
@@ -269,6 +270,7 @@ enum class Opcode {
V_MIN3_I32,
V_MIN3_U32,
V_MIN3_F16,
V_MIN3_U16,
V_MAX3_F32,
V_MAX3_I32,
V_MAX3_U32,
@@ -382,6 +384,7 @@ enum class Opcode {
V_CMPX_LG_F32,
V_CMPX_GE_F32,
V_CMPX_O_F32,
V_CMPX_U_F32,
V_CMPX_NGE_F32,
V_CMPX_NLG_F32,
V_CMPX_NGT_F32,
@@ -398,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,
@@ -454,19 +458,34 @@ enum class Opcode {
V_CMP_NE_U32,
V_CMP_GE_U32,
V_CMP_T_U32,
V_CMP_F_I64,
V_CMP_EQ_I64,
V_CMP_LT_I64,
V_CMP_LE_I64,
V_CMP_NE_I64,
V_CMP_T_I64,
V_CMP_F_U64,
V_CMP_LT_U64,
V_CMP_EQ_U64,
V_CMP_LE_U64,
V_CMP_GT_U64,
V_CMP_NE_U64,
V_CMP_GE_U64,
V_CMP_T_U64,
V_CMPX_F_I64,
V_CMPX_LT_I64,
V_CMPX_EQ_I64,
V_CMPX_LE_I64,
V_CMPX_NE_I64,
V_CMPX_T_I64,
V_CMPX_F_U64,
V_CMPX_LT_U64,
V_CMPX_EQ_U64,
V_CMPX_LE_U64,
V_CMPX_GT_U64,
V_CMPX_NE_U64,
V_CMPX_GE_U64,
V_CMPX_T_U64,
V_CMPX_LT_U32,
V_CMPX_EQ_U32,
V_CMPX_LE_U32,
@@ -489,10 +508,12 @@ enum class Opcode {
BUFFER_LOAD_FORMAT_XY,
BUFFER_LOAD_FORMAT_XYZ,
BUFFER_LOAD_FORMAT_XYZW,
BUFFER_LOAD_FORMAT_D16_X,
BUFFER_STORE_FORMAT_X,
BUFFER_STORE_FORMAT_XY,
BUFFER_STORE_FORMAT_XYZ,
BUFFER_STORE_FORMAT_XYZW,
BUFFER_STORE_FORMAT_D16_X,
BUFFER_LOAD_UBYTE,
BUFFER_LOAD_SBYTE,
BUFFER_LOAD_USHORT,
@@ -579,6 +600,7 @@ enum class Opcode {
DS_BPERMUTE_B32,
DS_CONSUME,
DS_APPEND,
DS_ORDERED_COUNT,
DS_READ_I8,
DS_READ_U8,
DS_READ_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},
@@ -232,7 +232,7 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0x11u, Opcode::V_CMPX_LT_F32}, {0x12u, Opcode::V_CMPX_EQ_F32},
{0x13u, Opcode::V_CMPX_LE_F32}, {0x14u, Opcode::V_CMPX_GT_F32},
{0x15u, Opcode::V_CMPX_LG_F32}, {0x16u, Opcode::V_CMPX_GE_F32},
{0x17u, Opcode::V_CMPX_O_F32},
{0x17u, Opcode::V_CMPX_O_F32}, {0x18u, Opcode::V_CMPX_U_F32},
{0x19u, Opcode::V_CMPX_NGE_F32}, {0x1au, Opcode::V_CMPX_NLG_F32},
{0x1bu, Opcode::V_CMPX_NGT_F32}, {0x1cu, Opcode::V_CMPX_NLE_F32},
{0x1du, Opcode::V_CMPX_NEQ_F32}, {0x1eu, Opcode::V_CMPX_NLT_F32},
@@ -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},
@@ -265,10 +266,18 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0xc7u, Opcode::V_CMP_T_U32}, {0xa2u, Opcode::V_CMP_EQ_I64, false},
{0xa1u, Opcode::V_CMP_LT_I64, false}, {0xa3u, Opcode::V_CMP_LE_I64, false},
{0xa5u, Opcode::V_CMP_NE_I64, false},
{0xa0u, Opcode::V_CMP_F_I64, false}, {0xa7u, Opcode::V_CMP_T_I64, false},
{0xb0u, Opcode::V_CMPX_F_I64, false}, {0xb1u, Opcode::V_CMPX_LT_I64, false},
{0xb2u, Opcode::V_CMPX_EQ_I64, false}, {0xb3u, Opcode::V_CMPX_LE_I64, false},
{0xb7u, Opcode::V_CMPX_T_I64, false},
{0xb5u, Opcode::V_CMPX_NE_I64, false}, {0xd1u, Opcode::V_CMPX_LT_U32},
{0xd2u, Opcode::V_CMPX_EQ_U32}, {0xd3u, Opcode::V_CMPX_LE_U32},
{0xd4u, Opcode::V_CMPX_GT_U32}, {0xd5u, Opcode::V_CMPX_NE_U32},
{0xd6u, Opcode::V_CMPX_GE_U32}, {0xe1u, Opcode::V_CMP_LT_U64, false},
{0xe0u, Opcode::V_CMP_F_U64, false}, {0xe7u, Opcode::V_CMP_T_U64, false},
{0xf0u, Opcode::V_CMPX_F_U64, false}, {0xf1u, Opcode::V_CMPX_LT_U64, false},
{0xf2u, Opcode::V_CMPX_EQ_U64, false}, {0xf4u, Opcode::V_CMPX_GT_U64, false},
{0xf6u, Opcode::V_CMPX_GE_U64, false}, {0xf7u, Opcode::V_CMPX_T_U64, false},
{0xe2u, Opcode::V_CMP_EQ_U64, false}, {0xe3u, Opcode::V_CMP_LE_U64, false},
{0xe4u, Opcode::V_CMP_GT_U64, false}, {0xe5u, Opcode::V_CMP_NE_U64, false},
{0xe6u, Opcode::V_CMP_GE_U64, false}, {0xf3u, Opcode::V_CMPX_LE_U64, false},
@@ -310,6 +319,7 @@ constexpr OpcodeMap VOP3_OPCODE_LIST[] = {
{0x152u, Opcode::V_MIN3_I32},
{0x153u, Opcode::V_MIN3_U32},
{0x351u, Opcode::V_MIN3_F16},
{0x353u, Opcode::V_MIN3_U16},
{0x154u, Opcode::V_MAX3_F32},
{0x155u, Opcode::V_MAX3_I32},
{0x156u, Opcode::V_MAX3_U32},
@@ -460,8 +470,9 @@ bool IsNativeVop3F16TernaryOpcode(Opcode opcode) {
opcode == Opcode::V_MED3_F16 || opcode == Opcode::V_FMA_F16;
}
bool IsNativeVop3I16TernaryOpcode(Opcode opcode) {
return opcode == Opcode::V_MED3_I16 || opcode == Opcode::V_MAD_I16;
bool IsNativeVop3B16TernaryOpcode(Opcode opcode) {
return opcode == Opcode::V_MIN3_U16 || opcode == Opcode::V_MED3_I16 ||
opcode == Opcode::V_MAD_I16;
}
bool IsNativeVop3B16BinaryOpcode(Opcode opcode) {
@@ -568,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},
@@ -892,6 +903,7 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
case Opcode::V_CMPX_LG_F32:
case Opcode::V_CMPX_GE_F32:
case Opcode::V_CMPX_O_F32:
case Opcode::V_CMPX_U_F32:
case Opcode::V_CMPX_NGE_F32:
case Opcode::V_CMPX_NLG_F32:
case Opcode::V_CMPX_NGT_F32:
@@ -918,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;
@@ -1366,7 +1379,7 @@ void ApplyNativeVop3TernaryModifiers(Instruction& inst, uint32_t op_sel, uint32_
inst.dst.sdwa_sel = ((op_sel & 0x8u) != 0) ? 5u : 4u;
}
void ApplyNativeVop3I16TernarySelectors(Instruction& inst, uint32_t op_sel) {
void ApplyNativeVop3B16TernarySelectors(Instruction& inst, uint32_t op_sel) {
Operand* sources[] = {&inst.src0, &inst.src1, &inst.src2};
for (uint32_t i = 0; i < 3u; i++) {
sources[i]->op_sel = ((op_sel >> i) & 1u) != 0;
@@ -1449,6 +1462,9 @@ bool SupportsNativeVop3ResultModifiers(Opcode opcode) {
case Opcode::V_MAD_F32:
case Opcode::V_FMA_F32:
case Opcode::V_FMA_F16:
case Opcode::V_MIN3_F16:
case Opcode::V_MAX3_F16:
case Opcode::V_MED3_F16:
case Opcode::V_CUBEID_F32:
case Opcode::V_CVT_PKRTZ_F16_F32:
case Opcode::V_LDEXP_F32:
@@ -1479,9 +1495,10 @@ bool HasUnsupportedNativeVop3Modifiers(Opcode opcode, bool permlane, bool mad_mi
return clamp != 0u || omod != 0u;
}
if (IsNativeVop3F16TernaryOpcode(opcode)) {
return opcode != Opcode::V_FMA_F16 && (clamp != 0u || omod != 0u);
// Clamp is applied by WriteF16 for every f16 ternary; omod only for V_FMA_F16.
return opcode != Opcode::V_FMA_F16 && omod != 0u;
}
if (IsNativeVop3I16TernaryOpcode(opcode)) {
if (IsNativeVop3B16TernaryOpcode(opcode)) {
return abs != 0u || (clamp != 0u && !clamp_modifier) || omod != 0u || neg != 0u;
}
if (IsNativeVop3B16BinaryOpcode(opcode)) {
@@ -1529,6 +1546,7 @@ bool IsVopcCompareExec(Opcode opcode) {
case Opcode::V_CMPX_LG_F32:
case Opcode::V_CMPX_GE_F32:
case Opcode::V_CMPX_O_F32:
case Opcode::V_CMPX_U_F32:
case Opcode::V_CMPX_NGE_F32:
case Opcode::V_CMPX_NLG_F32:
case Opcode::V_CMPX_NGT_F32:
@@ -1549,9 +1567,20 @@ bool IsVopcCompareExec(Opcode opcode) {
case Opcode::V_CMPX_GT_U32:
case Opcode::V_CMPX_NE_U32:
case Opcode::V_CMPX_GE_U32:
case Opcode::V_CMPX_F_I64:
case Opcode::V_CMPX_LT_I64:
case Opcode::V_CMPX_EQ_I64:
case Opcode::V_CMPX_LE_I64:
case Opcode::V_CMPX_NE_I64:
case Opcode::V_CMPX_T_I64:
case Opcode::V_CMPX_F_U64:
case Opcode::V_CMPX_LT_U64:
case Opcode::V_CMPX_EQ_U64:
case Opcode::V_CMPX_LE_U64:
case Opcode::V_CMPX_GT_U64:
case Opcode::V_CMPX_NE_U64:
case Opcode::V_CMPX_GE_U64:
case Opcode::V_CMPX_T_U64:
case Opcode::V_CMPX_LT_I16:
case Opcode::V_CMPX_EQ_I16:
case Opcode::V_CMPX_LE_I16:
@@ -1706,7 +1735,7 @@ void DecodeVop3(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
const bool vop3b_uses_sdst = carry_in_out || vop3b_carry_out || vop3b_mad_u64;
const bool mad_mix = false;
const bool f16_ternary = IsNativeVop3F16TernaryOpcode(inst.opcode);
const bool i16_ternary = IsNativeVop3I16TernaryOpcode(inst.opcode);
const bool b16_ternary = IsNativeVop3B16TernaryOpcode(inst.opcode);
const bool b16_binary = IsNativeVop3B16BinaryOpcode(inst.opcode);
const bool pack_b32_f16 = inst.opcode == Opcode::V_PACK_B32_F16;
const bool permlane = IsPermlaneOpcode(inst.opcode);
@@ -1817,8 +1846,8 @@ void DecodeVop3(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
ApplyNativeVop3TernaryModifiers(inst, op_sel, abs, neg);
} else if (f16_ternary) {
ApplyNativeVop3TernaryModifiers(inst, op_sel, abs, neg);
} else if (i16_ternary) {
ApplyNativeVop3I16TernarySelectors(inst, op_sel);
} else if (b16_ternary) {
ApplyNativeVop3B16TernarySelectors(inst, op_sel);
} else if (b16_binary) {
ApplyNativeVop3B16BinaryModifiers(inst, op_sel);
} else if (pack_b32_f16) {
@@ -31,6 +31,7 @@ void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueO
const bool signed_64 = opcode == IR::ValueOpcode::SLessThan64 ||
opcode == IR::ValueOpcode::SLessThanEqual64 ||
inst.opcode == Decoder::Opcode::V_CMP_EQ_I64 ||
inst.opcode == Decoder::Opcode::V_CMPX_EQ_I64 ||
inst.opcode == Decoder::Opcode::V_CMP_NE_I64 ||
inst.opcode == Decoder::Opcode::V_CMPX_NE_I64;
const auto read = [&](const Decoder::Operand& operand) {
@@ -31,11 +31,12 @@ void Translator::V_MAD_I16(const Decoder::Instruction& inst) {
Write16Bits(DestinationOperand(inst), ir.BitwiseAnd(result, IR::U32(IR::Value(0xffffu))));
}
void Translator::V_MED3_I16(const Decoder::Instruction& inst) {
void Translator::Integer16Ternary(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool sign) {
const auto result = IR::U32(ir.Emit(
IR::ValueOpcode::SMedTri32, {ReadU16AsU32(inst.src0, true), ReadU16AsU32(inst.src1, true),
ReadU16AsU32(inst.src2, true)}));
Write16Bits(DestinationOperand(inst), ir.BitwiseAnd(result, IR::U32(IR::Value(0xffffu))));
opcode, {ReadU16AsU32(inst.src0, sign), ReadU16AsU32(inst.src1, sign),
ReadU16AsU32(inst.src2, sign)}));
Write16Bits(DestinationOperand(inst), result);
}
void Translator::PackedInteger16Shift(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
@@ -187,7 +187,7 @@ Decoder::Operand MemorySourceAt(const Decoder::Instruction& decoded, uint32_t in
if (decoded.family == Decoder::Family::MUBUF || decoded.family == Decoder::Family::MTBUF) {
const bool store_or_atomic =
(decoded.opcode >= Decoder::Opcode::BUFFER_STORE_FORMAT_X &&
decoded.opcode <= Decoder::Opcode::BUFFER_STORE_FORMAT_XYZW) ||
decoded.opcode <= Decoder::Opcode::BUFFER_STORE_FORMAT_D16_X) ||
(decoded.opcode >= Decoder::Opcode::BUFFER_STORE_BYTE &&
decoded.opcode <= Decoder::Opcode::BUFFER_STORE_DWORDX4) ||
(decoded.opcode >= Decoder::Opcode::TBUFFER_STORE_FORMAT_X &&
@@ -427,7 +427,7 @@ void Translator::S_LOAD(const Decoder::Instruction& inst, bool raw) {
void Translator::BUFFER_LOAD(const Decoder::Instruction& inst) {
const auto memory = MemoryInfoFromDecoded(inst);
IR::ValueOpcode opcode;
const auto bits = memory.data_bits;
const auto bits = memory.formatted ? 32u : memory.data_bits;
const auto sign = memory.data_signed;
switch (bits) {
case 8u: opcode = IR::ValueOpcode::LoadBufferU8; break;
@@ -473,7 +473,8 @@ void Translator::BUFFER_STORE(const Decoder::Instruction& inst) {
const auto data = ReadU32(data_src);
IR::ValueOpcode opcode;
IR::Value value;
switch (memory.data_bits) {
// Formatted stores carry packed VGPR bits; the format determines their conversion.
switch (memory.formatted ? 32u : memory.data_bits) {
case 8u:
opcode = IR::ValueOpcode::StoreBufferU8;
value = NarrowSubdword(data, 8u);
@@ -883,6 +884,50 @@ void Translator::DS_SWIZZLE_B32(const Decoder::Instruction& inst) {
ReadU32(MemorySourceAt(inst, 1)), ir.GetExec()}));
}
void Translator::DS_ORDERED_COUNT(const Decoder::Instruction& inst) {
// Approximate ordered counting with one GDS atomic per wave. Wave-launch ordering and
// release/done synchronization are not emulated.
const auto memory = MemoryInfoFromDecoded(inst);
const auto m0 = ir.GetM0();
auto address = ir.BitwiseAnd(ir.ShiftRightLogical(m0, IR::U32(IR::Value(16u))),
IR::U32(IR::Value(0xfffcu)));
if (((inst.secondary_offset >> 2u) & 3u) == 1u) {
const auto packer = ir.BitwiseAnd(m0, IR::U32(IR::Value(0xfu)));
address = ir.IAdd(address, ir.ShiftLeftLogical(packer, IR::U32(IR::Value(2u))));
}
const auto exec = ir.GetExec();
const auto ballot = ir.Emit(IR::ValueOpcode::Ballot, {exec});
const auto low = ir.CompositeExtract(ballot, 0u);
const auto high = ir.CompositeExtract(ballot, 1u);
const auto active = ir.INotEqual(ir.BitwiseOr(low, high), IR::U32(IR::Value(0u)));
const auto first = ir.Select(
ir.INotEqual(low, IR::U32(IR::Value(0u))),
IR::U32(ir.Emit(IR::ValueOpcode::FindILsb32, {low})),
ir.IAdd(IR::U32(IR::Value(32u)), IR::U32(ir.Emit(IR::ValueOpcode::FindILsb32, {high}))));
// An empty EXEC still returns the counter. Elect lane zero and perform an add of zero.
const auto selected = ir.Select(active, first, IR::U32(IR::Value(0u)));
const auto lane = IR::U32(ir.Emit(IR::ValueOpcode::LaneId));
const auto is_first = ir.IEqual(lane, selected);
const auto source = IR::U32(ir.Emit(IR::ValueOpcode::ReadFirstLane,
{ReadU32(inst.src0), is_first}));
const auto value = ir.Select(active, source, IR::U32(IR::Value(0u)));
const auto flags = AddMemoryInfo(memory, inst.pc);
IR::U32 result;
if (((inst.secondary_offset >> 4u) & 3u) == 1u) {
const auto swapped = IR::U32(ir.Emit(IR::ValueOpcode::SharedAtomicSwap32,
{address, value, ir.LogicalAnd(is_first, active)}, flags));
const auto unchanged = IR::U32(ir.Emit(IR::ValueOpcode::SharedAtomicIAdd32,
{address, IR::Value(0u), ir.LogicalAnd(is_first, ir.LogicalNot(active))}, flags));
result = ir.Select(active, swapped, unchanged);
} else {
result = IR::U32(ir.Emit(IR::ValueOpcode::SharedAtomicIAdd32,
{address, value, is_first}, flags));
}
// DS_ORDERED_COUNT writes every destination lane, regardless of EXEC.
ir.SetVectorReg(static_cast<IR::VectorReg>(inst.dst.reg),
IR::U32(ir.Emit(IR::ValueOpcode::ReadFirstLane, {result, is_first})));
}
void Translator::DS_PERMUTE(const Decoder::Instruction& inst, bool backward) {
const auto address = ir.IAdd(ReadU32(inst.src0), IR::U32(IR::Value(inst.offset)));
WriteOperand(inst.dst, ir.Emit(backward ? IR::ValueOpcode::BpermuteU32
@@ -936,6 +981,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::BUFFER_LOAD_FORMAT_XY:
case Decoder::Opcode::BUFFER_LOAD_FORMAT_XYZ:
case Decoder::Opcode::BUFFER_LOAD_FORMAT_XYZW:
case Decoder::Opcode::BUFFER_LOAD_FORMAT_D16_X:
case Decoder::Opcode::TBUFFER_LOAD_FORMAT_X:
case Decoder::Opcode::TBUFFER_LOAD_FORMAT_XY:
case Decoder::Opcode::TBUFFER_LOAD_FORMAT_XYZ:
@@ -951,6 +997,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::BUFFER_STORE_FORMAT_XY:
case Decoder::Opcode::BUFFER_STORE_FORMAT_XYZ:
case Decoder::Opcode::BUFFER_STORE_FORMAT_XYZW:
case Decoder::Opcode::BUFFER_STORE_FORMAT_D16_X:
case Decoder::Opcode::TBUFFER_STORE_FORMAT_X:
case Decoder::Opcode::TBUFFER_STORE_FORMAT_XY:
case Decoder::Opcode::TBUFFER_STORE_FORMAT_XYZ:
@@ -1114,6 +1161,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::DS_SWIZZLE_B32: return DS_SWIZZLE_B32(inst);
case Decoder::Opcode::DS_PERMUTE_B32: return DS_PERMUTE(inst, false);
case Decoder::Opcode::DS_BPERMUTE_B32: return DS_PERMUTE(inst, true);
case Decoder::Opcode::DS_ORDERED_COUNT: return DS_ORDERED_COUNT(inst);
case Decoder::Opcode::DS_CONSUME:
return DS_APPEND_CONSUME(inst, IR::ValueOpcode::DataConsume);
case Decoder::Opcode::DS_APPEND:
@@ -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);
@@ -889,9 +889,16 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
case Decoder::Opcode::V_CMP_GT_U64:
case Decoder::Opcode::V_CMP_NE_U64:
case Decoder::Opcode::V_CMP_GE_U64:
case Decoder::Opcode::V_CMPX_LT_I64:
case Decoder::Opcode::V_CMPX_EQ_I64:
case Decoder::Opcode::V_CMPX_LE_I64:
case Decoder::Opcode::V_CMPX_NE_I64:
case Decoder::Opcode::V_CMPX_LT_U64:
case Decoder::Opcode::V_CMPX_EQ_U64:
case Decoder::Opcode::V_CMPX_LE_U64:
case Decoder::Opcode::V_CMPX_GT_U64:
case Decoder::Opcode::V_CMPX_NE_U64:
case Decoder::Opcode::V_CMPX_GE_U64:
include_vector(inst.src0, 2u);
include_vector(inst.src1, 2u);
break;
@@ -1130,7 +1137,8 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
}
} else if (options.stage == ShaderType::Mesh) {
const auto& mesh = options.input_info.vertex->mesh;
EXIT_NOT_IMPLEMENTED(mesh.primitives_per_group == 0u || mesh.vertices_per_group > 64u ||
EXIT_NOT_IMPLEMENTED(mesh.primitives_per_group == 0u || mesh.vertices_per_group > 0x1ffu ||
mesh.primitives_per_group > 0x1ffu ||
total_threads > 15u * options.wave_size);
const auto u32 = [](uint32_t value) { return IR::U32(IR::Value(value)); };
const auto draw = [&](uint32_t index) {
@@ -1166,6 +1174,10 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
const auto wave_info = entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(wave, u32(24)),
u32(((total_threads + options.wave_size - 1u) /
options.wave_size) << 28u));
entry_ir.SetScalarReg(
static_cast<IR::ScalarReg>(2),
entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(vertices, u32(12)),
entry_ir.ShiftLeftLogical(primitives, u32(22))));
entry_ir.SetScalarReg(
static_cast<IR::ScalarReg>(3),
entry_ir.BitwiseOr(wave_info, entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(
@@ -1274,6 +1286,8 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
}
};
barycentric_pair(ps->ps_perspective_center_vgpr, IR::StageInputKind::BaryCoordSmooth);
barycentric_pair(ps->ps_perspective_sample_vgpr,
IR::StageInputKind::BaryCoordSmoothSample);
barycentric_pair(ps->ps_perspective_centroid_vgpr,
IR::StageInputKind::BaryCoordSmoothCentroid);
uint32_t reg = ps->ps_system_input_base;
@@ -109,6 +109,7 @@ private:
void DS_ADDTID(const Decoder::Instruction& inst, bool write);
void DS_SWIZZLE_B32(const Decoder::Instruction& inst);
void DS_PERMUTE(const Decoder::Instruction& inst, bool backward);
void DS_ORDERED_COUNT(const Decoder::Instruction& inst);
IR::F32 SelectF32(IR::U1 condition, IR::F32 true_value, IR::F32 false_value);
IR::U32 ConvertF32ToU32Saturated(IR::F32 value, float upper_bound, float safe_upper,
@@ -165,8 +166,8 @@ private:
void FloatCube(const Decoder::Instruction& inst, uint32_t result_kind);
void Integer16Shift(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool arithmetic);
void Integer16Binary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool sign);
void Integer16Ternary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool sign);
void V_MAD_I16(const Decoder::Instruction& inst);
void V_MED3_I16(const Decoder::Instruction& inst);
void PackedInteger16Shift(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool arithmetic);
void PackedInteger16Binary(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
@@ -96,16 +96,34 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMPX_LE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false, true);
return;
case O::V_CMP_F_I64:
case O::V_CMP_F_U64: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_T_I64:
case O::V_CMP_T_U64: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMPX_F_I64:
case O::V_CMPX_F_U64: EmitCompareConstant(inst, false, false, true); return;
case O::V_CMPX_T_I64:
case O::V_CMPX_T_U64: EmitCompareConstant(inst, true, false, true); return;
case O::V_CMP_EQ_I64:
case O::V_CMP_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_EQ_I64:
case O::V_CMPX_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, true);
return;
case O::V_CMP_LT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false, true);
return;
case O::V_CMP_GT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_GT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false, true);
return;
case O::V_CMP_NE_I64:
case O::V_CMP_NE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false, false);
@@ -118,9 +136,15 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMP_LT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true);
return;
case O::V_CMP_LE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true);
return;
case O::V_CMP_LE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, false);
return;
@@ -130,6 +154,9 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMP_GE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_GE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, true);
return;
case O::V_CMP_EQ_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false, false);
@@ -344,8 +371,10 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMP_O_F32: EmitFloatOrderedCompare(inst, true, false); return;
case O::V_CMP_U_F32: EmitFloatOrderedCompare(inst, false, false); return;
case O::V_CMPX_O_F32: EmitFloatOrderedCompare(inst, true, true); return;
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;
@@ -394,7 +423,8 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_SUB_NC_I16: return Integer16Binary(inst, IR::ValueOpcode::ISub32, false);
case O::V_MUL_LO_U16: return Integer16Binary(inst, IR::ValueOpcode::IMul32, false);
case O::V_MAD_I16: return V_MAD_I16(inst);
case O::V_MED3_I16: return V_MED3_I16(inst);
case O::V_MED3_I16: return Integer16Ternary(inst, IR::ValueOpcode::SMedTri32, true);
case O::V_MIN3_U16: return Integer16Ternary(inst, IR::ValueOpcode::UMinTri32, false);
case O::V_MIN_I16: return Integer16Binary(inst, IR::ValueOpcode::SMin32, true);
case O::V_MAX_I16: return Integer16Binary(inst, IR::ValueOpcode::SMax32, true);
case O::V_MIN_U16: return Integer16Binary(inst, IR::ValueOpcode::UMin32, false);
@@ -196,6 +196,7 @@ enum class StageInputKind {
Layer,
SampleId,
BaryCoordSmooth,
BaryCoordSmoothSample,
BaryCoordSmoothCentroid,
BaryCoordNoPerspective,
WorkgroupId,
@@ -1731,7 +1731,7 @@ private:
indirect.table_immediate = table_immediate;
indirect.table_stride = table_stride;
indirect.workgroup_axis = WorkgroupAxis(key);
if (indirect.workgroup_axis != UINT32_MAX && table_source.dword_count == 2u) {
if (indirect.workgroup_axis != UINT32_MAX && !plan.table_indexed) {
// The descriptor also supplies dimensions to shader arithmetic. Keep its reads;
// only the image handle is projected onto the bounded workgroup key.
plan.retain_reads = true;
@@ -143,6 +143,7 @@ void ValidateValueReferences(const Program& program, ShaderStageInputInfo input_
}
break;
case StageInputKind::BaryCoordSmooth:
case StageInputKind::BaryCoordSmoothSample:
case StageInputKind::BaryCoordSmoothCentroid:
case StageInputKind::BaryCoordNoPerspective:
if (component >= 2u) {
@@ -299,6 +300,9 @@ void CollectBuiltinInputs(const Program& program, ShaderInfo& info) {
break;
case StageInputKind::Layer: AddInput(info, kind, 0, 1, "gl_Layer"); break;
case StageInputKind::SampleId: AddInput(info, kind, 0, 1, "gl_SampleID"); break;
case StageInputKind::BaryCoordSmoothSample:
AddInput(info, StageInputKind::SampleId, 0, 1, "gl_SampleID");
[[fallthrough]];
case StageInputKind::BaryCoordSmooth:
case StageInputKind::BaryCoordSmoothCentroid:
AddInput(info, StageInputKind::BaryCoordSmooth, 0, 3, "gl_BaryCoordKHR");
@@ -386,16 +390,23 @@ void CollectOutputs(const Program& program, ShaderStageInputInfo input_info, Sha
AddOutput(info, StageOutputKind::Parameter, export_info.index,
export_info.index, fmt::format("out_param_{}", export_info.index));
break;
case ExportTargetKind::Mrt:
case ExportTargetKind::Mrt: {
if (alpha_remap && export_info.index != 0) {
break;
}
AddOutput(info, StageOutputKind::Mrt, export_info.index, export_info.index,
const auto slot = input_info.pixel->dual_source_blending
? 0u : ShaderPixelExportTarget(input_info.pixel->target_shader_mask,
export_info.index);
if (slot >= 8) {
break;
}
AddOutput(info, StageOutputKind::Mrt, export_info.index, slot,
fmt::format("out_mrt_{}", export_info.index));
if (alpha_remap) {
AddOutput(info, StageOutputKind::Mrt, 1, 1, "out_mrt_1");
AddOutput(info, StageOutputKind::Mrt, 1, 0, "out_mrt_1");
}
break;
}
default: break;
}
}
+25 -10
View File
@@ -525,6 +525,9 @@ static void ShaderGetStaticInputInfoPS(
EXIT_NOT_IMPLEMENTED(ps_info.input_num > std::size(ps_info.interpolator_settings));
ps_info.ps_system_input_base = ShaderCalcPsSystemInputBase(sh);
const uint32_t active_inputs = sh.ps_input_ena & sh.ps_input_addr;
if ((active_inputs & 0x00000001u) != 0) {
ps_info.ps_perspective_sample_vgpr = 0;
}
if ((active_inputs & 0x00000002u) != 0) {
ps_info.ps_perspective_center_vgpr = (active_inputs & 0x00000001u) != 0 ? 2u : 0u;
}
@@ -558,10 +561,12 @@ static void ShaderGetStaticInputInfoPS(
ps_info.interpolator_settings[i] = sh.ps_interpolator_settings[i];
}
ps_info.target_shader_mask = sh.m_cbShaderMask;
for (int i = 0; i < 8; i++) {
const auto slot = ShaderPixelExportTarget(ps_info.target_shader_mask, i);
ps_info.target_output_mode[i] = sh.target_output_mode[i];
ps_info.target_export_mapping[i] = sh.target_output_mode[i] != 0
? target_export_mapping[i]
ps_info.target_export_mapping[i] = sh.target_output_mode[i] != 0 && slot < 8
? target_export_mapping[slot]
: Prospero::ColorComponentMapping {};
}
}
@@ -649,6 +654,7 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
key.push_back(info.ps_system_input_base);
key.push_back(info.custom_interpolation_mask);
key.push_back(info.ps_perspective_center_vgpr);
key.push_back(info.ps_perspective_sample_vgpr);
key.push_back(info.ps_perspective_centroid_vgpr);
key.push_back(static_cast<uint32_t>(info.ps_pos_x));
key.push_back(static_cast<uint32_t>(info.ps_pos_y));
@@ -664,6 +670,8 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
key.push_back(static_cast<uint32_t>(info.dual_source_blending));
key.push_back(static_cast<uint32_t>(info.alpha_blend_source));
key.insert(key.end(), std::begin(info.target_output_mode), std::end(info.target_output_mode));
const auto mask = info.target_shader_mask;
key.push_back((mask | (mask >> 1u) | (mask >> 2u) | (mask >> 3u)) & 0x11111111u);
for (uint32_t base = 0; base < info.target_export_mapping.size(); base += 4u) {
uint32_t packed = 0;
for (uint32_t i = 0; i < 4u; i++) {
@@ -697,11 +705,14 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
const auto& sh = context.GetShaderRegisters();
const auto [data, hash] = ShaderGetMappedData(regs.es_regs.data_addr, "ShaderGetInputInfoVS():");
const bool merged = (context.GetShaderStages() & 0x20u) != 0;
// GS_EN controls amplification, not whether an NGG shader uses a workgroup.
const bool native_ngg = !merged && data.type == Prospero::ShaderBinaryType::kGs &&
regs.gs_regs.rsrc2.lds_size != 0;
auto params = GetShaderParams(
regs.es_regs.data_addr, hash,
std::span<const uint32_t>(regs.gs_user_sgpr.value, regs.gs_regs.rsrc2.user_sgpr), data,
merged ? 8u : 0u);
if (!merged) {
merged || native_ngg ? 8u : 0u);
if (!merged && !native_ngg) {
if (!ShaderGetStaticVertexInputInfo(regs.es_regs.data_addr, regs.gs_user_sgpr,
regs.gs_regs.rsrc2.user_sgpr, sh, data, info)) {
EXIT("failed to prepare vertex shader program\n");
@@ -746,7 +757,7 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
group.primitive_group_size != 1u || group.vertex_group_size != 1u ||
user_vgpr.vgpr1 || user_vgpr.vgpr2 || user_vgpr.vgpr3 ||
mesh.max_vertices != sh.m_vgtGsMaxVertOut);
} else {
} else if (merged) {
EXIT_NOT_IMPLEMENTED(regs.gs_regs.rsrc1.gs_vgpr_component_count != 3u ||
regs.gs_regs.rsrc2.es_vgpr_component_count != 3u);
}
@@ -755,7 +766,7 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
user_config.GetPrimType() != Prospero::PrimitiveType::kTriFan &&
user_config.GetPrimType() != Prospero::PrimitiveType::kTriStrip &&
user_config.GetPrimType() != Prospero::PrimitiveType::kTriList) ||
sh.m_vgtGsOutPrimType != 2u || sh.m_vgtGsMaxVertOut < 3u ||
sh.m_vgtGsOutPrimType != 2u || (!native_ngg && sh.m_vgtGsMaxVertOut < 3u) ||
group.vertex_group_size < mesh.InputPrimitiveSize() ||
mesh.max_vertices == 0u) {
EXIT("unsupported GS assembly: input=%u output=%u vertices=%u GE=%u/%u max_output=%u\n",
@@ -763,14 +774,16 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
group.primitive_group_size, group.vertex_group_size, mesh.max_vertices);
}
mesh.max_primitives = mesh.fast_launch ? mesh.max_vertices :
native_ngg ? mesh.InputPrimitiveCount(mesh.max_vertices) :
group.primitive_group_size * (sh.m_vgtGsMaxVertOut - 2u);
mesh.primitives_per_group = std::min({static_cast<uint32_t>(group.primitive_group_size),
mesh.InputPrimitiveCount(group.vertex_group_size),
mesh.max_vertices / sh.m_vgtGsMaxVertOut});
native_ngg ? mesh.max_primitives :
mesh.max_vertices / sh.m_vgtGsMaxVertOut});
EXIT_IF(mesh.primitives_per_group == 0u);
mesh.vertices_per_group = mesh.InputVertexCount(mesh.primitives_per_group);
mesh.threads_num[0] =
((mesh.max_vertices + mesh.wave_size - 1u) / mesh.wave_size) * mesh.wave_size;
const auto threads = native_ngg && !mesh.fast_launch ? mesh.vertices_per_group : mesh.max_vertices;
mesh.threads_num[0] = ((threads + mesh.wave_size - 1u) / mesh.wave_size) * mesh.wave_size;
mesh.threads_num[1] = mesh.threads_num[2] = 1u;
return params;
}
@@ -910,6 +923,7 @@ void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info) {
"\t ps_system_input_base = %u\n"
"\t custom_interpolation_mask = 0x%08" PRIx32 "\n"
"\t ps_perspective_center_vgpr = %" PRIu32 "\n"
"\t ps_perspective_sample_vgpr = %" PRIu32 "\n"
"\t ps_perspective_centroid_vgpr = %" PRIu32 "\n"
"\t ps_pos_x = %s\n"
"\t ps_pos_y = %s\n"
@@ -923,7 +937,8 @@ void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info) {
"\t ps_early_z = %s\n"
"\t ps_execute_on_noop = %s\n",
info.input_num, info.ps_system_input_base, info.custom_interpolation_mask,
info.ps_perspective_center_vgpr, info.ps_perspective_centroid_vgpr,
info.ps_perspective_center_vgpr, info.ps_perspective_sample_vgpr,
info.ps_perspective_centroid_vgpr,
info.ps_pos_x ? "true" : "false",
info.ps_pos_y ? "true" : "false", info.ps_pos_z ? "true" : "false",
info.ps_pos_w ? "true" : "false", info.ps_front_face ? "true" : "false",
+3
View File
@@ -167,8 +167,10 @@ struct ShaderPixelInputInfo {
uint32_t ps_system_input_base = 0;
uint32_t custom_interpolation_mask = 0;
uint32_t ps_perspective_center_vgpr = UINT32_MAX;
uint32_t ps_perspective_sample_vgpr = UINT32_MAX;
uint32_t ps_perspective_centroid_vgpr = UINT32_MAX;
uint8_t target_output_mode[8] = {};
uint32_t target_shader_mask = UINT32_MAX;
std::array<Prospero::ColorComponentMapping, 8> target_export_mapping = {};
uint32_t scratch_size_dwords = 0;
bool ps_pos_x = false;
@@ -309,6 +311,7 @@ struct ShaderMappedData {
void ShaderInit();
void ShaderMapUserData(uint64_t addr, const ShaderMappedData& data);
uint32_t ShaderPixelExportTarget(uint32_t shader_mask, uint32_t export_index);
void ShaderDbgDumpInputInfo(const ShaderVertexInputInfo& info);
void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info);
@@ -11,6 +11,19 @@ constexpr uint32_t PsInputFlatShade = 0x00000400u;
} // namespace
uint32_t ShaderPixelExportTarget(uint32_t shader_mask, uint32_t export_index) {
for (uint32_t target = 0; target < 8u; target++) {
if (((shader_mask >> (target * 4u)) & 0xfu) == 0u) {
continue;
}
if (export_index == 0u) {
return target;
}
export_index--;
}
return UINT32_MAX;
}
uint32_t ShaderPixelParameterMappedLocation(const ShaderPixelInputInfo& info, uint32_t input) {
return input < info.input_num ? info.interpolator_settings[input] & PsInputOffsetMask : input;
}
+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);
+109 -97
View File
@@ -21,6 +21,7 @@
#include <map>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <vector>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
@@ -168,6 +169,7 @@ static bool VirtualRangesOverlap(uint64_t left_start, uint64_t left_size, uint64
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
static uint32_t g_test_backing_store_unmaps_before_failure = UINT32_MAX;
static callback_func_t g_test_before_backing_map = nullptr;
static callback_func_t g_test_backing_read = nullptr;
#endif
#include "memoryAddressSpace.inc"
@@ -359,12 +361,6 @@ public:
EditUnlocked(start, size, [protection](Range* r) { r->protection = protection; });
}
void SetMemoryType(uint64_t start, uint64_t size, int memory_type) {
Common::LockGuard lock(m_mutex);
EditUnlocked(start, size, [memory_type](Range* r) { r->memory_type = memory_type; });
}
bool Query(uint64_t addr, int flags, Range* out) {
EXIT_IF(out == nullptr);
@@ -702,7 +698,7 @@ public:
bool Release(uint64_t start, size_t len, uint64_t* vaddr, uint64_t* size,
GpuAccessMode* gpu_mode);
bool Map(uint64_t vaddr, uint64_t phys_addr, size_t len, int prot, VirtualMemory::Mode mode,
GpuAccessMode gpu_mode);
GpuAccessMode gpu_mode, const char* name, int memory_type);
bool Unmap(uint64_t vaddr, uint64_t size, GpuAccessMode* gpu_mode,
uint64_t* host_vaddr_to_release = nullptr);
bool Find(uint64_t vaddr, uint64_t* base_addr, size_t* len, int* prot,
@@ -715,7 +711,6 @@ public:
void ProtectMapping(uint64_t vaddr, uint64_t size, int prot, VirtualMemory::Mode mode,
GpuAccessMode gpu_mode);
void SetVirtualRangeName(uint64_t vaddr, uint64_t len, const char* name);
void SetVirtualRangeMemoryType(uint64_t vaddr, uint64_t len, int memory_type);
[[nodiscard]] Common::Mutex& GetMutex() { return m_mutex; }
[[nodiscard]] const std::map<uint64_t, AllocatedBlock>& GetPhysicalBlocks() const {
@@ -1368,7 +1363,8 @@ bool PhysicalMemory::Release(uint64_t start, size_t len, uint64_t* vaddr, uint64
}
bool PhysicalMemory::Map(uint64_t vaddr, uint64_t phys_addr, size_t len, int prot,
VirtualMemory::Mode mode, GpuAccessMode gpu_mode) {
VirtualMemory::Mode mode, GpuAccessMode gpu_mode, const char* name,
int memory_type) {
Common::LockGuard lock(m_mutex);
if (len == 0 || UINT64_MAX - phys_addr < len) {
@@ -1413,6 +1409,8 @@ bool PhysicalMemory::Map(uint64_t vaddr, uint64_t phys_addr, size_t len, int pro
mapping.prot = prot;
mapping.mode = mode;
mapping.gpu_mode = gpu_mode;
mapping.memory_type = memory_type;
CopyVirtualRangeName(mapping.name, name);
m_mappings.push_back(mapping);
if (has_automatic) {
RemoveFreeRange(m_automatic_free, phys_addr, len);
@@ -2040,16 +2038,6 @@ void FlexibleMemory::SetVirtualRangeName(uint64_t vaddr, uint64_t len, const cha
}
}
void PhysicalMemory::SetVirtualRangeMemoryType(uint64_t vaddr, uint64_t len, int memory_type) {
Common::LockGuard lock(m_mutex);
for (auto& b: m_mappings) {
if (VirtualRangesOverlap(vaddr, len, b.map_vaddr, b.map_size)) {
b.memory_type = memory_type;
}
}
}
uint64_t FlexibleMemory::Available() {
Common::LockGuard lock(m_mutex);
@@ -2268,12 +2256,9 @@ static bool UnmapPooledBackingTransactional(const std::vector<PooledMemory::Mapp
return true;
}
int32_t KYTY_SYSV_ABI KernelMapNamedFlexibleMemory(void** addr_in_out, size_t len, int prot,
int flags, const char* name) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
// The ABI entry points and batch operations hold g_memory_operation_mutex.
static int MapFlexibleMemory(void** addr_in_out, size_t len, int prot, int flags,
const char* name) {
EXIT_NOT_IMPLEMENTED(addr_in_out == nullptr);
constexpr size_t PAGE_SIZE = 0x4000;
@@ -2398,8 +2383,17 @@ int32_t KYTY_SYSV_ABI KernelMapNamedFlexibleMemory(void** addr_in_out, size_t le
return OK;
}
int32_t KYTY_SYSV_ABI KernelMapNamedFlexibleMemory(void** addr_in_out, size_t len, int prot,
int flags, const char* name) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
return MapFlexibleMemory(addr_in_out, len, prot, flags, name);
}
int KYTY_SYSV_ABI KernelMapFlexibleMemory(void** addr_in_out, size_t len, int prot, int flags) {
return KernelMapNamedFlexibleMemory(addr_in_out, len, prot, flags, "");
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
return MapFlexibleMemory(addr_in_out, len, prot, flags, "");
}
int KYTY_SYSV_ABI KernelSetPrtAperture(int index, void* addr, size_t len) {
@@ -2607,11 +2601,8 @@ static int UnmapMemoryRange(uint64_t vaddr, size_t len) {
return OK;
}
int KYTY_SYSV_ABI KernelMunmap(uint64_t vaddr, size_t len) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
// The ABI entry point and batch operations hold g_memory_operation_mutex.
static int UnmapMemory(uint64_t vaddr, size_t len) {
LOGF("\t start = 0x%016" PRIx64 "\n"
"\t len = 0x%016" PRIx64 "\n",
vaddr, len);
@@ -2627,6 +2618,12 @@ int KYTY_SYSV_ABI KernelMunmap(uint64_t vaddr, size_t len) {
return UnmapMemoryRange(vaddr, len);
}
int KYTY_SYSV_ABI KernelMunmap(uint64_t vaddr, size_t len) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
return UnmapMemory(vaddr, len);
}
size_t KYTY_SYSV_ABI KernelGetDirectMemorySize() {
PRINT_NAME();
@@ -2909,7 +2906,8 @@ static int ReleaseDirectMemoryInternal(int64_t start, size_t len) {
for (const auto& removed: metadata_unmapped) {
EXIT_IF(!g_physical_memory->Map(removed.map_vaddr, removed.start_addr,
removed.map_size, removed.prot, removed.mode,
removed.gpu_mode));
removed.gpu_mode, removed.name,
removed.memory_type));
}
restore_owner_aliases(owner_unmapped);
restore_gpu_aliases();
@@ -2980,12 +2978,10 @@ int KYTY_SYSV_ABI KernelCheckedReleaseDirectMemory(int64_t start, size_t len) {
return result == KERNEL_ERROR_EACCES ? KERNEL_ERROR_ENOENT : result;
}
int KYTY_SYSV_ABI KernelMapDirectMemory(void** addr, size_t len, int prot, int flags,
int64_t direct_memory_start, size_t alignment) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
// The ABI entry points and batch operations hold g_memory_operation_mutex.
static int MapDirectMemory(void** addr, size_t len, int prot, int flags,
int64_t direct_memory_start, size_t alignment, const char* name,
int type = -1) {
if (addr == nullptr) {
return KERNEL_ERROR_EFAULT;
}
@@ -3036,10 +3032,8 @@ int KYTY_SYSV_ABI KernelMapDirectMemory(void** addr, size_t len, int prot, int f
old.start_addr, old.mode));
}
EXIT_IF(!g_physical_memory->Map(old.map_vaddr, old.start_addr, old.map_size,
old.prot, old.mode, old.gpu_mode));
g_physical_memory->SetVirtualRangeName(old.map_vaddr, old.map_size, old.name);
g_physical_memory->SetVirtualRangeMemoryType(old.map_vaddr, old.map_size,
old.memory_type);
old.prot, old.mode, old.gpu_mode, old.name,
old.memory_type));
}
MapGpuRange(in_addr, len);
};
@@ -3168,7 +3162,11 @@ int KYTY_SYSV_ABI KernelMapDirectMemory(void** addr, size_t len, int prot, int f
return KERNEL_ERROR_ENOMEM;
}
if (!g_physical_memory->Map(out_addr, direct_memory_start, len, prot, mode, gpu_mode)) {
PhysicalMemory::AllocatedBlock mapped_block {};
g_physical_memory->Find(direct_memory_start, false, &mapped_block);
const int memory_type = type == -1 ? mapped_block.memory_type : type;
if (!g_physical_memory->Map(out_addr, direct_memory_start, len, prot, mode, gpu_mode, name,
memory_type)) {
LOGF_COLOR(Log::Color::Red, "\t [Fail]\n");
if (replaced_mappings.empty()) {
EXIT_IF(!g_guest_address_space->UnmapBacking(out_addr, len));
@@ -3178,18 +3176,16 @@ int KYTY_SYSV_ABI KernelMapDirectMemory(void** addr, size_t len, int prot, int f
return KERNEL_ERROR_EBUSY;
}
PhysicalMemory::AllocatedBlock mapped_block {};
g_physical_memory->Find(direct_memory_start, false, &mapped_block);
const bool published = !replaced_mappings.empty() || reserved_target
? g_virtual_ranges->ReplaceSpan(
out_addr, len, replaced_mappings.empty()
? VirtualRangeType::Reserved
: VirtualRangeType::Direct,
direct_memory_start,
prot, mapped_block.memory_type, VirtualRangeType::Direct, "")
prot, memory_type, VirtualRangeType::Direct, name)
: g_virtual_ranges->Add(out_addr, len, direct_memory_start, prot,
mapped_block.memory_type,
VirtualRangeType::Direct, "");
memory_type,
VirtualRangeType::Direct, name);
if (!published) {
GpuAccessMode rollback_gpu_mode = GpuAccessMode::NoAccess;
EXIT_IF(!g_physical_memory->Unmap(out_addr, len, &rollback_gpu_mode));
@@ -3212,22 +3208,18 @@ int KYTY_SYSV_ABI KernelMapDirectMemory(void** addr, size_t len, int prot, int f
return OK;
}
int KYTY_SYSV_ABI KernelMapDirectMemory(void** addr, size_t len, int prot, int flags,
int64_t direct_memory_start, size_t alignment) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
return MapDirectMemory(addr, len, prot, flags, direct_memory_start, alignment, "");
}
int KYTY_SYSV_ABI KernelMapDirectMemory2(void** addr, size_t len, int type, int prot, int flags,
int64_t direct_memory_start, size_t alignment) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
LOGF("\t type = %d\n", type);
auto ret = KernelMapDirectMemory(addr, len, prot, flags, direct_memory_start, alignment);
if (ret == OK && addr != nullptr && *addr != nullptr) {
const auto out_addr = reinterpret_cast<uint64_t>(*addr);
g_physical_memory->SetVirtualRangeMemoryType(out_addr, len, type);
g_virtual_ranges->SetMemoryType(out_addr, len, type);
}
return ret;
return MapDirectMemory(addr, len, prot, flags, direct_memory_start, alignment, "", type);
}
int MapAutomaticMemory(uint64_t vaddr, size_t size, int type, int prot) {
@@ -3246,11 +3238,11 @@ int MapAutomaticMemory(uint64_t vaddr, size_t size, int type, int prot) {
uint64_t mapped = 0;
for (const auto& [offset, length]: ranges) {
void* address = reinterpret_cast<void*>(vaddr + mapped);
const int result = KernelMapDirectMemory2(&address, length, type, prot, 0x10,
static_cast<int64_t>(offset), page_size);
const int result = MapDirectMemory(&address, length, prot, 0x10,
static_cast<int64_t>(offset), page_size, "", type);
if (result != OK) {
if (mapped != 0) {
EXIT_IF(KernelMunmap(vaddr, mapped) != OK);
EXIT_IF(UnmapMemory(vaddr, mapped) != OK);
}
g_physical_memory->RestoreAutomatic(ranges);
return result;
@@ -3277,13 +3269,7 @@ int KYTY_SYSV_ABI KernelMapNamedDirectMemory(void** addr, size_t len, int prot,
return KERNEL_ERROR_ENAMETOOLONG;
}
auto ret = KernelMapDirectMemory(addr, len, prot, flags, direct_memory_start, alignment);
if (ret == OK && addr != nullptr) {
g_physical_memory->SetVirtualRangeName(reinterpret_cast<uint64_t>(*addr), len, name);
g_virtual_ranges->Rename(reinterpret_cast<uint64_t>(*addr), len, name);
}
return ret;
return MapDirectMemory(addr, len, prot, flags, direct_memory_start, alignment, name);
}
int KYTY_SYSV_ABI KernelIsAddressSanitizerEnabled() {
@@ -3389,7 +3375,8 @@ static bool ReplaceFixedRangeWithReserved(uint64_t start, uint64_t size) {
if (chunk.backend_unmapped) {
backend_restored = g_physical_memory->Map(
chunk.range.start, chunk.range.offset, chunk.range.size,
chunk.range.protection, chunk.mode, chunk.gpu_mode);
chunk.range.protection, chunk.mode, chunk.gpu_mode, chunk.range.name,
chunk.range.memory_type);
}
} else if (chunk.range.type == VirtualRangeType::Flexible && chunk.backend_unmapped) {
host_restored = backend_restored =
@@ -3584,6 +3571,10 @@ void TestBeforeNextBackingMap(callback_func_t callback) {
g_test_before_backing_map = callback;
}
void TestSetBackingReadCallback(callback_func_t callback) {
g_test_backing_read = callback;
}
void TestFailNextPhysicalMemoryUnmap() {
TestFailPhysicalMemoryUnmapAfter(0);
}
@@ -3621,6 +3612,36 @@ uint64_t TestGuestBackingSize() {
return g_guest_address_space->GetBackingSize();
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool TestWindowsBackingViewModes() {
constexpr uint64_t size = 0x10000;
GuestBackingStore backing(size);
void* placeholder = VirtualAlloc2(GetCurrentProcess(), nullptr, size,
MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS,
nullptr, 0);
if (placeholder == nullptr) {
return false;
}
const auto address = reinterpret_cast<uint64_t>(placeholder);
bool valid = true;
for (auto [mode, protection]: std::array {
std::pair {VirtualMemory::Mode::NoAccess, DWORD {PAGE_NOACCESS}},
std::pair {VirtualMemory::Mode::Read, DWORD {PAGE_READONLY}},
std::pair {VirtualMemory::Mode::ReadWrite, DWORD {PAGE_READWRITE}},
std::pair {VirtualMemory::Mode::ExecuteReadWrite, DWORD {PAGE_EXECUTE_READWRITE}}}) {
if (!backing.MapFixed(address, size, 0, mode)) {
valid = false;
break;
}
MEMORY_BASIC_INFORMATION info {};
valid = VirtualQuery(placeholder, &info, sizeof(info)) != 0 &&
info.Type == MEM_MAPPED && info.Protect == protection && valid;
EXIT_IF(!backing.Unmap(address, size));
}
return VirtualFree(placeholder, 0, MEM_RELEASE) != 0 && valid;
}
#endif
bool TestGuestFreeRangeBounds() {
return GuestFreeRangeContains(0x10000, 0x20000, 0x18000, 0x4000) &&
!GuestFreeRangeContains(0x10000, 0x20000, 0x40000, 0x4000) &&
@@ -3875,11 +3896,8 @@ bool FreeGuestMemory(uint64_t vaddr, uint64_t size) {
return FreeGuestMemoryOwner(vaddr, mapped_size);
}
int KYTY_SYSV_ABI KernelMprotect(const void* addr, size_t len, int prot) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
// The ABI entry points and batch operations hold g_memory_operation_mutex.
static int ProtectMemory(const void* addr, size_t len, int prot) {
auto vaddr = reinterpret_cast<uint64_t>(addr);
LOGF("\t addr = 0x%016" PRIx64 "\n"
@@ -3937,19 +3955,17 @@ int KYTY_SYSV_ABI KernelMprotect(const void* addr, size_t len, int prot) {
return OK;
}
int KYTY_SYSV_ABI KernelMprotect(const void* addr, size_t len, int prot) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
return ProtectMemory(addr, len, prot);
}
int KYTY_SYSV_ABI KernelMtypeprotect(const void* addr, size_t len, int type, int prot) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
LOGF("\t addr = 0x%016" PRIx64 "\n"
"\t len = 0x%016" PRIx64 "\n"
"\t type = 0x%08" PRIx32 "\n"
"\t prot = 0x%08" PRIx32 "\n",
reinterpret_cast<uint64_t>(addr), static_cast<uint64_t>(len), static_cast<uint32_t>(type),
static_cast<uint32_t>(prot));
return KernelMprotect(addr, len, prot);
LOGF("\t type = %d\n", type);
return ProtectMemory(addr, len, prot);
}
int KYTY_SYSV_ABI KernelBatchMap2(KernelBatchMapEntry* entries, int num_entries,
@@ -3996,23 +4012,19 @@ int KYTY_SYSV_ABI KernelBatchMap2(KernelBatchMapEntry* entries, int num_entries,
int result = OK;
switch (entry->operation) {
case MAP_OP_MAP_DIRECT:
result = KernelMapNamedDirectMemory(&entry->start, entry->length, entry->protection,
flags, static_cast<int64_t>(entry->offset), 0,
"anon");
result = MapDirectMemory(&entry->start, entry->length, entry->protection, flags,
static_cast<int64_t>(entry->offset), 0, "anon");
break;
case MAP_OP_UNMAP:
result = KernelMunmap(reinterpret_cast<uint64_t>(entry->start), entry->length);
result = UnmapMemory(reinterpret_cast<uint64_t>(entry->start), entry->length);
break;
case MAP_OP_PROTECT:
result = KernelMprotect(entry->start, entry->length, entry->protection);
case MAP_OP_TYPE_PROTECT:
result = ProtectMemory(entry->start, entry->length, entry->protection);
break;
case MAP_OP_MAP_FLEXIBLE:
result = KernelMapNamedFlexibleMemory(&entry->start, entry->length,
entry->protection, flags, "anon");
break;
case MAP_OP_TYPE_PROTECT:
result =
KernelMtypeprotect(entry->start, entry->length, entry->type, entry->protection);
result = MapFlexibleMemory(&entry->start, entry->length, entry->protection, flags,
"anon");
break;
default: result = KERNEL_ERROR_EINVAL; break;
}
+1
View File
@@ -192,6 +192,7 @@ bool FreeGuestMemory(uint64_t vaddr, uint64_t size);
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
void TestBeforeNextBackingMap(callback_func_t callback);
void TestSetBackingReadCallback(callback_func_t callback);
void TestFailNextPhysicalMemoryUnmap();
void TestFailPhysicalMemoryUnmapAfter(uint32_t successful_unmaps);
void TestFailGuestBackingStoreUnmapAfter(uint32_t successful_unmaps);
+88 -66
View File
@@ -161,30 +161,90 @@ public:
if (!IsAvailable() || !RangeOk(backing_offset, size)) {
return false;
}
std::lock_guard lock(m_mutex);
std::memset(m_backing_base + backing_offset, 0, size);
return true;
}
bool TryWriteBacking(uint64_t vaddr, const void* data, uint64_t size) {
if (data == nullptr) {
if (!IsAvailable() || data == nullptr || size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
std::lock_guard lock(m_mutex);
const auto end = vaddr + size;
auto first = m_maps.upper_bound(vaddr);
if (first == m_maps.begin()) {
return false;
}
--first;
auto current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
if (mapping == m_maps.end() || current < mapping->second.vaddr ||
current >= mapping->second.vaddr + mapping->second.size) {
return false;
}
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
if (!RangeOk(entry.backing_offset + current - entry.vaddr, bytes)) {
return false;
}
current += bytes;
}
const auto* source = static_cast<const uint8_t*>(data);
return TryTransferBacking(vaddr, size,
[source](uint8_t* backing, uint64_t data_offset, size_t bytes) {
std::memcpy(backing, source + data_offset, bytes);
});
current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
const auto offset = entry.backing_offset + current - entry.vaddr;
std::memcpy(m_backing_base + offset, source + (current - vaddr), bytes);
current += bytes;
}
return true;
}
bool TryReadBacking(uint64_t vaddr, void* data, uint64_t size) {
if (data == nullptr) {
if (!IsAvailable() || data == nullptr || size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
std::shared_lock lock(m_mutex);
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
if (g_test_backing_read != nullptr) {
g_test_backing_read(vaddr, size);
}
#endif
const auto end = vaddr + size;
auto first = m_maps.upper_bound(vaddr);
if (first == m_maps.begin()) {
return false;
}
--first;
auto current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
if (mapping == m_maps.end() || current < mapping->second.vaddr ||
current >= mapping->second.vaddr + mapping->second.size) {
return false;
}
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
if (!RangeOk(entry.backing_offset + current - entry.vaddr, bytes)) {
return false;
}
current += bytes;
}
auto* destination = static_cast<uint8_t*>(data);
return TryTransferBacking(
vaddr, size, [destination](uint8_t* backing, uint64_t data_offset, size_t bytes) {
std::memcpy(destination + data_offset, backing, bytes);
});
current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
const auto offset = entry.backing_offset + current - entry.vaddr;
std::memcpy(destination + (current - vaddr), m_backing_base + offset, bytes);
current += bytes;
}
return true;
}
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size) {
@@ -192,29 +252,24 @@ public:
return false;
}
std::lock_guard lock(m_mutex);
const auto end = vaddr + size;
auto first = m_maps.lower_bound(vaddr);
if (first != m_maps.begin()) {
auto previous = std::prev(first);
if (previous->second.vaddr + previous->second.size > vaddr) {
first = previous;
}
std::shared_lock lock(m_mutex);
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
if (g_test_backing_read != nullptr) {
g_test_backing_read(vaddr, size);
}
for (auto mapping = first; mapping != m_maps.end() && mapping->second.vaddr < end;
++mapping) {
const auto& entry = mapping->second;
const auto start = std::max(vaddr, entry.vaddr);
const auto bytes = std::min(end, entry.vaddr + entry.size) - start;
if (!RangeOk(entry.backing_offset + start - entry.vaddr, bytes)) {
return false;
#endif
const auto end = vaddr + size;
auto mapping = m_maps.lower_bound(vaddr);
if (mapping != m_maps.begin()) {
auto previous = std::prev(mapping);
if (previous->second.vaddr + previous->second.size > vaddr) {
mapping = previous;
}
}
auto* destination = static_cast<uint8_t*>(data);
auto cursor = vaddr;
for (auto mapping = first; mapping != m_maps.end() && mapping->second.vaddr < end;
++mapping) {
for (; mapping != m_maps.end() && mapping->second.vaddr < end; ++mapping) {
const auto& entry = mapping->second;
const auto start = std::max(vaddr, entry.vaddr);
const auto bytes = std::min(end, entry.vaddr + entry.size) - start;
@@ -437,7 +492,7 @@ public:
return false;
}
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
const auto end = vaddr + size;
auto current = vaddr;
@@ -457,39 +512,6 @@ public:
}
private:
template <typename Transfer>
bool TryTransferBacking(uint64_t vaddr, uint64_t size, Transfer transfer) {
if (!IsAvailable() || size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
std::lock_guard lock(m_mutex);
const auto end = vaddr + size;
auto current = vaddr;
while (current < end) {
MapEntry entry {};
if (!FindContainingUnlocked(current, 1, &entry)) {
return false;
}
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
if (!RangeOk(entry.backing_offset + current - entry.vaddr, bytes)) {
return false;
}
current += bytes;
}
current = vaddr;
while (current < end) {
MapEntry entry {};
EXIT_IF(!FindContainingUnlocked(current, 1, &entry));
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
const auto offset = entry.backing_offset + current - entry.vaddr;
transfer(m_backing_base + offset, current - vaddr, static_cast<size_t>(bytes));
current += bytes;
}
return true;
}
struct MapEntry {
uint64_t vaddr = 0;
uint64_t size = 0;
@@ -597,7 +619,7 @@ private:
return nullptr;
}
if (!ApplyProtection(ptr, size, mode)) {
if (mode == VirtualMemory::Mode::NoAccess && !ApplyProtection(ptr, size, mode)) {
UnmapViewOfFile2(GetCurrentProcess(), ptr, MEM_PRESERVE_PLACEHOLDER);
SetFailure(failure_reason, FailureReason::ProtectFailed);
return nullptr;
@@ -656,7 +678,7 @@ private:
uint8_t* m_backing_base = nullptr;
uint64_t m_size = 0;
std::mutex m_mutex;
std::shared_mutex m_mutex;
std::map<uint64_t, MapEntry> m_maps;
};
@@ -727,7 +749,7 @@ public:
if (size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
return std::any_of(m_owned.begin(), m_owned.end(), [vaddr, size](const auto& range) {
return vaddr >= range.first && vaddr + size <= range.first + range.second;
});
@@ -755,7 +777,7 @@ public:
if (!Owns(vaddr, size)) {
return false;
}
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
auto mapping = m_mapped.upper_bound(vaddr);
if (mapping != m_mapped.begin()) {
--mapping;
@@ -1381,7 +1403,7 @@ private:
#endif
}
std::mutex m_mutex;
std::shared_mutex m_mutex;
std::map<uint64_t, uint64_t> m_free;
std::map<uint64_t, MappedRegion> m_mapped;
std::vector<std::pair<uint64_t, uint64_t>> m_owned;
+9
View File
@@ -3200,6 +3200,15 @@ static void* RunThread(void* arg) {
os_thread_id = GetHostThreadId();
#endif
thread->host_thread_id = os_thread_id;
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX
if (!thread->name.empty()) {
#if defined(__APPLE__)
pthread_setname_np(thread->name.substr(0, 63).c_str());
#else
pthread_setname_np(pthread_self(), thread->name.substr(0, 15).c_str());
#endif
}
#endif
LOGF("\tPthread run begin: %s, id = %d, os_thread_id = %" PRIu64 ", entry = 0x%016" PRIx64
", arg = 0x%016" PRIx64 ", stack_addr = 0x%016" PRIx64 ", stack_size = %" PRIu64 "\n",
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>840</width>
<width>900</width>
<height>620</height>
</rect>
</property>
@@ -179,6 +179,18 @@
<property name="text"><string>Auto-hide cursor</string></property>
</widget>
</item>
<item row="9" column="1">
<widget class="QCheckBox" name="checkBox_skip_notice_screen">
<property name="toolTip"><string>Skip startup logos and notices in supported games.</string></property>
<property name="text"><string>Skip boot logos</string></property>
</widget>
</item>
<item row="10" column="1">
<widget class="QCheckBox" name="checkBox_sync_raw_image_buffers">
<property name="toolTip"><string>Can fix missing textures and incorrect transparency in some games. May reduce performance.</string></property>
<property name="text"><string>Accurate texture synchronization</string></property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -400,6 +412,13 @@ To use your own sound, replace the emulator's assets/sounds/trophy-unlock.wav wi
<property name="toolTip"><string>Restore default settings</string></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="settings_button">
<property name="minimumSize"><size><width>80</width><height>25</height></size></property>
<property name="text"><string>Game Configs</string></property>
<property name="toolTip"><string>Import or export game configs</string></property>
</widget>
</item>
<item>
<spacer name="buttonSpacer">
<property name="orientation"><enum>Qt::Horizontal</enum></property>
@@ -125,7 +125,7 @@
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Edit configuration&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>Edit game config</string>
</property>
<property name="text">
<string/>
@@ -150,7 +150,7 @@
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Delete configuration&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>Clear game config</string>
</property>
<property name="styleSheet">
<string notr="true"/>
+15 -87
View File
@@ -6,6 +6,7 @@
#include <QByteArray>
#include <QChar>
#include <QColor>
#include <QJsonObject>
#include <QMetaEnum>
#include <QMetaType>
#include <QObject>
@@ -14,9 +15,6 @@
#include <QStringList>
#include <QVariant>
#define KYTY_CFG_SET(n) s->setValue(#n, QVariant::fromValue(n).toString());
#define KYTY_CFG_GET(n) n = s->value(#n).value<decltype(n)>();
template <class T>
inline QStringList EnumToList() {
QStringList ret;
@@ -121,12 +119,14 @@ public:
bool fullscreen_enabled = false;
bool hide_cursor_enabled = false;
bool readback_linear_images = false;
bool sync_raw_image_buffers = false;
bool tessellation_enabled = false;
bool trophy_enabled = true;
bool skip_notice_screen = false;
int vblank_frequency = 60;
int console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
bool shader_validation_enabled = true;
bool shader_validation_enabled = false;
ShaderOptimizationType shader_optimization_type = ShaderOptimizationType::Performance;
LogDirection shader_log_direction = LogDirection::Silent;
QString shader_log_folder = "_Shaders";
@@ -154,8 +154,10 @@ public:
fullscreen_enabled = other.fullscreen_enabled;
hide_cursor_enabled = other.hide_cursor_enabled;
readback_linear_images = other.readback_linear_images;
sync_raw_image_buffers = other.sync_raw_image_buffers;
tessellation_enabled = other.tessellation_enabled;
trophy_enabled = other.trophy_enabled;
skip_notice_screen = other.skip_notice_screen;
vblank_frequency = other.vblank_frequency;
console_language = other.console_language;
vulkan_validation_enabled = other.vulkan_validation_enabled;
@@ -188,90 +190,16 @@ public:
game_comment = other.game_comment;
}
void WriteSettings(QSettings* s) const {
KYTY_CFG_SET(name);
KYTY_CFG_SET(basedir);
KYTY_CFG_SET(game_path);
KYTY_CFG_SET(custom_settings);
KYTY_CFG_SET(screen_resolution);
KYTY_CFG_SET(user_name);
KYTY_CFG_SET(user_id);
KYTY_CFG_SET(audio_input_device);
KYTY_CFG_SET(present_mode);
KYTY_CFG_SET(gpu_index);
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(hide_cursor_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(tessellation_enabled);
KYTY_CFG_SET(trophy_enabled);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
KYTY_CFG_SET(vulkan_validation_enabled);
KYTY_CFG_SET(shader_validation_enabled);
KYTY_CFG_SET(shader_optimization_type);
KYTY_CFG_SET(shader_log_direction);
KYTY_CFG_SET(shader_log_folder);
KYTY_CFG_SET(command_buffer_dump_enabled);
KYTY_CFG_SET(command_buffer_dump_folder);
KYTY_CFG_SET(printf_direction);
KYTY_CFG_SET(printf_output_file);
KYTY_CFG_SET(profiler_enabled);
KYTY_CFG_SET(renderdoc_enabled);
KYTY_CFG_SET(amd_cpu_enabled);
#if defined(_WIN32)
KYTY_CFG_SET(red_zone_protection_enabled);
#endif
s->setValue("host_input_mapping", host_input_mapping);
KYTY_CFG_SET(elf);
}
void WriteSettings(QSettings* s) const;
void ReadSettings(QSettings* s);
void ReadSettings(QSettings* s) {
KYTY_CFG_GET(name);
KYTY_CFG_GET(basedir);
KYTY_CFG_GET(game_path);
KYTY_CFG_GET(custom_settings);
KYTY_CFG_GET(screen_resolution);
user_name = s->value("user_name", user_name).toString();
bool user_id_ok = false;
auto saved_user_id = s->value("user_id", user_id).toInt(&user_id_ok);
user_id = user_id_ok && Config::IsConfiguredUserIdValid(saved_user_id)
? saved_user_id
: Config::DEFAULT_USER_ID;
audio_input_device = s->value("audio_input_device", audio_input_device).toString();
KYTY_CFG_GET(present_mode);
gpu_index = s->value("gpu_index", -1).toInt();
if (EnumToText(present_mode).isEmpty()) {
present_mode = PresentMode::Mailbox;
}
KYTY_CFG_GET(fullscreen_enabled);
KYTY_CFG_GET(hide_cursor_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(tessellation_enabled);
trophy_enabled = s->value("trophy_enabled", trophy_enabled).toBool();
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
console_language = s->value("console_language", console_language).toInt();
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
console_language = DEFAULT_CONSOLE_LANGUAGE;
}
KYTY_CFG_GET(vulkan_validation_enabled);
KYTY_CFG_GET(shader_validation_enabled);
KYTY_CFG_GET(shader_optimization_type);
KYTY_CFG_GET(shader_log_direction);
KYTY_CFG_GET(shader_log_folder);
KYTY_CFG_GET(command_buffer_dump_enabled);
KYTY_CFG_GET(command_buffer_dump_folder);
KYTY_CFG_GET(printf_direction);
KYTY_CFG_GET(printf_output_file);
KYTY_CFG_GET(profiler_enabled);
KYTY_CFG_GET(renderdoc_enabled);
amd_cpu_enabled = s->value("amd_cpu_enabled", false).toBool();
#if defined(_WIN32)
red_zone_protection_enabled =
s->value("red_zone_protection_enabled", red_zone_protection_enabled).toBool();
#endif
host_input_mapping = s->value("host_input_mapping", host_input_mapping).toStringList();
elf = s->value("elf", elf).toString();
}
[[nodiscard]] QVariantMap GameSettings() const;
// Call on a temporary configuration: validation can fail after loading values.
bool SetGameSettings(const QJsonObject& settings, QString& error);
private:
template <class Settings>
void ReadGameSettingsValues(const Settings& s);
};
Q_DECLARE_METATYPE(Configuration*)
@@ -33,6 +33,8 @@ public:
signals:
void PreviewControllerColor(const QString& color);
void ImportGameSettings();
void ExportGameSettings();
private:
Ui::ConfigurationEditDialog* m_ui = nullptr;
@@ -74,6 +74,8 @@ private:
void SelectItem(QTreeWidgetItem* witem);
void ApplyCompatibility();
void UpdateToolbarIcons();
void ImportGameSettings(QWidget* parent);
void ExportGameSettings(QWidget* parent) const;
[[nodiscard]] bool HasValidGameDirectory() const;
ConfigurationItem* m_selected_item = nullptr;
+149
View File
@@ -0,0 +1,149 @@
#include "configuration.h"
QVariantMap Configuration::GameSettings() const {
QVariantMap values;
#define KYTY_CFG_SET(n) values.insert(QStringLiteral(#n), QVariant::fromValue(n).toString())
KYTY_CFG_SET(screen_resolution);
KYTY_CFG_SET(user_name);
KYTY_CFG_SET(user_id);
KYTY_CFG_SET(audio_input_device);
KYTY_CFG_SET(present_mode);
KYTY_CFG_SET(gpu_index);
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(hide_cursor_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(sync_raw_image_buffers);
KYTY_CFG_SET(tessellation_enabled);
KYTY_CFG_SET(trophy_enabled);
KYTY_CFG_SET(skip_notice_screen);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
KYTY_CFG_SET(vulkan_validation_enabled);
KYTY_CFG_SET(shader_validation_enabled);
KYTY_CFG_SET(shader_optimization_type);
KYTY_CFG_SET(shader_log_direction);
KYTY_CFG_SET(shader_log_folder);
KYTY_CFG_SET(command_buffer_dump_enabled);
KYTY_CFG_SET(command_buffer_dump_folder);
KYTY_CFG_SET(printf_direction);
KYTY_CFG_SET(printf_output_file);
KYTY_CFG_SET(profiler_enabled);
KYTY_CFG_SET(renderdoc_enabled);
KYTY_CFG_SET(amd_cpu_enabled);
#if defined(_WIN32)
KYTY_CFG_SET(red_zone_protection_enabled);
#endif
KYTY_CFG_SET(elf);
#undef KYTY_CFG_SET
return values;
}
template <class Settings>
void Configuration::ReadGameSettingsValues(const Settings& s) {
#define KYTY_CFG_GET(n) n = s.value(QStringLiteral(#n)).template value<decltype(n)>()
KYTY_CFG_GET(screen_resolution);
user_name = s.value("user_name", user_name).toString();
bool user_id_ok = false;
auto saved_user_id = s.value("user_id", user_id).toInt(&user_id_ok);
user_id = user_id_ok && Config::IsConfiguredUserIdValid(saved_user_id)
? saved_user_id
: Config::DEFAULT_USER_ID;
audio_input_device = s.value("audio_input_device", audio_input_device).toString();
KYTY_CFG_GET(present_mode);
gpu_index = s.value("gpu_index", -1).toInt();
if (EnumToText(present_mode).isEmpty()) {
present_mode = PresentMode::Mailbox;
}
KYTY_CFG_GET(fullscreen_enabled);
KYTY_CFG_GET(hide_cursor_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(sync_raw_image_buffers);
KYTY_CFG_GET(tessellation_enabled);
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
KYTY_CFG_GET(skip_notice_screen);
vblank_frequency = s.value("vblank_frequency", vblank_frequency).toInt();
console_language = s.value("console_language", console_language).toInt();
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
console_language = DEFAULT_CONSOLE_LANGUAGE;
}
KYTY_CFG_GET(vulkan_validation_enabled);
KYTY_CFG_GET(shader_validation_enabled);
KYTY_CFG_GET(shader_optimization_type);
KYTY_CFG_GET(shader_log_direction);
KYTY_CFG_GET(shader_log_folder);
KYTY_CFG_GET(command_buffer_dump_enabled);
KYTY_CFG_GET(command_buffer_dump_folder);
KYTY_CFG_GET(printf_direction);
KYTY_CFG_GET(printf_output_file);
KYTY_CFG_GET(profiler_enabled);
KYTY_CFG_GET(renderdoc_enabled);
amd_cpu_enabled = s.value("amd_cpu_enabled", false).toBool();
#if defined(_WIN32)
red_zone_protection_enabled =
s.value("red_zone_protection_enabled", red_zone_protection_enabled).toBool();
#endif
elf = s.value("elf", elf).toString();
#undef KYTY_CFG_GET
}
void Configuration::WriteSettings(QSettings* s) const {
const auto values = GameSettings();
for (auto it = values.constBegin(); it != values.constEnd(); ++it) {
s->setValue(it.key(), it.value());
}
s->setValue("name", name);
s->setValue("basedir", basedir);
s->setValue("game_path", game_path);
s->setValue("custom_settings", QVariant::fromValue(custom_settings).toString());
s->setValue("host_input_mapping", host_input_mapping);
}
void Configuration::ReadSettings(QSettings* s) {
ReadGameSettingsValues(*s);
name = s->value("name").toString();
basedir = s->value("basedir").toString();
game_path = s->value("game_path").toString();
custom_settings = s->value("custom_settings").toBool();
host_input_mapping = s->value("host_input_mapping", host_input_mapping).toStringList();
}
bool Configuration::SetGameSettings(const QJsonObject& settings, QString& error) {
const auto fail = [&error](const QString& message) {
error = message;
return false;
};
auto values = GameSettings();
for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) {
if (!values.contains(it.key())) {
return fail(tr("Unknown game setting: %1").arg(it.key()));
}
if (!it.value().isString()) {
return fail(tr("Game setting %1 must be a string.").arg(it.key()));
}
values.insert(it.key(), it.value().toString());
}
ReadGameSettingsValues(values);
const auto canonical = GameSettings();
for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) {
if (canonical.value(it.key()) != values.value(it.key())) {
return fail(tr("Invalid value for game setting: %1").arg(it.key()));
}
}
if (user_name.trimmed().isEmpty() || user_name.toUtf8().size() > Config::MAX_USER_NAME_LENGTH) {
return fail(tr("User name must contain 1-16 UTF-8 bytes."));
}
if (gpu_index < -1) {
return fail(tr("GPU index must be -1 or greater."));
}
if (vblank_frequency < 30 || vblank_frequency > 360) {
return fail(tr("Vblank frequency must be between 30 and 360."));
}
if ((shader_log_direction == LogDirection::File && shader_log_folder.isEmpty()) ||
(printf_direction == LogDirection::File && printf_output_file.isEmpty()) ||
(command_buffer_dump_enabled && command_buffer_dump_folder.isEmpty())) {
return fail(tr("Enabled logging and command buffer dumps require an output path."));
}
return true;
}
@@ -19,6 +19,7 @@
#include <QLineEdit>
#include <QListView>
#include <QListWidget>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QResizeEvent>
@@ -126,6 +127,7 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
setMinimumWidth(width());
InitGameDirectories();
m_ui->controller_group->setVisible(false);
m_ui->settings_button->setVisible(false);
connect(m_ui->ok_button, &QPushButton::clicked, this, &ConfigurationEditDialog::save);
connect(m_ui->cancel_button, &QPushButton::clicked, this, &QDialog::reject);
@@ -273,7 +275,9 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen);
m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
m_ui->comboBox_console_language->clear();
@@ -354,6 +358,11 @@ void ConfigurationEditDialog::InitGameDirectories() {
void ConfigurationEditDialog::SetGlobalSettings(const QStringList& dirs) {
m_global_settings = true;
auto* menu = new QMenu(m_ui->settings_button);
menu->addAction(tr("Import..."), this, &ConfigurationEditDialog::ImportGameSettings);
menu->addAction(tr("Export..."), this, &ConfigurationEditDialog::ExportGameSettings);
m_ui->settings_button->setMenu(menu);
m_ui->settings_button->setVisible(true);
m_game_dirs_list->clear();
m_game_dirs_group->setMinimumWidth(GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH);
@@ -427,7 +436,9 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
info.readback_linear_images = ui.checkBox_readback->isChecked();
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked();
info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
info.console_language = ui.comboBox_console_language->currentIndex();
+139 -6
View File
@@ -24,9 +24,11 @@
#include <QDialog>
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QHeaderView>
#include <QIcon>
#include <QInputDialog>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
@@ -40,6 +42,7 @@
#include <QProcess>
#include <QPushButton>
#include <QRegularExpression>
#include <QSaveFile>
#include <QSet>
#include <QSettings>
#include <QSignalBlocker>
@@ -765,7 +768,7 @@ void ConfigurationListWidget::edit_configuration() {
auto info = CreateConfiguration(*item);
ConfigurationEditDialog dlg(*info, this);
dlg.setWindowTitle(tr("Edit game settings"));
dlg.setWindowTitle(tr("Edit game config"));
connect(&dlg, &ConfigurationEditDialog::PreviewControllerColor, this,
&ConfigurationListWidget::PreviewControllerColor);
@@ -789,8 +792,8 @@ void ConfigurationListWidget::delete_configuartion() {
return;
}
if (QMessageBox::Yes == QMessageBox::question(this, tr("Clear custom settings"),
tr("Do you want to clear custom settings?"))) {
if (QMessageBox::Yes == QMessageBox::question(this, tr("Clear game config"),
tr("Clear this game's config and use global settings?"))) {
delete m_custom_infos.take(item->GetInfo().game_path);
item->GetInfo().custom_settings = false;
WriteSettings();
@@ -810,6 +813,10 @@ void ConfigurationListWidget::edit_global_settings() {
dlg.SetGlobalSettings(m_game_dirs);
connect(&dlg, &ConfigurationEditDialog::PreviewControllerColor, this,
&ConfigurationListWidget::PreviewControllerColor);
connect(&dlg, &ConfigurationEditDialog::ImportGameSettings, this,
[this, &dlg]() { ImportGameSettings(&dlg); });
connect(&dlg, &ConfigurationEditDialog::ExportGameSettings, this,
[this, &dlg]() { ExportGameSettings(&dlg); });
if (dlg.exec() == QDialog::Accepted) {
m_global_info.CopyEmulatorSettingsFrom(info);
@@ -822,7 +829,133 @@ void ConfigurationListWidget::edit_global_settings() {
ScanGameDirectory();
}
}
emit Select();
SelectItem(m_ui->cfgs_list->currentItem());
}
static bool IsGameSettingsTitleId(const QString& title_id) {
static const QRegularExpression pattern(QStringLiteral("\\APPSA[0-9]{5}\\z"));
return pattern.match(title_id).hasMatch();
}
void ConfigurationListWidget::ImportGameSettings(QWidget* parent) {
const auto path = QFileDialog::getOpenFileName(parent, tr("Import game configs"), {},
tr("JSON files (*.json)"));
if (path.isEmpty()) {
return;
}
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::warning(parent, tr("Import failed"), file.errorString());
return;
}
const auto document = QJsonDocument::fromJson(file.readAll());
if (!document.isObject() || document.object().isEmpty()) {
QMessageBox::warning(
parent, tr("Import failed"),
tr("Expected a JSON object containing game configs keyed by PPSA code."));
return;
}
const auto settings = document.object();
for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) {
Configuration info;
QString error;
if (!IsGameSettingsTitleId(it.key()) || !it.value().isObject() ||
it.value().toObject().isEmpty() ||
!info.SetGameSettings(it.value().toObject(), error)) {
QMessageBox::warning(parent, tr("Import failed"),
tr("Invalid game config for %1. %2").arg(it.key(), error));
return;
}
}
struct ImportedGame {
ConfigurationItem* item;
std::unique_ptr<Configuration> info;
};
std::vector<ImportedGame> imported;
auto unmatched = settings.keys();
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); ++index) {
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
const auto title_id = item->GetInfo().title_id.trimmed().toUpper();
if (!settings.contains(title_id)) {
continue;
}
auto info = CreateConfiguration(*item);
QString error;
if (!info->SetGameSettings(settings.value(title_id).toObject(), error)) {
QMessageBox::warning(parent, tr("Import failed"),
tr("Invalid game config for %1. %2").arg(title_id, error));
return;
}
info->custom_settings = true;
imported.push_back({item, std::move(info)});
unmatched.removeAll(title_id);
}
auto message = imported.empty() ? tr("No matching games found in the library.")
: tr("Configs will be updated for the following games:\n");
for (const auto& game: imported) {
message += tr("\n%1 — %2").arg(game.info->title_id.trimmed().toUpper(), game.info->name);
}
if (!unmatched.isEmpty()) {
message += tr("\nGames not found: %1").arg(unmatched.join(QStringLiteral(", ")));
}
if (imported.empty()) {
QMessageBox::information(parent, tr("Import game configs"), message);
return;
}
QMessageBox confirmation(QMessageBox::Question, tr("Import game configs"), message,
QMessageBox::Apply | QMessageBox::Cancel, parent);
confirmation.setTextFormat(Qt::PlainText);
confirmation.setDefaultButton(QMessageBox::Cancel);
if (confirmation.exec() != QMessageBox::Apply) {
return;
}
for (auto& [item, info]: imported) {
const auto game_path = info->game_path;
delete m_custom_infos.take(game_path);
m_custom_infos.insert(game_path, info.release());
item->GetInfo().custom_settings = true;
item->Update();
}
WriteSettings();
}
void ConfigurationListWidget::ExportGameSettings(QWidget* parent) const {
QMap<QString, const ConfigurationItem*> games;
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); ++index) {
const auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
const auto& game = item->GetInfo();
const auto title_id = game.title_id.trimmed().toUpper();
if (m_custom_infos.contains(game.game_path) && IsGameSettingsTitleId(title_id)) {
games.insert(tr("%1 — %2 (%3)").arg(title_id, game.name, game.game_path), item);
}
}
if (games.isEmpty()) {
QMessageBox::information(parent, tr("Export game config"),
tr("No game configs with a PPSA code were found."));
return;
}
bool accepted = false;
const auto selected = QInputDialog::getItem(parent, tr("Export game config"), tr("Game:"),
games.keys(), 0, false, &accepted);
if (!accepted) {
return;
}
const auto& game = games.value(selected)->GetInfo();
const auto title_id = game.title_id.trimmed().toUpper();
const auto path =
QFileDialog::getSaveFileName(parent, tr("Export game config"),
title_id + QStringLiteral(".json"), tr("JSON files (*.json)"));
if (path.isEmpty()) {
return;
}
const auto settings =
QJsonObject::fromVariantMap(m_custom_infos.value(game.game_path)->GameSettings());
const auto data =
QJsonDocument(QJsonObject {{title_id, settings}}).toJson(QJsonDocument::Indented);
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) {
QMessageBox::warning(parent, tr("Export failed"), file.errorString());
}
}
void ConfigurationListWidget::edit_input_mapping() {
@@ -1040,11 +1173,11 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
tr("Remove save data..."), this, SLOT(remove_save_data()));
menu.addSeparator();
QAction* action_edit =
menu.addAction(style()->standardIcon(QStyle::SP_FileIcon), tr("Edit game settings..."),
menu.addAction(style()->standardIcon(QStyle::SP_FileIcon), tr("Edit game config..."),
this, SLOT(edit_configuration()));
QAction* action_delete =
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
tr("Clear custom settings"), this, SLOT(delete_configuartion()));
tr("Clear game config"), this, SLOT(delete_configuartion()));
if (item == nullptr) {
menu.addSeparator();
+2
View File
@@ -245,7 +245,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--hide-cursor";
}
args << "--readback-linear-images" << BoolArg(info.readback_linear_images);
args << "--sync-raw-image-buffers" << BoolArg(info.sync_raw_image_buffers);
args << "--trophy-notifications" << BoolArg(info.trophy_enabled);
args << "--skip-notice-screen" << BoolArg(info.skip_notice_screen);
if (info.tessellation_enabled) {
args << "--tessellation";
}
+130 -39
View File
@@ -9,6 +9,7 @@
#include <QDateTime>
#include <QDialogButtonBox>
#include <QFileInfo>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QImage>
#include <QLabel>
@@ -18,6 +19,7 @@
#include <QPainterPath>
#include <QPixmap>
#include <QProgressBar>
#include <QPushButton>
#include <QRegularExpression>
#include <QSize>
#include <QStringList>
@@ -25,6 +27,8 @@
#include <QTabWidget>
#include <QVBoxLayout>
#include <utility>
namespace {
static const QRegularExpression TrophyFilePattern(QStringLiteral("^trophy(\\d+)\\.ucp$"));
@@ -54,17 +58,115 @@ static QString GradeToText(int grade) {
}
}
static QString TrophyTooltip(const Common::Trophies::Trophy& trophy) {
QStringList lines {QString::fromStdString(trophy.name),
QString::fromStdString(trophy.description),
QObject::tr("Grade: %1").arg(GradeToText(trophy.grade))};
if (trophy.hidden) {
lines.append(QObject::tr("Hidden trophy"));
QString TrophyName(int id, const Common::Trophies::Trophy& trophy, bool masked) {
return masked ? QObject::tr("Hidden trophy")
: trophy.name.empty() ? QObject::tr("Trophy %1").arg(id)
: QString::fromStdString(trophy.name);
}
QPixmap TrophyIcon(const Common::Trophies::Trophy& trophy, bool unlocked, bool masked, int size) {
QPixmap pixmap;
if (masked) {
pixmap = QPixmap(QStringLiteral(":/icons/hidden-trophy.png"));
} else if (pixmap.loadFromData(reinterpret_cast<const uchar*>(trophy.icon_png.data()),
static_cast<uint>(trophy.icon_png.size())) &&
!unlocked) {
auto image = pixmap.toImage().convertToFormat(QImage::Format_ARGB32);
for (int y = 0; y < image.height(); ++y) {
auto* line = reinterpret_cast<QRgb*>(image.scanLine(y));
for (int x = 0; x < image.width(); ++x) {
const int gray = qGray(line[x]);
line[x] = qRgba(gray, gray, gray, qAlpha(line[x]));
}
}
pixmap = QPixmap::fromImage(image);
}
if (trophy.has_reward && !trophy.reward.empty()) {
lines.append(QObject::tr("Reward: %1").arg(QString::fromStdString(trophy.reward)));
return pixmap.isNull()
? pixmap
: pixmap.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
QString TrophyEarnedDate(int id, const Common::Trophies::UnlockData& unlocks) {
const auto found = unlocks.timestamps.find(id);
if (found == unlocks.timestamps.end()) {
return {};
}
return lines.join(QLatin1Char('\n'));
return QDateTime::fromMSecsSinceEpoch(
static_cast<qint64>((found->second - Common::Trophies::UnixEpochTick) / 1000))
.toLocalTime()
.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
}
void ShowTrophyInspector(QWidget* parent, int id, const Common::Trophies::Trophy& trophy,
bool unlocked, const QString& earned_date) {
QDialog dialog(parent);
dialog.setWindowTitle(QObject::tr("Trophy Details"));
dialog.resize(640, 380);
auto* layout = new QVBoxLayout(&dialog);
auto* top = new QHBoxLayout;
auto* icon = new QLabel(&dialog);
icon->setFixedSize(148, 148);
icon->setAlignment(Qt::AlignCenter);
auto* text_box = new QVBoxLayout;
auto* name = new QLabel(&dialog);
name->setTextFormat(Qt::PlainText);
name->setStyleSheet(QStringLiteral("font-size: 26px;"));
name->setWordWrap(true);
auto* subtitle = new QLabel(QObject::tr("Hidden trophy"), &dialog);
subtitle->setStyleSheet(QStringLiteral("font-size: 16px;"));
auto* reveal = new QPushButton(QObject::tr("Show Hidden Trophy"), &dialog);
text_box->addWidget(name);
text_box->addWidget(subtitle);
text_box->addWidget(reveal, 0, Qt::AlignLeft);
text_box->addStretch(1);
top->addWidget(icon);
top->addSpacing(16);
top->addLayout(text_box, 1);
layout->addLayout(top);
auto* grid = new QGridLayout;
grid->setColumnStretch(1, 1);
const auto make_row = [&](const QString& label, const QString& text = {}) {
auto* key = new QLabel(label, &dialog);
auto* value = new QLabel(text, &dialog);
key->setStyleSheet(QStringLiteral("font-size: 17px;"));
value->setStyleSheet(QStringLiteral("font-size: 17px;"));
value->setTextFormat(Qt::PlainText);
value->setWordWrap(true);
const int row = grid->rowCount();
grid->addWidget(key, row, 0);
grid->addWidget(value, row, 1);
return value;
};
auto* grade = make_row(QObject::tr("Grade"));
make_row(QObject::tr("Status"), unlocked ? QObject::tr("Earned") : QObject::tr("Not earned"));
make_row(QObject::tr("Earned date"),
unlocked && !earned_date.isEmpty() ? earned_date : QStringLiteral("-"));
auto* details = make_row(QObject::tr("Details"));
auto* reward =
trophy.has_reward && !trophy.reward.empty() ? make_row(QObject::tr("Reward")) : nullptr;
layout->addLayout(grid);
layout->addStretch(1);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dialog);
QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
layout->addWidget(buttons);
const auto refresh = [&](bool masked) {
icon->setPixmap(TrophyIcon(trophy, unlocked, masked, 148));
name->setText(TrophyName(id, trophy, masked));
subtitle->setVisible(trophy.hidden && !masked);
reveal->setVisible(masked);
grade->setText(masked ? QObject::tr("Hidden trophy") : GradeToText(trophy.grade));
details->setText(masked || trophy.description.empty()
? QStringLiteral("-")
: QString::fromStdString(trophy.description));
if (reward != nullptr) {
reward->setText(masked ? QStringLiteral("-") : QString::fromStdString(trophy.reward));
}
};
QObject::connect(reveal, &QPushButton::clicked, &dialog, [&] { refresh(false); });
refresh(trophy.hidden && !unlocked);
dialog.exec();
}
} // namespace
@@ -337,13 +439,13 @@ bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runt
}
QStringList errors;
for (const auto& file: trophy_files) {
const auto package =
auto package =
Common::Trophies::LoadPackage(GameContent::ToPath(file), info.console_language);
if (package.trophies.empty()) {
errors.append(tr("Could not read trophy package %1.").arg(QFileInfo(file).fileName()));
continue;
}
const auto unlocks = LoadUnlocks(info, runtime_directory, file);
auto unlocks = LoadUnlocks(info, runtime_directory, file);
const auto counts = Common::Trophies::GetProgress(package, unlocks);
auto* page = new QWidget(m_tabs);
auto* page_layout = new QVBoxLayout(page);
@@ -395,8 +497,10 @@ bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runt
const auto hidden_locked = trophy.hidden && !trophy_unlocked;
auto* item = new QListWidgetItem(list);
item->setSizeHint(QSize(900, 96));
item->setData(Qt::UserRole, id);
auto* card = new QWidget(list);
card->setAttribute(Qt::WA_TransparentForMouseEvents);
card->setStyleSheet(
QStringLiteral("QWidget#card { background: palette(base); border-radius: 12px; }"));
card->setObjectName(QStringLiteral("card"));
@@ -404,26 +508,12 @@ bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runt
auto* icon = new QLabel(card);
icon->setFixedSize(76, 76);
icon->setAlignment(Qt::AlignCenter);
QPixmap pixmap;
if (hidden_locked) {
pixmap = QPixmap(QStringLiteral(":/icons/hidden-trophy.png"));
} else if (pixmap.loadFromData(reinterpret_cast<const uchar*>(trophy.icon_png.data()),
static_cast<uint>(trophy.icon_png.size())) &&
!trophy_unlocked) {
pixmap =
QPixmap::fromImage(pixmap.toImage().convertToFormat(QImage::Format_Grayscale8));
}
if (!pixmap.isNull()) {
icon->setPixmap(
pixmap.scaled(76, 76, Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
icon->setPixmap(TrophyIcon(trophy, trophy_unlocked, hidden_locked, 76));
row->addWidget(icon);
auto* left = new QVBoxLayout;
const auto name_text = hidden_locked ? tr("Hidden trophy")
: trophy.name.empty() ? tr("Trophy %1").arg(id)
: QString::fromStdString(trophy.name);
auto* name = new QLabel(name_text, card);
auto* left = new QVBoxLayout;
auto* name = new QLabel(TrophyName(id, trophy, hidden_locked), card);
name->setTextFormat(Qt::PlainText);
name->setStyleSheet(QStringLiteral("font-size: 20px; font-weight: bold;"));
left->addWidget(name);
if (!hidden_locked) {
@@ -440,15 +530,9 @@ bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runt
row->addLayout(left, 2);
auto* right = new QVBoxLayout;
QString date_text;
if (const auto found = unlocks.timestamps.find(id); found != unlocks.timestamps.end()) {
date_text = QDateTime::fromMSecsSinceEpoch(
static_cast<qint64>(
(found->second - Common::Trophies::UnixEpochTick) / 1000))
.toLocalTime()
.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
} else if (trophy_unlocked) {
auto* right = new QVBoxLayout;
auto date_text = TrophyEarnedDate(id, unlocks);
if (date_text.isEmpty() && trophy_unlocked) {
date_text = tr("Earned");
}
auto* date = new QLabel(date_text, card);
@@ -456,15 +540,22 @@ bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runt
date->setStyleSheet(QStringLiteral("font-size: 15px;"));
auto* detail = new QLabel(
hidden_locked ? QString {} : QString::fromStdString(trophy.description), card);
detail->setTextFormat(Qt::PlainText);
detail->setWordWrap(true);
detail->setAlignment(Qt::AlignLeft | Qt::AlignTop);
detail->setStyleSheet(QStringLiteral("font-size: 17px;"));
right->addWidget(date);
right->addWidget(detail, 1);
row->addLayout(right, 3);
card->setToolTip(hidden_locked ? name_text : TrophyTooltip(trophy));
list->setItemWidget(item, card);
}
connect(list, &QListWidget::itemClicked, this,
[this, trophies = std::move(package.trophies),
unlocks = std::move(unlocks)](QListWidgetItem* item) {
const int id = item->data(Qt::UserRole).toInt();
ShowTrophyInspector(this, id, trophies.at(id), unlocks.unlocked.contains(id),
TrophyEarnedDate(id, unlocks));
});
m_tabs->addTab(page, QString::fromStdString(package.title));
}
m_tabs->tabBar()->setVisible(m_tabs->count() > 1);
+14
View File
@@ -2388,6 +2388,12 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer*
return cmd;
}
uint64_t KYTY_SYSV_ABI AgcDcbSetShRegistersIndirectGetSize() {
PRINT_NAME();
return 5u * sizeof(uint32_t);
}
uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegistersIndirect(CommandBuffer* buf,
const volatile ShaderRegister* regs,
uint32_t num_regs) {
@@ -3211,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) {
+2
View File
@@ -137,6 +137,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirect(CommandBuffer*
uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer* buf,
const volatile ShaderRegister* regs,
uint32_t num_regs);
uint64_t KYTY_SYSV_ABI AgcDcbSetShRegistersIndirectGetSize();
uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegistersIndirect(CommandBuffer* buf,
const volatile ShaderRegister* regs,
uint32_t num_regs);
@@ -205,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;
+89
View File
@@ -67,8 +67,13 @@ struct PadTriggerEffectParam {
PadTriggerEffectCommand command[2];
};
struct PadTriggerEffectStateInformation {
int32_t state[2];
};
static_assert(sizeof(PadTriggerEffectCommand) == 56);
static_assert(sizeof(PadTriggerEffectParam) == 120);
static_assert(sizeof(PadTriggerEffectStateInformation) == 8);
struct DualSenseEffects {
uint8_t enable_bits;
@@ -118,6 +123,7 @@ public:
int GetActiveControllerId();
void SetLightBar(uint8_t r, uint8_t g, uint8_t b);
bool SetTriggerEffect(const PadTriggerEffectParam& param);
void GetTriggerEffectState(PadTriggerEffectStateInformation* info);
void CycleSetting(Setting setting);
float GetSettingScale(Setting setting) const;
void ReadState(ControllerState* state, bool* flag, int* count);
@@ -131,6 +137,7 @@ private:
// Apply cached output without changing its game-requested lifetime; caller holds m_mutex.
void ApplyVibration();
bool SendTriggerEffect(const PadTriggerEffectParam& param);
void UpdateTriggerEffectState();
Common::Mutex m_mutex;
std::vector<int> m_connected_ids;
@@ -152,6 +159,7 @@ private:
std::array<uint8_t, 2> m_vibration {};
uint64_t m_vibration_until = 0;
PadTriggerEffectParam m_trigger_effect {};
PadTriggerEffectStateInformation m_trigger_state {};
// Setting changes share the output lock; audio only needs an atomic scale snapshot.
std::array<std::atomic<uint32_t>, 3> m_setting_steps {};
};
@@ -330,6 +338,51 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command,
}
}
enum TriggerState : int32_t {
TRIGGER_STATE_OFF = 0,
TRIGGER_STATE_FEEDBACK_STANDBY = 1,
TRIGGER_STATE_FEEDBACK_ACTIVE = 2,
TRIGGER_STATE_WEAPON_STANDBY = 3,
TRIGGER_STATE_WEAPON_PULLING = 4,
TRIGGER_STATE_WEAPON_FIRED = 5,
TRIGGER_STATE_VIBRATION_STANDBY = 6,
TRIGGER_STATE_VIBRATION_ACTIVE = 7,
};
static int32_t TriggerEffectState(const PadTriggerEffectCommand& command, int value,
int32_t previous) {
// Trigger positions use nonuniform travel ranges, with endpoints at 0 and 255.
const int pos = value == 0 ? 0 : value == 255 ? 9 : 1 + value / 32;
switch (command.mode) {
case 1: // feedback: resistance from position data[0], strength data[1]
return command.data[1] != 0 && pos >= command.data[0] ? TRIGGER_STATE_FEEDBACK_ACTIVE
: TRIGGER_STATE_FEEDBACK_STANDBY;
case 2: // weapon: resistance from data[0], released at data[1]
if (value < command.data[0] * 32) {
return TRIGGER_STATE_WEAPON_STANDBY;
}
return previous == TRIGGER_STATE_WEAPON_FIRED ||
value >= std::min(command.data[1] * 32, 254)
? TRIGGER_STATE_WEAPON_FIRED
: TRIGGER_STATE_WEAPON_PULLING;
case 3: // vibration from position data[0], amplitude data[1], frequency data[2]
return command.data[1] != 0 && command.data[2] != 0 && pos >= command.data[0]
? TRIGGER_STATE_VIBRATION_ACTIVE
: TRIGGER_STATE_VIBRATION_STANDBY;
case 4: // multiple-position feedback: strength per position
return command.data[pos] != 0 ? TRIGGER_STATE_FEEDBACK_ACTIVE
: TRIGGER_STATE_FEEDBACK_STANDBY;
case 5: // slope feedback between data[0] and data[1]
return pos >= command.data[0] && pos < command.data[1] ? TRIGGER_STATE_FEEDBACK_ACTIVE
: TRIGGER_STATE_FEEDBACK_STANDBY;
case 6: // multiple-position vibration: frequency data[0], amplitude per position
return command.data[0] != 0 && command.data[1 + pos] != 0
? TRIGGER_STATE_VIBRATION_ACTIVE
: TRIGGER_STATE_VIBRATION_STANDBY;
default: return TRIGGER_STATE_OFF;
}
}
void Initialize() {
EXIT_IF(g_controller != nullptr);
@@ -419,6 +472,7 @@ void GameController::CheckActive() {
m_vibration = {};
m_vibration_until = 0;
m_trigger_effect = {};
m_trigger_state = {};
}
void GameController::AddState() {
@@ -466,6 +520,7 @@ void GameController::Axis(int id, Controller::Axis axis, int value) {
}
if (trigger != 0) {
m_state.buttons = value > 0 ? m_state.buttons | trigger : m_state.buttons & ~trigger;
UpdateTriggerEffectState();
}
AddState();
@@ -622,6 +677,7 @@ void GameController::ResetInputState() {
m_state.buttons = 0;
std::fill_n(m_state.axes, 4, 128);
std::fill_n(m_state.axes + 4, 2, 0);
UpdateTriggerEffectState();
for (auto& touch: m_state.touch) {
touch = {};
}
@@ -730,9 +786,25 @@ bool GameController::SetTriggerEffect(const PadTriggerEffectParam& param) {
m_trigger_effect.command[i] = param.command[i];
}
}
UpdateTriggerEffectState();
return true;
}
void GameController::GetTriggerEffectState(PadTriggerEffectStateInformation* info) {
Common::LockGuard lock(m_mutex);
*info = m_trigger_state;
}
void GameController::UpdateTriggerEffectState() {
// Track every input event so a weapon remains fired even when the game polls after release
// starts.
for (int i = 0; i < 2; i++) {
m_trigger_state.state[i] = TriggerEffectState(
m_trigger_effect.command[i], m_state.axes[static_cast<int>(Axis::TriggerLeft) + i],
m_trigger_state.state[i]);
}
}
bool GameController::SendTriggerEffect(const PadTriggerEffectParam& param) {
if ((param.trigger_mask & ~0x03u) != 0) {
return false;
@@ -1098,4 +1170,21 @@ int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* p
return g_controller->SetTriggerEffect(*param) ? OK : PAD_ERROR_INVALID_ARG;
}
int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle, PadTriggerEffectStateInformation* info) {
PRINT_NAME();
LOGF("\t handle = %d\n", handle);
if (handle != 1) {
return PAD_ERROR_INVALID_HANDLE;
}
if (info == nullptr) {
return PAD_ERROR_INVALID_ARG;
}
g_controller->GetTriggerEffectState(info);
return OK;
}
} // namespace Libs::Controller
+2
View File
@@ -53,6 +53,7 @@ struct PadData;
struct PadVibrationParam;
struct PadLightBarParam;
struct PadTriggerEffectParam;
struct PadTriggerEffectStateInformation;
inline int controller_get_axis(int min, int max, int value) {
int v = (255 * (value - min)) / (max - min);
@@ -92,6 +93,7 @@ int KYTY_SYSV_ABI PadReadState(int handle, PadData* data);
int KYTY_SYSV_ABI PadRead(int handle, PadData* data, int num);
int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param);
int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* param);
int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle, PadTriggerEffectStateInformation* info);
int KYTY_SYSV_ABI PadResetLightBar(int handle);
int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param);
+2 -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);
+3
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);
@@ -95,6 +96,7 @@ LIB_DEFINE(InitAgcDriver_1) {
LIB_FUNC("w4-d0n60hdo", Gen5::AgcDcbSetUcRegisterDirect);
LIB_FUNC("ZvwO9euwYzc", Gen5::AgcDcbSetCxRegistersIndirect);
LIB_FUNC("-HOOCn0JY48", Gen5::AgcDcbSetShRegistersIndirect);
LIB_FUNC("nNlUtdDDvZ0", Gen5::AgcDcbSetShRegistersIndirectGetSize);
LIB_FUNC("hvUfkUIQcOE", Gen5::AgcDcbSetUcRegistersIndirect);
LIB_FUNC("GIIW2J37e70", Gen5::AgcDcbSetIndexSize);
LIB_FUNC("l4fM9K-Lyks", Gen5::AgcDcbSetIndexBuffer);
@@ -155,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);
}
+60 -43
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"
@@ -52,6 +53,7 @@ struct ResolvedPathInfo {
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;
@@ -92,15 +94,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,18 +113,6 @@ 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) {
std::scoped_lock lock(g_mutex);
const auto it = g_files.find(file_id);
@@ -213,7 +194,6 @@ 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);
if (Common::File::IsDirectoryExisting(real_path)) {
@@ -232,7 +212,14 @@ 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;
}
@@ -240,8 +227,6 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
if (log_missing) {
LOGF("\tAPR resolve missing path: %s -> %s\n", guest_path, 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) {
@@ -665,10 +650,15 @@ struct CommandBufferState {
uint64_t address = 0;
uint64_t value = 0;
};
struct WaitAddressCommand {
uint64_t address;
struct WaitCommand {
uint64_t source;
uint64_t value;
uint8_t compare;
bool is_counter;
};
struct WriteCounterCommand {
uint8_t index;
uint32_t value;
};
struct AmmMapCommand {
AmmCommandKind kind = AmmCommandKind::MapAuto;
@@ -680,7 +670,7 @@ struct CommandBufferState {
uint8_t gpu_mask_id = 0;
};
using CommandData = std::variant<ReadFileCommand, KernelEventCommand, WriteAddressCommand,
AmmMapCommand, WaitAddressCommand>;
AmmMapCommand, WaitCommand, WriteCounterCommand>;
struct Command {
uint64_t record_offset;
CommandData data;
@@ -705,6 +695,8 @@ static_assert(sizeof(AmmUsageStatsData) == 0x18);
static std::mutex g_command_buffer_mutex;
static std::unordered_map<uint64_t, CommandBufferState> g_command_buffers;
static std::unordered_map<uint64_t, uint64_t> g_command_buffer_aliases;
// AMM and APR share 256 32-bit counters; only the first 128 are application-writable.
static std::array<std::atomic<uint32_t>, 256> g_counters {};
using CommandBufferIterator = std::unordered_map<uint64_t, CommandBufferState>::iterator;
static void RegisterCommandBufferAliasLocked(uint64_t command_buffer, uint64_t buffer) {
@@ -1207,6 +1199,9 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) {
const auto& command = *payload;
std::atomic_ref(*reinterpret_cast<uint64_t*>(command.address))
.store(command.value, std::memory_order_release);
} else if (const auto* command =
std::get_if<CommandBufferState::WriteCounterCommand>(&entry.data)) {
g_counters[command->index].store(command->value, std::memory_order_release);
} else if (const auto* payload = std::get_if<CommandBufferState::AmmMapCommand>(&entry.data)) {
const auto& command = *payload;
int result = OK;
@@ -1235,17 +1230,20 @@ struct Submission {
uint32_t id = 0;
};
static bool IsWaitSatisfied(const CommandBufferState::WaitAddressCommand& wait) {
const uint64_t value =
std::atomic_ref(*reinterpret_cast<uint64_t*>(wait.address)).load(std::memory_order_acquire);
static bool IsWaitSatisfied(const CommandBufferState::WaitCommand& wait) {
const uint64_t value = wait.is_counter
? g_counters[wait.source].load(std::memory_order_acquire)
: std::atomic_ref(*reinterpret_cast<uint64_t*>(wait.source))
.load(std::memory_order_acquire);
const uint64_t sign = uint64_t {1} << (wait.is_counter ? 31 : 63);
switch (wait.compare) {
case 0: return value == wait.value;
case 1: return value > wait.value;
case 2: return value < wait.value;
case 3: return value != wait.value;
case 4: return static_cast<int64_t>(value - wait.value) >= 0;
case 5: return static_cast<int64_t>(value) > static_cast<int64_t>(wait.value);
default: return static_cast<int64_t>(value) < static_cast<int64_t>(wait.value);
case 4: return ((value - wait.value) & sign) == 0;
case 5: return (value ^ sign) > (wait.value ^ sign);
default: return (value ^ sign) < (wait.value ^ sign);
}
}
@@ -1279,7 +1277,7 @@ private:
pending = true;
auto& submission = queue.front();
if (submission.cursor < submission.commands.size()) {
const auto* wait = std::get_if<CommandBufferState::WaitAddressCommand>(
const auto* wait = std::get_if<CommandBufferState::WaitCommand>(
&submission.commands[submission.cursor].data);
if (wait != nullptr && !IsWaitSatisfied(*wait)) {
continue;
@@ -1927,18 +1925,27 @@ static int KYTY_SYSV_ABI CommandBufferWaitOnAddress(void* command_b
return LibKernel::KERNEL_ERROR_EINVAL;
}
return AppendCommandRecord(reinterpret_cast<uint64_t>(command_buffer),
CommandBufferState::WaitAddressCommand {
reinterpret_cast<uint64_t>(address), value, compare},
CommandBufferState::WaitCommand {
reinterpret_cast<uint64_t>(address), value, compare, false},
0x20)
? OK
: LibKernel::KERNEL_ERROR_EBUSY;
}
static int KYTY_SYSV_ABI CommandBufferWaitOnCounter(void* command_buffer, uint8_t, uint8_t,
uint64_t, uint8_t, uint8_t, uint64_t, uint8_t) {
static int KYTY_SYSV_ABI CommandBufferWaitOnCounter(void* command_buffer, uint8_t index,
uint8_t access, uint64_t value, uint8_t compare,
uint8_t mask_op, uint64_t, uint8_t flush) {
PRINT_NAME();
return AppendNoOpCommand(command_buffer, 0x20);
if (command_buffer == nullptr || access > 7 || compare > 6 || mask_op > 1 || flush > 1) {
return LibKernel::KERNEL_ERROR_EINVAL;
}
EXIT_NOT_IMPLEMENTED(access != 1 || mask_op != 0 || flush != 0 || value > UINT32_MAX);
return AppendCommandRecord(reinterpret_cast<uint64_t>(command_buffer),
CommandBufferState::WaitCommand {index, value, compare, true},
0x20)
? OK
: LibKernel::KERNEL_ERROR_EBUSY;
}
static int AppendWriteAddressCommand(void* command_buffer, volatile uint64_t* address,
@@ -1970,11 +1977,21 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressOnCompletion(void*
return AppendWriteAddressCommand(command_buffer, address, value);
}
static int KYTY_SYSV_ABI CommandBufferWriteCounter(void* command_buffer, uint8_t, uint8_t, uint64_t,
uint8_t, uint32_t) {
static int KYTY_SYSV_ABI CommandBufferWriteCounter(void* command_buffer, uint8_t index,
uint8_t access, uint64_t value,
uint8_t operation, uint32_t mode) {
PRINT_NAME();
return AppendNoOpCommand(command_buffer, 0x20);
if (command_buffer == nullptr || index >= 128 || access > 7 || operation > 4 || mode > 1) {
return LibKernel::KERNEL_ERROR_EINVAL;
}
EXIT_NOT_IMPLEMENTED(access != 1 || operation != 0 || mode != 0);
return AppendCommandRecord(reinterpret_cast<uint64_t>(command_buffer),
CommandBufferState::WriteCounterCommand {
index, static_cast<uint32_t>(value)},
0x20)
? OK
: LibKernel::KERNEL_ERROR_EBUSY;
}
static int KYTY_SYSV_ABI CommandBufferWriteCounterOnCompletion(void* command_buffer, uint8_t,
+2
View File
@@ -6,6 +6,7 @@
#include "common/stringUtils.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "libs/systemService.h"
#include "loader/symbolDatabase.h"
#include "loader/systemContent.h"
@@ -96,6 +97,7 @@ int KYTY_SYSV_ABI AppContentInitialize(const AppContentInitParam* init_param,
LOGF("\t TITLE_ID missing\n");
}
SystemService::NotifyEntitlementUpdate();
return OK;
}
+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;
}
+63 -29
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);
@@ -2831,6 +2838,8 @@ constexpr int32_t KERNEL_AIO_STATE_ABORTED = 4;
constexpr int32_t KERNEL_AIO_STATE_NOTIFIED = 0x10000;
constexpr int32_t KERNEL_AIO_MAX_QUEUE = 512;
constexpr int32_t KERNEL_AIO_MAX_REQUESTS = 128;
constexpr uint32_t KERNEL_AIO_WAIT_AND = 1;
constexpr uint32_t KERNEL_AIO_WAIT_OR = 2;
struct KernelAioResult {
int64_t return_value;
@@ -2933,26 +2942,28 @@ int KYTY_SYSV_ABI KernelAioSubmitWriteCommands(KernelAioRwRequest* req, int32_t
return OK;
}
static int32_t kernel_aio_get_state(int32_t id, bool notify) {
if (!kernel_aio_is_valid_id(id)) {
return LibKernel::KERNEL_ERROR_ESRCH;
}
auto current = g_kernel_aio_state[id].load(std::memory_order_acquire);
while (notify && (current == KERNEL_AIO_STATE_COMPLETED || current == KERNEL_AIO_STATE_ABORTED)) {
if (g_kernel_aio_state[id].compare_exchange_weak(current, current | KERNEL_AIO_STATE_NOTIFIED,
std::memory_order_acq_rel)) {
break;
}
}
return current == 0 ? LibKernel::KERNEL_ERROR_ESRCH : current;
}
int KYTY_SYSV_ABI KernelAioPollRequest(int32_t id, int32_t* state) {
PRINT_NAME();
if (state == nullptr) {
return LibKernel::KERNEL_ERROR_EFAULT;
}
if (!kernel_aio_is_valid_id(id)) {
*state = LibKernel::KERNEL_ERROR_ESRCH;
return OK;
}
auto current = g_kernel_aio_state[id].load(std::memory_order_acquire);
while (current == KERNEL_AIO_STATE_COMPLETED || current == KERNEL_AIO_STATE_ABORTED) {
if (g_kernel_aio_state[id].compare_exchange_weak(current, current | KERNEL_AIO_STATE_NOTIFIED,
std::memory_order_acq_rel)) {
break;
}
}
*state = current == 0 ? LibKernel::KERNEL_ERROR_ESRCH : current;
*state = kernel_aio_get_state(id, true);
return OK;
}
@@ -2969,31 +2980,53 @@ int KYTY_SYSV_ABI KernelAioPollRequests(int32_t* ids, int32_t num, int32_t* stat
return OK;
}
int KYTY_SYSV_ABI KernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec) {
int KYTY_SYSV_ABI KernelAioWaitRequests(int32_t* ids, int32_t num, int32_t* states, uint32_t mode,
uint32_t* usec) {
PRINT_NAME();
if (state == nullptr) {
if (ids == nullptr || states == nullptr) {
return LibKernel::KERNEL_ERROR_EFAULT;
}
if (!kernel_aio_is_valid_id(id)) {
return KernelAioPollRequest(id, state);
if (num <= 0 || num > KERNEL_AIO_MAX_REQUESTS ||
(num != 1 && mode != KERNEL_AIO_WAIT_AND && mode != KERNEL_AIO_WAIT_OR)) {
return LibKernel::KERNEL_ERROR_EINVAL;
}
uint32_t waited = 0;
auto current = g_kernel_aio_state[id].load(std::memory_order_acquire);
while (current == KERNEL_AIO_STATE_PROCESSING) {
if (usec != nullptr && *usec != 0 && waited >= *usec) {
*state = current;
return LibKernel::KERNEL_ERROR_ETIMEDOUT;
const auto collect_states = [&](bool notify) {
int32_t finished = 0;
bool newly_complete = false;
for (int32_t i = 0; i < num; i++) {
const auto current = kernel_aio_get_state(ids[i], notify);
states[i] = current;
finished += current != KERNEL_AIO_STATE_PROCESSING;
newly_complete |= current == KERNEL_AIO_STATE_COMPLETED || current == KERNEL_AIO_STATE_ABORTED;
}
return finished == num || (mode == KERNEL_AIO_WAIT_OR && newly_complete);
};
const auto start = std::chrono::steady_clock::now();
for (;;) {
const bool complete = collect_states(false);
const auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - start).count();
const bool timed_out = usec != nullptr && elapsed >= *usec;
if (complete || timed_out) {
// A competing waiter can claim the newly completed request after the first scan.
if (collect_states(true)) {
if (usec != nullptr) {
*usec = elapsed < *usec ? *usec - static_cast<uint32_t>(elapsed) : 0;
}
return OK;
}
if (timed_out) {
return LibKernel::KERNEL_ERROR_ETIMEDOUT;
}
}
Common::Thread::SleepMicro(10);
waited += 10;
current = g_kernel_aio_state[id].load(std::memory_order_acquire);
}
}
return KernelAioPollRequest(id, state);
int KYTY_SYSV_ABI KernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec) {
return KernelAioWaitRequests(&id, 1, state, KERNEL_AIO_WAIT_AND, usec);
}
int KYTY_SYSV_ABI KernelAioDeleteRequest(int32_t id, int32_t* ret) {
@@ -3376,6 +3409,7 @@ LIB_DEFINE(InitLibKernel_1) {
LIB_FUNC("2pOuoWoCxdk", KernelAioPollRequest);
LIB_FUNC("o7O4z3jwKzo", KernelAioPollRequests);
LIB_FUNC("KOF-oJbQVvc", KernelAioWaitRequest);
LIB_FUNC("lgK+oIWkJyA", KernelAioWaitRequests);
LIB_FUNC("XQ8C8y+de+E", KernelAioSubmitWriteCommands);
LIB_FUNC("nu4a0-arQis", KernelAioInitializeParam);
LIB_FUNC("il03nluKfMk", LibKernel::KernelRaiseException);
+27 -2
View File
@@ -89,6 +89,10 @@ int KYTY_SYSV_ABI NetBind(int s, const void* addr, uint32_t addrlen) {
return FinishSocketCall(Net::Bind(s, addr, addrlen));
}
int KYTY_SYSV_ABI NetConnect(int s, const void* addr, uint32_t addrlen) {
return FinishSocketCall(Net::Connect(s, addr, addrlen));
}
int KYTY_SYSV_ABI NetListen(int s, int backlog) {
return FinishSocketCall(Net::Listen(s, backlog));
}
@@ -170,14 +174,24 @@ int KYTY_SYSV_ABI NetSocketClose(int s) {
return FinishNetCall(Net::SocketClose(s));
}
int KYTY_SYSV_ABI NetGetsockopt(int s, int level, int optname, void* optval, uint32_t* optlen) {
return FinishSocketCall(Net::Getsockopt(s, level, optname, optval, optlen));
}
int KYTY_SYSV_ABI NetSetsockopt(int s, int level, int optname, const void* optval,
uint32_t optlen) {
return FinishSocketCall(Net::Setsockopt(s, level, optname, optval, optlen));
}
int KYTY_SYSV_ABI NetSend(int s, const void* buf, size_t len, int flags) {
int KYTY_SYSV_ABI NetSendto(int s, const void* buf, size_t len, int flags, const void* addr,
uint32_t addrlen) {
const auto size = std::min<size_t>(len, std::numeric_limits<int>::max());
return FinishSocketCall(static_cast<int>(Net::Send(s, buf, size, flags | 0x20000)));
return FinishSocketCall(
static_cast<int>(Net::Sendto(s, buf, size, flags | 0x20000, addr, addrlen)));
}
int KYTY_SYSV_ABI NetSend(int s, const void* buf, size_t len, int flags) {
return NetSendto(s, buf, len, flags, nullptr, 0);
}
int KYTY_SYSV_ABI NetRecv(int s, void* buf, size_t len, int flags) {
@@ -185,6 +199,12 @@ int KYTY_SYSV_ABI NetRecv(int s, void* buf, size_t len, int flags) {
return FinishSocketCall(static_cast<int>(Net::Recv(s, buf, size, flags)));
}
int KYTY_SYSV_ABI NetRecvfrom(int s, void* buf, size_t len, int flags, void* addr,
uint32_t* addrlen) {
const auto size = std::min<size_t>(len, std::numeric_limits<int>::max());
return FinishSocketCall(static_cast<int>(Net::Recvfrom(s, buf, size, flags, addr, addrlen)));
}
uint32_t KYTY_SYSV_ABI NetHtonl(uint32_t host32) {
return ((host32 & 0x000000ffu) << 24u) | ((host32 & 0x0000ff00u) << 8u) |
((host32 & 0x00ff0000u) >> 8u) | ((host32 & 0xff000000u) >> 24u);
@@ -209,6 +229,7 @@ LIB_DEFINE(InitNet_1_Net) {
LIB_FUNC("HQOwnfMGipQ", LibNet::GetNetErrorAddr);
LIB_FUNC("PIWqhn9oSxc", LibNet::NetAccept);
LIB_FUNC("bErx49PgxyY", LibNet::NetBind);
LIB_FUNC("OXXX4mUk3uk", LibNet::NetConnect);
LIB_FUNC("dgJBaeJnGpo", LibNet::NetPoolCreate);
LIB_FUNC("K7RlrTkI-mw", LibNet::NetPoolDestroy);
LIB_FUNC("C4UgDHHPvdw", LibNet::NetResolverCreate);
@@ -229,9 +250,12 @@ LIB_DEFINE(InitNet_1_Net) {
LIB_FUNC("TSM6whtekok", LibNet::NetShutdown);
LIB_FUNC("Q4qBuN-c0ZM", LibNet::NetSocket);
LIB_FUNC("45ggEzakPJQ", LibNet::NetSocketClose);
LIB_FUNC("xphrZusl78E", LibNet::NetGetsockopt);
LIB_FUNC("2mKX2Spso7I", LibNet::NetSetsockopt);
LIB_FUNC("beRjXBn-z+o", LibNet::NetSend);
LIB_FUNC("gvD1greCu0A", LibNet::NetSendto);
LIB_FUNC("9wO9XrMsNhc", LibNet::NetRecv);
LIB_FUNC("304ooNZxWDY", LibNet::NetRecvfrom);
LIB_FUNC("9T2pDF2Ryqg", LibNet::NetHtonl);
LIB_FUNC("iWQWrwiSt8A", LibNet::NetHtons);
LIB_FUNC("pQGpHYopAIY", LibNet::NetNtohl);
@@ -1535,6 +1559,7 @@ LIB_DEFINE(InitNet_1_NpManager) {
LIB_FUNC("rbknaUjpqWo", NpManager::NpGetAccountIdA);
LIB_FUNC("JT+t00a3TxA", NpManager::NpGetAccountCountryA);
LIB_FUNC("+4DegjBqV1g", NpManager::NpGetAccountAge);
LIB_FUNC("3Tcz5bNCfZQ", NpManager::NpGetAccountLanguage2);
LIB_FUNC("GpLQDNKICac", NpManager::NpCreateRequest);
LIB_FUNC("eiqMCt9UshI", NpManager::NpCreateAsyncRequest);
LIB_FUNC("S7QTn72PrDw", NpManager::NpDeleteRequest);
+1 -21
View File
@@ -22,26 +22,6 @@ static int KYTY_SYSV_ABI PadSetVibrationMode(int handle, int mode) {
return 0;
}
struct PadTriggerEffectStateInformation {
int32_t state[2];
};
static int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle,
PadTriggerEffectStateInformation* info) {
PRINT_NAME();
LOGF("\t handle = %d\n", handle);
if (info == nullptr) {
return -2137653243; /* 0x80960005 */
}
info->state[0] = 0;
info->state[1] = 0;
return 0;
}
static int KYTY_SYSV_ABI PadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool enabled) {
PRINT_NAME();
@@ -214,7 +194,7 @@ LIB_DEFINE(InitPad_1) {
LIB_FUNC("yFVnOdGxvZY", Controller::PadSetVibration);
LIB_FUNC("W2G-yoyMF5U", PadSetVibrationMode);
LIB_FUNC("Yq0zOH7YNOM", PadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse);
LIB_FUNC("znaWI0gpuo8", PadGetTriggerEffectState);
LIB_FUNC("znaWI0gpuo8", Controller::PadGetTriggerEffectState);
LIB_FUNC("DscD1i9HX1w", Controller::PadResetLightBar);
LIB_FUNC("RR4novUEENY", Controller::PadSetLightBar);
LIB_FUNC("6ncge5+l5Qs", PadClose);
+25 -1
View File
@@ -6,6 +6,9 @@
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
#include <mutex>
#include <unordered_map>
namespace Libs {
LIB_VERSION("Sysmodule", 1, "Sysmodule", 1, 1);
@@ -17,6 +20,16 @@ int KYTY_SYSV_ABI KernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleI
namespace Sysmodule {
constexpr int SYSMODULE_ERROR_UNLOADED = static_cast<int>(0x805a1001u);
static std::mutex g_module_mutex;
static std::unordered_map<uint16_t, uint32_t> g_module_refs;
static void LoadModule(uint16_t id) {
std::scoped_lock lock(g_module_mutex);
++g_module_refs[id];
}
static KYTY_SYSV_ABI int SysmoduleGetModuleInfoForUnwind(uint64_t addr, int flags,
LibKernel::ModuleInfoForUnwind* info) {
return LibKernel::KernelGetModuleInfoForUnwind(addr, flags, info);
@@ -27,6 +40,7 @@ static KYTY_SYSV_ABI int SysmoduleLoadModule(uint16_t id) {
LOGF("\t id = %d\n", static_cast<int>(id));
LoadModule(id);
return 0;
}
@@ -35,6 +49,14 @@ static KYTY_SYSV_ABI int SysmoduleUnloadModule(uint16_t id) {
LOGF("\t id = %d\n", static_cast<int>(id));
std::scoped_lock lock(g_module_mutex);
const auto module = g_module_refs.find(id);
if (module == g_module_refs.end()) {
return SYSMODULE_ERROR_UNLOADED;
}
if (--module->second == 0) {
g_module_refs.erase(module);
}
return 0;
}
@@ -49,6 +71,7 @@ static KYTY_SYSV_ABI int SysmoduleLoadModuleInternalWithArg(uint16_t id, int arg
EXIT_IF(arg3 != 0);
EXIT_IF(ret == nullptr);
LoadModule(id);
*ret = 0;
return 0;
@@ -59,7 +82,8 @@ static KYTY_SYSV_ABI int SysmoduleIsLoaded(uint16_t id) {
LOGF("\t id = %d\n", static_cast<int>(id));
return 0;
std::scoped_lock lock(g_module_mutex);
return g_module_refs.contains(id) ? 0 : SYSMODULE_ERROR_UNLOADED;
}
} // namespace Sysmodule
+24 -2
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;
}
@@ -206,7 +228,7 @@ static int KYTY_SYSV_ABI SystemServiceGetNoticeScreenSkipFlag(bool* value) {
return SYSTEM_SERVICE_ERROR_PARAMETER;
}
*value = false;
*value = Config::SkipNoticeScreen();
return OK;
}
+109 -23
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;
@@ -992,6 +988,7 @@ static int ConvertHostSocketError(int error) {
case WSAENETUNREACH: posix_error = Posix::POSIX_ENETUNREACH; break;
case WSAENOTCONN: posix_error = Posix::POSIX_ENOTCONN; break;
case WSAENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
case WSAENOPROTOOPT: posix_error = Posix::POSIX_ENOPROTOOPT; break;
case WSAEOPNOTSUPP: posix_error = Posix::POSIX_EOPNOTSUPP; break;
case WSAEPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
case WSAESHUTDOWN: posix_error = Posix::POSIX_ESHUTDOWN; break;
@@ -1028,6 +1025,7 @@ static int ConvertHostSocketError(int error) {
case ENETUNREACH: posix_error = Posix::POSIX_ENETUNREACH; break;
case ENOTCONN: posix_error = Posix::POSIX_ENOTCONN; break;
case ENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
case ENOPROTOOPT: posix_error = Posix::POSIX_ENOPROTOOPT; break;
case EOPNOTSUPP: posix_error = Posix::POSIX_EOPNOTSUPP; break;
case EPIPE: posix_error = Posix::POSIX_EPIPE; break;
case EPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
@@ -1066,6 +1064,26 @@ static int ConvertSocketOptionLevel(int level) {
return (level == 0xffff ? SOL_SOCKET : level);
}
static int ConvertSocketOptionName(int level, int option) {
if (level == 0xffff) {
switch (option) {
case 0x0020: return SO_BROADCAST;
case 0x1001: return SO_SNDBUF;
case 0x1002: return SO_RCVBUF;
case 0x1007: return SO_ERROR;
case 0x1105: return SO_SNDTIMEO;
default: break;
}
} else if (level == 6 && option == 1) {
return TCP_NODELAY;
}
#if defined(_WIN32)
return option;
#else
return -1;
#endif
}
static int* P2pSocketOption(P2pEndpoint& endpoint, int option) {
switch (option) {
case 0x0020: return &endpoint.broadcast;
@@ -1542,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;
}
@@ -2149,21 +2165,41 @@ int KYTY_SYSV_ABI Getsockopt(int s, int level, int optname, void* optval, uint32
return 0;
}
// Guest socket options: SOL_SOCKET=0xffff, SO_ERROR=0x1007.
const bool socket_error = (level == 0xffff && optname == 0x1007);
#if !defined(_WIN32)
if (!socket_error) {
const bool send_timeout = (level == 0xffff && optname == 0x1105);
optname = ConvertSocketOptionName(level, optname);
if (optname < 0) {
return SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
}
#if defined(_WIN32)
DWORD timeout {};
#else
timeval timeout {};
#endif
if (socket_error) {
optname = SO_ERROR;
}
SocketLength len = static_cast<SocketLength>(*optlen);
if (::getsockopt(socket, ConvertSocketOptionLevel(level), optname, static_cast<char*>(optval),
void* value = optval;
if (send_timeout) {
if (*optlen < sizeof(int)) {
return SetGuestSocketError(Posix::POSIX_EINVAL);
}
value = &timeout;
len = sizeof(timeout);
}
if (::getsockopt(socket, ConvertSocketOptionLevel(level), optname, static_cast<char*>(value),
&len) != 0) {
return SetHostSocketError();
}
if (send_timeout) {
#if defined(_WIN32)
const int64_t usec = static_cast<int64_t>(timeout) * 1000;
#else
const int64_t usec = static_cast<int64_t>(timeout.tv_sec) * 1'000'000 + timeout.tv_usec;
#endif
const int guest_timeout =
static_cast<int>(std::min<int64_t>(usec, std::numeric_limits<int>::max()));
std::memcpy(optval, &guest_timeout, sizeof(guest_timeout));
len = sizeof(guest_timeout);
}
if (socket_error && len >= static_cast<SocketLength>(sizeof(int))) {
auto* error = static_cast<int*>(optval);
*error = ConvertHostSocketError(*error);
@@ -2215,6 +2251,18 @@ int KYTY_SYSV_ABI Setsockopt(int s, int level, int optname, const void* optval,
return 0;
}
#if defined(__linux__)
if (level == 0xffff && optname == 0x10000) {
if (optlen != sizeof(int)) {
return SetGuestSocketError(Posix::POSIX_EINVAL);
}
int enabled = 0;
std::memcpy(&enabled, optval, sizeof(enabled));
// Native sends preserve 255.255.255.255 without changing broadcast permission.
return enabled != 0 ? 0 : SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
}
#endif
constexpr int ORBIS_SO_NBIO = 0x1200;
if (ConvertSocketOptionLevel(level) == SOL_SOCKET && optname == ORBIS_SO_NBIO &&
optlen >= sizeof(int)) {
@@ -2230,14 +2278,32 @@ int KYTY_SYSV_ABI Setsockopt(int s, int level, int optname, const void* optval,
#endif
return failed ? SetHostSocketError() : 0;
}
#if !defined(_WIN32)
// Guest TCP options: IPPROTO_TCP=6, TCP_NODELAY=1.
if (level != 6 || optname != 1) {
const bool send_timeout = (level == 0xffff && optname == 0x1105);
optname = ConvertSocketOptionName(level, optname);
if (optname < 0) {
return SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
}
level = IPPROTO_TCP;
optname = TCP_NODELAY;
#if defined(_WIN32)
DWORD timeout {};
#else
timeval timeout {};
#endif
if (send_timeout) {
if (optlen != sizeof(int)) {
return SetGuestSocketError(Posix::POSIX_EINVAL);
}
int usec = 0;
std::memcpy(&usec, optval, sizeof(usec));
usec = std::max(usec, 0);
#if defined(_WIN32)
timeout = static_cast<DWORD>((static_cast<int64_t>(usec) + 999) / 1000);
#else
timeout.tv_sec = usec / 1'000'000;
timeout.tv_usec = usec % 1'000'000;
#endif
optval = &timeout;
optlen = sizeof(timeout);
}
if (::setsockopt(socket, ConvertSocketOptionLevel(level), optname,
static_cast<const char*>(optval), static_cast<SocketLength>(optlen)) != 0) {
@@ -4040,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
-30
View File
@@ -21,10 +21,8 @@
#include <set>
#include <unordered_set>
#include <vector>
#if !defined(__APPLE__)
#include <xbyak/xbyak.h>
#include <xbyak/xbyak_util.h>
#endif
#ifdef min
#undef min
@@ -33,9 +31,7 @@
#undef max
#endif
#if !defined(__APPLE__)
using namespace Xbyak::util;
#endif
namespace Loader {
@@ -52,8 +48,6 @@ using s64 = int64_t;
constexpr size_t NearJumpSize = 5;
#if !defined(__APPLE__)
struct PatchModule {
std::mutex mutex {};
u8* start = nullptr;
@@ -1573,19 +1567,6 @@ GuestInstructionPatchResult PatchGuestInstructions(u64 segment_addr, u64 segment
return result;
}
#else
GuestInstructionHostFeatures GetGuestInstructionHostFeatures() {
return {};
}
GuestInstructionPatchResult PatchGuestInstructions(u64, u64, std::span<const uintptr_t>, bool, bool,
GuestInstructionHostFeatures) {
return {};
}
#endif
namespace {
constexpr uint8_t DW_EH_PE_FORMAT_MASK = 0x0f;
@@ -1763,7 +1744,6 @@ bool DecodeEhFrameFunctionStarts(uint64_t eh_frame_header_addr, uint64_t eh_fram
void RegisterGuestInstructionPatchModule(void* module_ptr, uint64_t module_size,
void* trampoline_area_ptr, uint64_t trampoline_area_size) {
#if !defined(__APPLE__)
EXIT_IF(module_ptr == nullptr || module_size == 0 || trampoline_area_ptr == nullptr ||
trampoline_area_size == 0);
const auto module_addr = reinterpret_cast<u64>(module_ptr);
@@ -1772,20 +1752,10 @@ void RegisterGuestInstructionPatchModule(void* module_ptr, uint64_t module_size,
std::forward_as_tuple(static_cast<u8*>(module_ptr), module_size,
static_cast<u8*>(trampoline_area_ptr),
trampoline_area_size));
#else
(void)module_ptr;
(void)module_size;
(void)trampoline_area_ptr;
(void)trampoline_area_size;
#endif
}
void UnregisterGuestInstructionPatchModule(void* module_ptr) {
#if !defined(__APPLE__)
g_patch_modules.erase(reinterpret_cast<u64>(module_ptr));
#else
(void)module_ptr;
#endif
}
#undef ASSERT
-4
View File
@@ -1670,11 +1670,7 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
uint64_t tls_handler_size = is_shared ? 0 : Jit::SafeCall::GetSize();
EXIT_IF(tls_handler_size > UINT64_MAX - program->base_size_aligned);
program->mapped_size = program->base_size_aligned + tls_handler_size;
#if !defined(__APPLE__)
const bool emulate_amd = Config::AmdCpuEnabled();
#else
const bool emulate_amd = false;
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
const bool protect_memory_faults = Config::RedZoneProtectionEnabled();
#else
+15 -8
View File
@@ -12,6 +12,7 @@
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#include <windows.h> // IWYU pragma: keep
#elif defined(__APPLE__)
#include <sched.h>
#include <sys/ucontext.h>
#else
#include <sched.h>
@@ -468,6 +469,16 @@ struct Context {
default: return nullptr;
}
}
// The kernel hands AVX threads the longer context that carries YMM[255:128]; a plain SSE
// context has no such state to clear.
void ClearUpperYmm(uint8_t index) const {
if (index >= 16 || native->uc_mcsize < sizeof(_STRUCT_MCONTEXT_AVX64)) {
return;
}
auto* avx = reinterpret_cast<_STRUCT_MCONTEXT_AVX64*>(native->uc_mcontext);
auto* ymmh = &avx->__fs.__fpu_ymmh0;
ymmh[index] = {};
}
#else
ucontext_t* native;
@@ -703,8 +714,6 @@ static bool TryEmulateCpuExtensions(Context& context) {
return true;
}
#if !defined(__APPLE__)
static bool TryEmulateMonitorxMwaitx(Context& context) {
const auto* rip = reinterpret_cast<const uint8_t*>(context.Rip());
if (rip[0] != 0x0f || rip[1] != 0x01 || (rip[2] != 0xfa && rip[2] != 0xfb)) {
@@ -790,8 +799,6 @@ static bool TryEmulateReciprocalSquareRoot(Context& context) {
return true;
}
#endif
bool TryEmulate(void* native_context) {
if (native_context == nullptr) {
return false;
@@ -807,12 +814,12 @@ bool TryEmulate(void* native_context) {
#else
Context context {static_cast<ucontext_t*>(native_context)};
#endif
#if !defined(__APPLE__)
return TryEmulateReciprocalSquareRoot(context) || TryEmulateMonitorxMwaitx(context) ||
TryEmulateSse4a(context) || TryEmulateShaNi(context) || TryEmulateCpuExtensions(context);
#else
return TryEmulateSse4a(context) || TryEmulateCpuExtensions(context);
TryEmulateSse4a(context) ||
#if !defined(__APPLE__)
TryEmulateShaNi(context) ||
#endif
TryEmulateCpuExtensions(context);
}
} // namespace Loader::X64InstructionEmulator
+15
View File
@@ -84,9 +84,14 @@ static void PrintUsage() {
::printf(" --spirv-debug-printf <true|false> Enable SPIR-V debug printf.\n");
::printf(
" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
::printf(
" --sync-raw-image-buffers <true|false> Synchronize raw reads of GPU images. Default: false.\n");
::printf(
" --trophy-notifications <true|false> Show trophy unlock toasts and play their sound.\n");
::printf(" --playgo-hack Use the supplied PlayGo stub fallback.\n");
::printf(
" --skip-notice-screen <true|false> Skip startup logos and notices in supported games.\n"
" Default: false.\n");
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
::printf(" --redzone Protect the guest SysV red zone.\n");
#endif
@@ -423,11 +428,21 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--sync-raw-image-buffers") {
if (!ParseBool(value, options.config.sync_raw_image_buffers)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--trophy-notifications") {
if (!ParseBool(value, options.config.trophy_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--skip-notice-screen") {
if (!ParseBool(value, options.config.skip_notice_screen)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--keymap") {
const auto split = value.find('=');
if (split == std::string::npos || split == 0 || split + 1 == value.size()) {
+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");
}
+157
View File
@@ -343,6 +343,162 @@ void TestIndependentOutputsAndPadSwitch() {
"released controller retained a cached vibration");
}
}
void TestTriggerEffectState() {
Controller controller;
PadTriggerEffectStateInformation info {{-1, -1}};
const auto check_state = [&](int32_t left, int32_t right, const char* message) {
Check(PadGetTriggerEffectState(1, &info) == OK && info.state[0] == left &&
info.state[1] == right,
message);
};
const auto axis = [&](int left, int right) {
SetAxis(1, Axis::TriggerLeft, left);
SetAxis(1, Axis::TriggerRight, right);
};
PadTriggerEffectParam param {};
const auto set_effect = [&] {
Check(PadSetTriggerEffect(1, &param) == OK, "trigger request failed");
};
Check(PadGetTriggerEffectState(2, &info) == PAD_ERROR_INVALID_HANDLE && info.state[0] == -1 &&
info.state[1] == -1,
"invalid trigger handle changed the output");
Check(PadGetTriggerEffectState(2, nullptr) == PAD_ERROR_INVALID_HANDLE,
"trigger state checked the output before the handle");
Check(PadGetTriggerEffectState(1, nullptr) == PAD_ERROR_INVALID_ARG,
"trigger state accepted a null output");
check_state(0, 0, "unset trigger effects did not report off");
param.trigger_mask = 3;
param.command[0].mode = 1;
param.command[0].data[0] = 7;
param.command[0].data[1] = 8;
param.command[1].mode = 2;
param.command[1].data[0] = 2;
param.command[1].data[1] = 7;
param.command[1].data[2] = 6;
set_effect();
axis(191, 63);
check_state(1, 3, "triggers activated before the documented start positions");
axis(192, 64);
check_state(2, 4, "triggers did not activate at the documented start positions");
axis(255, 223);
check_state(2, 4, "weapon fired before its end position");
SetAxis(1, Axis::TriggerRight, 224);
SetAxis(1, Axis::TriggerRight, 100);
check_state(2, 5, "unpolled weapon firing was lost on partial release");
check_state(2, 5, "reading trigger state changed the weapon latch");
set_effect();
check_state(2, 5, "repeating a weapon request rearmed a held trigger");
CycleSetting(Setting::TriggerEffectIntensity);
check_state(2, 5, "muting trigger effects changed their reported states");
// A rejected two-trigger request must leave both cached commands and states intact.
auto invalid = param;
invalid.command[0].mode = 0;
invalid.command[1].data[1] = 2;
Check(PadSetTriggerEffect(1, &invalid) == PAD_ERROR_INVALID_ARG,
"invalid weapon end position was accepted");
SetAxis(1, Axis::TriggerLeft, 255);
check_state(2, 5, "an invalid request changed cached trigger commands or states");
SetAxis(1, Axis::TriggerRight, 63);
SetAxis(1, Axis::TriggerRight, 100);
check_state(2, 4, "unpolled release did not rearm the weapon");
param.trigger_mask = 2;
param.command[1].data[1] = 8;
set_effect();
axis(255, 253);
check_state(2, 4, "weapon end position 8 fired before output 254");
SetAxis(1, Axis::TriggerRight, 254);
check_state(2, 5, "weapon end position 8 did not fire at output 254");
// Masked updates preserve the other trigger, including its fired state.
param.trigger_mask = 1;
param.command[0].data[0] = 9;
set_effect();
SetAxis(1, Axis::TriggerLeft, 254);
check_state(1, 5, "feedback position 9 activated before full travel");
SetAxis(HOST_INPUT_CONTROLLER_ID, Axis::TriggerLeft, 255);
check_state(2, 5, "keyboard trigger travel did not activate feedback");
param.command[0].data[0] = 0;
set_effect();
SetAxis(1, Axis::TriggerLeft, 0);
check_state(2, 5, "feedback at position 0 was suppressed by a dead zone");
param.command[0].data[1] = 0;
set_effect();
check_state(1, 5, "zero-strength feedback reported active");
param.command[0] = {};
param.command[0].mode = 4;
param.command[0].data[0] = 4;
param.command[0].data[2] = 4;
param.command[0].data[8] = 4;
set_effect();
const int feedback_cases[][2] = {{0, 2}, {1, 1}, {31, 1}, {32, 2}, {63, 2},
{64, 1}, {223, 1}, {224, 2}, {254, 2}, {255, 1}};
for (const auto& test: feedback_cases) {
SetAxis(1, Axis::TriggerLeft, test[0]);
check_state(test[1], 5, "multi-position feedback used the wrong position");
}
param.command[0] = {};
param.command[0].mode = 5;
param.command[0].data[0] = 3;
param.command[0].data[1] = 7;
param.command[0].data[2] = 2;
param.command[0].data[3] = 6;
set_effect();
const int slope_cases[][2] = {{63, 1}, {64, 2}, {191, 2}, {192, 1}, {255, 1}};
for (const auto& test: slope_cases) {
SetAxis(1, Axis::TriggerLeft, test[0]);
check_state(test[1], 5, "slope feedback did not respect its start and end");
}
param.command[0].data[0] = 0;
set_effect();
SetAxis(1, Axis::TriggerLeft, 0);
check_state(2, 5, "slope feedback at position 0 was suppressed");
param.command[0] = {};
param.command[0].mode = 3;
param.command[0].data[0] = 6;
param.command[0].data[1] = 4;
param.command[0].data[2] = 20;
set_effect();
SetAxis(1, Axis::TriggerLeft, 159);
check_state(6, 5, "vibration activated before position 6");
SetAxis(1, Axis::TriggerLeft, 160);
check_state(7, 5, "vibration did not activate at position 6");
param.command[0].data[0] = 0;
set_effect();
SetAxis(1, Axis::TriggerLeft, 0);
check_state(7, 5, "vibration at position 0 was suppressed");
param.command[0].data[2] = 0;
set_effect();
check_state(6, 5, "zero-frequency vibration reported active");
param.command[0] = {};
param.command[0].mode = 6;
param.command[0].data[0] = 20;
param.command[0].data[10] = 4;
set_effect();
SetAxis(1, Axis::TriggerLeft, 254);
check_state(6, 5, "multi-position vibration activated before position 9");
SetAxis(1, Axis::TriggerLeft, 255);
check_state(7, 5, "multi-position vibration did not activate at position 9");
param.command[0].data[0] = 0;
set_effect();
check_state(6, 5, "zero-frequency multi-position vibration reported active");
ResetInputState();
check_state(6, 3, "input reset retained a fired weapon");
param.command[0].mode = 0;
set_effect();
check_state(0, 3, "off mode retained an effect state");
Connect(2);
Disconnect(1);
check_state(0, 0, "switching controllers retained effect states");
}
} // namespace
int main() {
@@ -353,6 +509,7 @@ int main() {
TestVibrationLifetime();
TestMaskedTriggersAndValidation();
TestIndependentOutputsAndPadSwitch();
TestTriggerEffectState();
Config::Shutdown();
return 0;
}
+453 -8
View File
@@ -34,12 +34,20 @@
#include <thread>
#include <vector>
#if defined(__linux__)
#include <sys/mman.h>
#include <unistd.h>
#endif
namespace Libs::LibKernelApr {
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 {
@@ -116,6 +124,138 @@ void CheckSaveRename(const std::filesystem::path &root,
"renamed save contents");
}
void TestRandomDevices() {
for (const auto* path : {"/dev/urandom", "/dev/random"}) {
const int fd = FileSystem::KernelOpen(path, 0, 0);
FileSystem::FileStat stat {};
Check(fd >= 3 && FileSystem::KernelFstat(fd, &stat) == OK &&
(stat.st_mode & 0170000) == 0020000,
"entropy sources are character devices");
std::array<uint8_t, 32> entropy {};
Check(FileSystem::KernelRead(fd, entropy.data(), entropy.size()) == entropy.size(),
"character device supplies requested entropy");
Check(FileSystem::KernelClose(fd) == OK, "close entropy source");
}
}
void TestFileDescriptorFlags() {
Loader::SymbolDatabase symbols;
Libs::InitLibKernel_1(&symbols);
const auto* symbol = symbols.Find(
{"8nY19bKoiZk", "Posix", 1, "libkernel", 1, 1, Loader::SymbolType::Func});
Check(symbol != nullptr, "POSIX fcntl export resolves");
const auto fcntl = reinterpret_cast<int (KYTY_SYSV_ABI *)(int, int, int)>(symbol->vaddr);
Check(fcntl(std::numeric_limits<int>::min(), 1, 0) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF,
"invalid descriptor reports EBADF");
for (const auto* path : {"/dev/urandom", "/dev/random"}) {
const int fd = FileSystem::KernelOpen(path, 0, 0);
Check(fd >= 3, "open descriptor for flag checks");
Check(fcntl(fd, 1, 0) == 0 && fcntl(fd, 2, 1) == 0 && fcntl(fd, 1, 0) == 1,
"entropy descriptor retains close-on-exec flag");
Check(fcntl(fd, 2, 0) == 0 && fcntl(fd, 1, 0) == 0,
"close-on-exec flag can be cleared");
Check(fcntl(fd, -1, 0) == -1 && *Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
"unsupported fcntl command reports EINVAL");
Check(FileSystem::KernelClose(fd) == OK, "close entropy source");
Check(fcntl(fd, 1, 0) == -1 && *Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF,
"closed descriptor reports EBADF");
const int cloexec_fd = FileSystem::KernelOpen(path, 0x00100000, 0);
Check(cloexec_fd >= 3 && fcntl(cloexec_fd, 1, 0) == 1,
"O_CLOEXEC sets the descriptor flag on open");
Check(FileSystem::KernelClose(cloexec_fd) == OK, "close flagged entropy source");
}
}
void TestSysmoduleReferences() {
Loader::SymbolDatabase symbols;
Libs::InitSysmodule_1(&symbols);
using ModuleCall = int (KYTY_SYSV_ABI *)(uint16_t);
const auto resolve = [&](const char* nid) {
const auto* symbol = symbols.Find(
{nid, "Sysmodule", 1, "Sysmodule", 1, 1, Loader::SymbolType::Func});
Check(symbol != nullptr, "Sysmodule export resolves");
return reinterpret_cast<ModuleCall>(symbol->vaddr);
};
const auto load = resolve("g8cM39EUZ6o");
const auto unload = resolve("eR2bZFAAU0Q");
const auto is_loaded = resolve("fMP5NHUOaMk");
constexpr int unloaded = static_cast<int>(0x805a1001u);
Check(load(0x113) == OK && is_loaded(0xb4) == unloaded,
"loading entitlement access does not mark AppContent loaded");
Check(unload(0x113) == OK, "release independent module reference");
for (const uint16_t id : {0x113, 0xb4}) {
Check(is_loaded(id) == unloaded, "unloaded module allows guest initialization");
Check(unload(id) == unloaded, "unloading an absent module reports UNLOADED");
Check(load(id) == OK && load(id) == OK && is_loaded(id) == OK,
"repeated loads retain references");
Check(unload(id) == OK && is_loaded(id) == OK,
"one unload preserves the remaining reference");
Check(unload(id) == OK && is_loaded(id) == unloaded,
"last unload restores unloaded status");
}
const auto* internal_symbol = symbols.Find(
{"hHrGoGoNf+s", "Sysmodule", 1, "Sysmodule", 1, 1, Loader::SymbolType::Func});
Check(internal_symbol != nullptr, "internal Sysmodule load export resolves");
const auto internal_load =
reinterpret_cast<int (KYTY_SYSV_ABI *)(uint16_t, int, int, int, int*)>(
internal_symbol->vaddr);
int result = -1;
Check(internal_load(0xb4, 0, 0, 0, &result) == OK && result == OK &&
is_loaded(0xb4) == OK && unload(0xb4) == OK && is_loaded(0xb4) == unloaded,
"internal and public module operations share load state");
}
void TestSystemServiceEntitlementEvents() {
Loader::SymbolDatabase symbols;
Libs::InitSystemService_1(&symbols);
Libs::InitAppContent_1(&symbols);
const auto resolve = [&](const char* nid, const char* library, const char* module) {
const auto* symbol = symbols.Find(
{nid, library, 1, module, 1, 1, Loader::SymbolType::Func});
Check(symbol != nullptr, "entitlement event export resolves");
return symbol->vaddr;
};
const auto initialize = reinterpret_cast<int (KYTY_SYSV_ABI *)(const void*, void*)>(
resolve("R9lA82OraNs", "AppContent", "AppContentUtil"));
struct Status {
int32_t event_num;
bool overlay, background, vr;
uint8_t reserved[127];
};
struct Event {
int32_t type;
uint8_t data[8192];
};
static_assert(sizeof(Status) == 136 && sizeof(Event) == 8196);
const auto get_status = reinterpret_cast<int (KYTY_SYSV_ABI *)(Status*)>(
resolve("rPo6tV8D9bM", "SystemService", "SystemService"));
const auto receive = reinterpret_cast<int (KYTY_SYSV_ABI *)(Event*)>(
resolve("656LMQSrg6U", "SystemService", "SystemService"));
Status status {};
Event event {};
Check(get_status(&status) == OK && status.event_num == 0,
"SystemService starts without pending events");
std::array<uint8_t, 32> init {};
std::array<uint8_t, 40> boot {};
Check(initialize(init.data(), boot.data()) == OK,
"AppContent initialization generates an entitlement notification");
Check(get_status(&status) == OK && status.event_num == 1 &&
get_status(&status) == OK && status.event_num == 1,
"status queries preserve pending notifications");
Check(receive(nullptr) == Libs::SystemService::SYSTEM_SERVICE_ERROR_PARAMETER &&
get_status(&status) == OK && status.event_num == 1,
"invalid event output does not consume a notification");
std::memset(&event, 0xff, sizeof(event));
Check(receive(&event) == OK && event.type == 0x10000003 &&
std::all_of(std::begin(event.data), std::end(event.data),
[](uint8_t byte) { return byte == 0; }) &&
get_status(&status) == OK && status.event_num == 0,
"receive delivers the entitlement event with cleared payload exactly once");
Check(receive(&event) == Libs::SystemService::SYSTEM_SERVICE_ERROR_NO_EVENT,
"empty event queue reports NO_EVENT");
}
void TestSaveOpenVisibility() {
constexpr char Path[] = "/savedata0/visible-save.dat";
constexpr char Payload[] = "saved progress";
@@ -164,12 +304,14 @@ void TestAioBatches() {
using Batch = int (KYTY_SYSV_ABI *)(int32_t *, int32_t, int32_t *);
using Single = int (KYTY_SYSV_ABI *)(int32_t, int32_t *);
using Wait = int (KYTY_SYSV_ABI *)(int32_t, int32_t *, uint32_t *);
using WaitBatch = int (KYTY_SYSV_ABI *)(int32_t *, int32_t, int32_t *, uint32_t, uint32_t *);
const auto submit = reinterpret_cast<Submit>(find("HgX7+AORI58"));
const auto poll = reinterpret_cast<Batch>(find("o7O4z3jwKzo"));
const auto erase = reinterpret_cast<Batch>(find("Ft3EtsZzAoY"));
const auto poll_one = reinterpret_cast<Single>(find("2pOuoWoCxdk"));
const auto erase_one = reinterpret_cast<Single>(find("5TgME6AYty4"));
const auto wait = reinterpret_cast<Wait>(find("KOF-oJbQVvc"));
const auto wait_batch = reinterpret_cast<WaitBatch>(find("lgK+oIWkJyA"));
constexpr char Payload[] = "AIO payload";
const int fd = FileSystem::KernelOpen("/savedata0/aio.dat", 0x602, 0777);
Check(fd >= 3 && FileSystem::KernelWrite(fd, Payload, sizeof(Payload)) == sizeof(Payload),
@@ -186,14 +328,22 @@ void TestAioBatches() {
}
ids[2] = -1;
std::array<int32_t, 3> states {-1, -1, -1};
Check(poll(ids.data(), ids.size(), states.data()) == OK &&
uint32_t timeout = 0;
Check(wait_batch(ids.data(), ids.size(), states.data(), 2, &timeout) == OK &&
states[0] == 3 && states[1] == 3 && states[2] == Kernel::KERNEL_ERROR_ESRCH,
"batch poll writes every state and per-request invalid-ID error");
"OR wait observes completed reads and invalid-ID errors even with a zero timeout");
Check(poll_one(ids[0], &states[0]) == OK && states[0] == (3 | 0x10000) &&
poll(ids.data(), 2, states.data()) == OK &&
states[0] == (3 | 0x10000) && states[1] == (3 | 0x10000) &&
wait(ids[0], &states[0], nullptr) == OK && states[0] == (3 | 0x10000),
"single and batch polls and waits share completion notification state");
timeout = 1000000;
Check(wait_batch(ids.data(), ids.size(), states.data(), 1, &timeout) == OK &&
states[0] == (3 | 0x10000) && states[1] == (3 | 0x10000) &&
states[2] == Kernel::KERNEL_ERROR_ESRCH && timeout <= 1000000 &&
wait_batch(ids.data(), ids.size(), states.data(), 2, nullptr) == OK &&
wait_batch(ids.data(), 1, states.data(), 0, nullptr) == OK,
"AND and OR waits return when all IDs are terminal and one-ID waits ignore mode");
Check(erase(ids.data(), ids.size(), states.data()) == OK &&
states[0] == OK && states[1] == OK && states[2] == Kernel::KERNEL_ERROR_ESRCH,
"batch deletion writes per-request results");
@@ -216,6 +366,22 @@ void TestAioBatches() {
}),
"maximum-sized batch returns all invalid-ID errors");
}
states.fill(42);
Check(wait_batch(nullptr, 1, states.data(), 1, nullptr) == Kernel::KERNEL_ERROR_EFAULT &&
wait_batch(ids.data(), 1, nullptr, 1, nullptr) == Kernel::KERNEL_ERROR_EFAULT &&
wait_batch(ids.data(), 0, states.data(), 1, nullptr) == Kernel::KERNEL_ERROR_EINVAL &&
wait_batch(ids.data(), 129, states.data(), 1, nullptr) == Kernel::KERNEL_ERROR_EINVAL &&
wait_batch(ids.data(), 2, states.data(), 0, nullptr) == Kernel::KERNEL_ERROR_EINVAL &&
wait_batch(ids.data(), 2, states.data(), 3, nullptr) == Kernel::KERNEL_ERROR_EINVAL &&
states == std::array<int32_t, 3> {42, 42, 42},
"invalid wait arguments do not modify outputs");
std::array<int32_t, 128> invalid_ids {};
std::array<int32_t, 128> errors {};
Check(wait_batch(invalid_ids.data(), invalid_ids.size(), errors.data(), 1, nullptr) == OK &&
std::all_of(errors.begin(), errors.end(), [](int32_t error) {
return error == Kernel::KERNEL_ERROR_ESRCH;
}),
"maximum-sized wait returns every invalid-ID error");
Check(FileSystem::KernelClose(fd) == OK, "close AIO read fixture");
}
@@ -638,6 +804,10 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
using Offset = uint64_t (KYTY_SYSV_ABI *)(void *);
using WaitAddress = int (KYTY_SYSV_ABI *)(void *, volatile uint64_t *, uint64_t,
uint8_t, uint8_t);
using WaitCounter = int (KYTY_SYSV_ABI *)(void *, uint8_t, uint8_t, uint64_t,
uint8_t, uint8_t, uint64_t, uint8_t);
using WriteCounter = int (KYTY_SYSV_ABI *)(void *, uint8_t, uint8_t, uint64_t,
uint8_t, uint32_t);
using WriteAddress = int (KYTY_SYSV_ABI *)(void *, volatile uint64_t *, uint64_t);
using ReadFile = int (KYTY_SYSV_ABI *)(void *, uint64_t, uint64_t, uint32_t,
void *, uint64_t, uint64_t);
@@ -654,6 +824,8 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
const auto set_buffer = reinterpret_cast<SetBuffer>(find("N-FSPA4S3nI"));
const auto offset = reinterpret_cast<Offset>(find("GnxKOHEawhk"));
const auto wait_address = reinterpret_cast<WaitAddress>(find("DLfoNxTFNVk"));
const auto wait_counter = reinterpret_cast<WaitCounter>(find("cQb8Zr8Q0Y0"));
const auto write_counter = reinterpret_cast<WriteCounter>(find("jK+yuYCI7MA"));
const auto write_address = reinterpret_cast<WriteAddress>(find("sJXyWHjP-F8"));
const auto read_file = reinterpret_cast<ReadFile>(find("mQ16-QdKv7k"));
const auto write_event = reinterpret_cast<WriteKernelEvent>(find("H896Pt-yB4I"));
@@ -727,6 +899,59 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
"submission wait observes the completed result");
}
}
constexpr std::array counter_comparisons {
Comparison{1, 0, 0, 1}, Comparison{1, 0, 0, 1},
Comparison{4, 0x7ffffffe, 0x7fffffff, 0x80000000},
Comparison{5, 0xffffffff, 0, 1}};
for (const auto &comparison : counter_comparisons) {
reset(1);
auto *producer = buffers[0].header.data();
auto *consumer = buffers[1].header.data();
auto *lower = buffers[2].header.data();
constexpr uint8_t Counter = 127;
constexpr auto Invalid = Libs::LibKernel::KERNEL_ERROR_EINVAL;
Check(write_counter(producer, 128, 1, 1, 0, 0) == Invalid &&
write_counter(producer, Counter, 8, 1, 0, 0) == Invalid &&
write_counter(producer, Counter, 1, 1, 5, 0) == Invalid &&
write_counter(producer, Counter, 1, 1, 0, 2) == Invalid &&
wait_counter(consumer, Counter, 8, 0, 1, 0, 0, 0) == Invalid &&
wait_counter(consumer, Counter, 1, 0, 7, 0, 0, 0) == Invalid &&
wait_counter(consumer, Counter, 1, 0, 1, 2, 0, 0) == Invalid &&
wait_counter(consumer, Counter, 1, 0, 1, 0, 0, 2) == Invalid &&
offset(producer) == 0 && offset(consumer) == 0,
"invalid counter operations fail without appending a command");
uint64_t retired = 0, lower_done = 0;
std::array<char, 3> output {};
Result result {1234, 5678};
std::array<uint32_t, 4> ids {};
if (comparison.blocked != 0) {
auto *initializer = buffers[3].header.data();
Check(write_counter(initializer, Counter, 1, comparison.blocked, 0, 0) == OK &&
submit_amm(buffers[3].data.data(), static_cast<uint32_t>(offset(initializer)),
0, &ids[3]) == OK && wait_amm(ids[3]) == OK,
"initialize shared counter before dependent submissions");
}
Check(read_file(producer, reinterpret_cast<uint64_t>(&buffers[0].header[3]),
reinterpret_cast<uint64_t>(&buffers[0].header[4]), file_id,
output.data(), output.size(), 0) == OK &&
write_counter(producer, Counter, 1, comparison.released, 0, 0) == OK &&
wait_counter(consumer, Counter, 1, comparison.reference, comparison.compare,
0, 0, 0) == OK &&
write_counter(consumer, Counter, 1, 0, 0, 0) == OK &&
write_address(consumer, &retired, 1) == OK &&
write_address(lower, &lower_done, 1) == OK,
"build APR completion and dependent AMM counter reset");
Check(submit_amm(buffers[1].data.data(), static_cast<uint32_t>(offset(consumer)),
0, &ids[1]) == OK &&
submit_amm(buffers[2].data.data(), static_cast<uint32_t>(offset(lower)),
1, &ids[2]) == OK && wait_amm(ids[2]) == OK &&
lower_done == 1 && retired == 0,
"counter wait blocks AMM retirement while its lower priority progresses");
Check(submit_apr(producer, 3, &result, &ids[0]) == OK && wait_amm(ids[1]) == OK &&
wait_apr(ids[0]) == OK && result.result == OK && retired == 1 &&
std::memcmp(output.data(), "APR", 3) == 0,
"shared counter completion releases AMM only after APR read and resets for reuse");
}
reset(1);
namespace EventQueue = Libs::LibKernel::EventQueue;
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
@@ -826,6 +1051,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) {
@@ -849,7 +1104,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");
}
@@ -1057,23 +1312,76 @@ 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;
Libs::LibNet::InitNet_1_Net(&symbols);
const auto *connect_symbol = symbols.Find(
{"OXXX4mUk3uk", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
const auto *getsockopt_symbol = symbols.Find(
{"xphrZusl78E", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
const auto *setsockopt_symbol = symbols.Find(
{"2mKX2Spso7I", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
const auto *send_symbol = symbols.Find(
{"beRjXBn-z+o", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
const auto *sendto_symbol = symbols.Find(
{"gvD1greCu0A", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
const auto *recv_symbol = symbols.Find(
{"9wO9XrMsNhc", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
const auto *recvfrom_symbol = symbols.Find(
{"304ooNZxWDY", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
const auto *errno_symbol = symbols.Find(
{"HQOwnfMGipQ", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
Check(send_symbol && recv_symbol && errno_symbol,
"Net send, receive and errno exports resolve with the guest ABI versions");
Check(connect_symbol && getsockopt_symbol && setsockopt_symbol && send_symbol && sendto_symbol &&
recv_symbol && recvfrom_symbol && errno_symbol,
"Net socket and errno exports resolve with the guest ABI versions");
using Connect = int (KYTY_SYSV_ABI *)(int, const void *, uint32_t);
using Getsockopt = int (KYTY_SYSV_ABI *)(int, int, int, void *, uint32_t *);
using Setsockopt = int (KYTY_SYSV_ABI *)(int, int, int, const void *, uint32_t);
using Send = int (KYTY_SYSV_ABI *)(int, const void *, size_t, int);
using Sendto = int (KYTY_SYSV_ABI *)(int, const void *, size_t, int, const void *, uint32_t);
using Recv = int (KYTY_SYSV_ABI *)(int, void *, size_t, int);
using Recvfrom = int (KYTY_SYSV_ABI *)(int, void *, size_t, int, void *, uint32_t *);
using Errno = int *(KYTY_SYSV_ABI *)();
const auto net_connect = reinterpret_cast<Connect>(connect_symbol->vaddr);
const auto net_getsockopt = reinterpret_cast<Getsockopt>(getsockopt_symbol->vaddr);
const auto net_setsockopt = reinterpret_cast<Setsockopt>(setsockopt_symbol->vaddr);
const auto net_send = reinterpret_cast<Send>(send_symbol->vaddr);
const auto net_sendto = reinterpret_cast<Sendto>(sendto_symbol->vaddr);
const auto net_recv = reinterpret_cast<Recv>(recv_symbol->vaddr);
const auto net_recvfrom = reinterpret_cast<Recvfrom>(recvfrom_symbol->vaddr);
auto *net_errno = reinterpret_cast<Errno>(errno_symbol->vaddr)();
const auto [reader, writer] = CreateTcpPair();
const int enabled = 1;
@@ -1082,10 +1390,15 @@ void CheckSocketWakeup() {
int socket_error = -1;
uint32_t error_size = sizeof(socket_error);
*Libs::Posix::GetErrorAddr() = Libs::Posix::POSIX_EINVAL;
Check(Net::Getsockopt(writer, 0xffff, 0x1007, &socket_error, &error_size) == 0 &&
*net_errno = Libs::Posix::POSIX_EINVAL;
Check(net_getsockopt(writer, 0xffff, 0x1007, &socket_error, &error_size) == 0 &&
socket_error == 0 && error_size == sizeof(socket_error) &&
*net_errno == Libs::Posix::POSIX_EINVAL &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
"SO_ERROR reports socket status without changing guest errno");
"Net SO_ERROR reports socket status without changing either guest errno");
Check(net_getsockopt(writer, 0xffff, 0x1007, nullptr, &error_size) ==
Libs::Network::NET_ERROR_EFAULT && *net_errno == Libs::Posix::POSIX_EFAULT,
"Net getsockopt translates an invalid output buffer");
std::array<uint64_t, 16> readable {};
const auto bit = uint64_t {1} << (reader % 64);
@@ -1122,6 +1435,131 @@ void CheckSocketWakeup() {
*net_errno == Libs::Posix::POSIX_EWOULDBLOCK,
"Net nonblocking receive translates POSIX failure and Net errno");
#endif
const int datagram = Net::Socket(2, 2, 0);
const int datagram_writer = Net::Socket(2, 2, 0);
std::array<uint8_t, 16> address {16, 2, 0, 0, 127, 0, 0, 1};
std::array<uint8_t, 16> peer {}, expected_peer {};
uint32_t address_size = address.size(), peer_size = peer.size();
uint32_t expected_peer_size = expected_peer.size();
Check(datagram >= 0 && datagram_writer >= 0 &&
Net::Bind(datagram, address.data(), address.size()) == 0 &&
Net::Bind(datagram_writer, address.data(), address.size()) == 0 &&
Net::Getsockname(datagram, address.data(), &address_size) == 0 &&
Net::Getsockname(datagram_writer, expected_peer.data(), &expected_peer_size) == 0 &&
Net::Setsockopt(datagram, 0xffff, 0x1200, &enabled, sizeof(enabled)) == 0,
"create nonblocking loopback datagrams for Net ABI verification");
*net_errno = Libs::Posix::POSIX_EINVAL;
*Libs::Posix::GetErrorAddr() = Libs::Posix::POSIX_EINVAL;
for (const int option : {0x1001, 0x1002}) {
constexpr int requested = 16384;
int actual = 0;
uint32_t size = sizeof(actual);
Check(net_setsockopt(datagram, 0xffff, option, &requested, sizeof(requested)) == 0 &&
net_getsockopt(datagram, 0xffff, option, &actual, &size) == 0 &&
actual >= requested && size == sizeof(actual),
"Net UDP send and receive buffers accept guest option numbers");
}
for (const int value : {1, 0}) {
int actual = -1;
uint32_t size = sizeof(actual);
Check(net_setsockopt(datagram, 0xffff, 0x20, &value, sizeof(value)) == 0 &&
net_getsockopt(datagram, 0xffff, 0x20, &actual, &size) == 0 &&
actual == value && size == sizeof(actual),
"Net UDP broadcast option can be enabled and disabled");
}
int timeout = 0;
int *timeout_value = &timeout;
#if defined(__linux__)
int broadcast = -1;
uint32_t broadcast_size = sizeof(broadcast);
Check(net_setsockopt(datagram, 0xffff, 0x10000, &enabled, sizeof(enabled)) == 0 &&
net_getsockopt(datagram, 0xffff, 0x20, &broadcast, &broadcast_size) == 0 &&
broadcast == 0,
"preserving the all-ones destination does not enable broadcast permission");
const long page_size = sysconf(_SC_PAGESIZE);
Check(page_size > 0, "get host page size for socket timeout boundary");
void *timeout_pages = mmap(nullptr, page_size * 2, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
Check(timeout_pages != MAP_FAILED &&
mprotect(static_cast<char *>(timeout_pages) + page_size, page_size,
PROT_NONE) == 0,
"guard memory after the four-byte socket timeout");
timeout_value = reinterpret_cast<int *>(static_cast<char *>(timeout_pages) +
page_size - sizeof(int));
#endif
for (const int value : {1500000, 0, -1}) {
*timeout_value = value;
Check(net_setsockopt(datagram, 0xffff, 0x1105, timeout_value, sizeof(int)) == 0,
"Net send timeout reads a four-byte microsecond value");
*timeout_value = -2;
uint32_t size = sizeof(int);
Check(net_getsockopt(datagram, 0xffff, 0x1105, timeout_value, &size) == 0 &&
*timeout_value == std::max(value, 0) && size == sizeof(int),
"Net send timeout returns four-byte microseconds and disables nonpositive values");
}
#if defined(__linux__)
Check(munmap(timeout_pages, page_size * 2) == 0, "free socket timeout guard pages");
#endif
Check(*net_errno == Libs::Posix::POSIX_EINVAL &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
"successful Net socket option calls preserve both guest errno values");
Check(net_setsockopt(datagram, 0xffff, 0x1105, nullptr, sizeof(timeout)) ==
Libs::Network::NET_ERROR_EFAULT && *net_errno == Libs::Posix::POSIX_EFAULT &&
net_getsockopt(datagram, 0xffff, 0x1105, &timeout, nullptr) ==
Libs::Network::NET_ERROR_EFAULT && *net_errno == Libs::Posix::POSIX_EFAULT,
"Net socket options reject null value and length pointers");
uint32_t short_size = sizeof(timeout) - 1;
Check(net_setsockopt(datagram, 0xffff, 0x1105, &timeout, short_size) ==
Libs::Network::NET_ERROR_EINVAL && *net_errno == Libs::Posix::POSIX_EINVAL &&
net_getsockopt(datagram, 0xffff, 0x1105, &timeout, &short_size) ==
Libs::Network::NET_ERROR_EINVAL && *net_errno == Libs::Posix::POSIX_EINVAL,
"Net send timeout rejects undersized values");
uint32_t option_size = sizeof(timeout);
Check(net_setsockopt(datagram, 0xffff, 0x7fffffff, &timeout, option_size) ==
Libs::Network::NET_ERROR_ENOPROTOOPT &&
*net_errno == Libs::Posix::POSIX_ENOPROTOOPT &&
net_getsockopt(datagram, 0xffff, 0x7fffffff, &timeout, &option_size) ==
Libs::Network::NET_ERROR_ENOPROTOOPT &&
*net_errno == Libs::Posix::POSIX_ENOPROTOOPT,
"Net unknown socket options return protocol-option errors");
#if defined(__linux__)
Check(net_setsockopt(datagram, 0xffff, 0x1007, &timeout, option_size) ==
Libs::Network::NET_ERROR_ENOPROTOOPT &&
*net_errno == Libs::Posix::POSIX_ENOPROTOOPT,
"Net native protocol-option errors retain their guest error code");
#endif
received.fill(0);
*net_errno = Libs::Posix::POSIX_EINVAL;
Check(net_sendto(datagram_writer, payload, sizeof(payload), 0,
address.data(), address_size) == sizeof(payload) &&
*net_errno == Libs::Posix::POSIX_EINVAL,
"Net sendto delivers to a guest sockaddr and preserves errno on success");
Check(net_recvfrom(datagram, received.data(), received.size(), 0,
peer.data(), &peer_size) == sizeof(payload) &&
std::memcmp(received.data(), payload, sizeof(payload)) == 0 &&
peer_size == expected_peer_size && peer == expected_peer &&
*net_errno == Libs::Posix::POSIX_EINVAL,
"Net recvfrom returns datagram bytes and sender address while preserving errno");
Check(net_recvfrom(datagram, received.data(), received.size(), 0, nullptr, nullptr) ==
Libs::Network::NET_ERROR_EWOULDBLOCK &&
*net_errno == Libs::Posix::POSIX_EWOULDBLOCK,
"Net recvfrom translates nonblocking failure with an omitted sender address");
*net_errno = Libs::Posix::POSIX_EINVAL;
Check(net_connect(datagram_writer, address.data(), address_size) == 0 &&
*net_errno == Libs::Posix::POSIX_EINVAL &&
net_sendto(datagram_writer, payload, sizeof(payload), 0, nullptr, 0) ==
sizeof(payload) && *net_errno == Libs::Posix::POSIX_EINVAL &&
net_recvfrom(datagram, received.data(), received.size(), 0, nullptr, nullptr) ==
sizeof(payload) && std::memcmp(received.data(), payload, sizeof(payload)) == 0,
"Net connect selects the peer used by sendto with an omitted destination");
Check(net_connect(-1, address.data(), address_size) == Libs::Network::NET_ERROR_EBADF &&
*net_errno == Libs::Posix::POSIX_EBADF,
"Net connect translates an invalid socket");
Check(net_sendto(-1, payload, sizeof(payload), 0, address.data(), address_size) ==
Libs::Network::NET_ERROR_EBADF && *net_errno == Libs::Posix::POSIX_EBADF,
"Net sendto translates an invalid socket");
Check(Net::SocketClose(datagram) == 0 && Net::SocketClose(datagram_writer) == 0,
"close Net loopback datagrams");
Check(net_send(-1, payload, sizeof(payload), 0) == Libs::Network::NET_ERROR_EBADF &&
*net_errno == Libs::Posix::POSIX_EBADF,
"Net send translates an invalid socket instead of returning POSIX minus one");
@@ -1138,10 +1576,12 @@ void CheckSocketWakeup() {
const auto previous_sigpipe = std::signal(SIGPIPE, SIG_DFL);
Check(previous_sigpipe != SIG_ERR, "set default SIGPIPE disposition for Net send");
const auto broken_send = net_send(disconnected, payload, sizeof(payload), 0);
const auto broken_sendto = net_sendto(disconnected, payload, sizeof(payload), 0, nullptr, 0);
std::signal(SIGPIPE, previous_sigpipe);
Check(broken_send == Libs::Network::NET_ERROR_EPIPE &&
broken_sendto == Libs::Network::NET_ERROR_EPIPE &&
*net_errno == Libs::Posix::POSIX_EPIPE,
"Net send reports a broken pipe without raising host SIGPIPE");
"Net send and sendto report a broken pipe without raising host SIGPIPE");
Check(Net::SocketClose(disconnected) == 0, "close unconnected socket");
#endif
#if defined(_WIN32)
@@ -1184,6 +1624,10 @@ int main(int, char**) {
TempDirectory temporary;
FileSystem::Initialize();
TestSysmoduleReferences();
TestSystemServiceEntitlementEvents();
TestRandomDevices();
TestFileDescriptorFlags();
CheckMountRoot(temporary.Path());
CheckUnmappedPaths(temporary.Path());
CheckArchiveMount(temporary.Path());
@@ -1198,6 +1642,7 @@ int main(int, char**) {
CheckSaveRename(temporary.Path(), "replacement-save");
FileSystem::Shutdown();
CheckSocketWakeup();
CheckEtherAddressFormatting();
TestNpWebApi2Memory();
graphics.reset();
subsystems.Destroy();
+90 -17
View File
@@ -9,6 +9,7 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <semaphore>
#include <string>
#include <thread>
@@ -210,6 +211,7 @@ struct ProtectionCall {
};
std::vector<ProtectionCall> g_protection_log;
std::mutex g_protection_log_mutex;
void ResetProtectionLog() {
g_protection_calls = 0;
@@ -225,8 +227,11 @@ bool ProtectAddressSpace(uint64_t vaddr, uint64_t size,
protection = PAGE_READWRITE;
}
DWORD old_protection = 0;
g_protection_calls++;
g_protection_log.push_back({vaddr, size, mode});
{
std::lock_guard lock(g_protection_log_mutex);
g_protection_calls++;
g_protection_log.push_back({vaddr, size, mode});
}
return VirtualProtect(reinterpret_cast<void *>(vaddr), size, protection,
&old_protection) != 0;
}
@@ -719,24 +724,91 @@ void TestGpuDownloadProtectionMirrors() {
}
void TestCrossRegionUpload() {
constexpr uint64_t region_size = 4ull * 1024ull * 1024ull;
TrackerHarness harness;
auto &tracker = harness.tracker;
auto &page_manager = harness.page_manager;
const auto page_size = page_manager.GetPageSize();
constexpr auto region_size = Libs::Graphics::TRACKER_REGION_SIZE;
constexpr auto page_size = Libs::Graphics::TRACKER_PAGE_SIZE;
auto *memory = AllocateFixedGuestRange(region_size * 2, 0x10000);
const auto address = reinterpret_cast<uint64_t>(memory);
const auto boundary = (address + region_size - 1) & ~(region_size - 1);
uint32_t ranges = 0;
tracker.ForEachUploadRange(
boundary - page_size, page_size * 2, false,
[&](uint64_t, uint64_t) noexcept { ranges++; }, []() noexcept {});
Check(ranges == 2 &&
!tracker.IsRegionCpuModified(boundary - page_size, page_size * 2) &&
!IsWritable(reinterpret_cast<void *>(boundary - page_size)) &&
!IsWritable(reinterpret_cast<void *>(boundary)),
"cross-region upload did not clear and protect both regions");
tracker.MarkRegionAsCpuModified(boundary - page_size, page_size * 2);
for (const bool is_written : {false, true}) {
TrackerHarness harness;
auto &tracker = harness.tracker;
uint32_t ranges = 0;
tracker.ForEachUploadRange(
boundary - page_size, page_size * 2, is_written,
[&](uint64_t start, uint64_t size) noexcept {
Check(start == boundary - page_size + ranges * page_size &&
size == page_size,
"cold upload did not visit both region boundaries exactly");
ranges++;
},
[&]() noexcept {
Check(ranges == 2 &&
Protection(reinterpret_cast<void *>(boundary - page_size)) ==
PAGE_READONLY &&
Protection(reinterpret_cast<void *>(boundary)) == PAGE_READONLY,
"cold upload completed before clearing and protecting both regions");
});
for (const auto page : {boundary - page_size, boundary}) {
Check(!tracker.IsRegionCpuModified(page, page_size) &&
tracker.IsRegionGpuModified(page, page_size) == is_written &&
Protection(reinterpret_cast<void *>(page)) ==
(is_written ? PAGE_NOACCESS : PAGE_READONLY),
"cross-region upload did not preserve final ownership and protection");
}
Check(IsWritable(reinterpret_cast<void *>(boundary - 2 * page_size)) &&
IsWritable(reinterpret_cast<void *>(boundary + page_size)),
"cross-region upload changed neighboring pages");
tracker.UnmarkRegionAsGpuModified(boundary - page_size, 2 * page_size);
tracker.UntrackMemory(address, region_size * 2);
}
Release(memory);
}
void TestConcurrentColdUploads() {
constexpr auto region_size = Libs::Graphics::TRACKER_REGION_SIZE;
constexpr auto page_size = Libs::Graphics::TRACKER_PAGE_SIZE;
TrackerHarness harness;
auto &tracker = harness.tracker;
auto *memory = AllocateFixedGuestRange(region_size * 2, region_size);
const auto address = reinterpret_cast<uint64_t>(memory);
std::counting_semaphore<2> start{0};
std::counting_semaphore<2> upload_entered{0};
std::counting_semaphore<2> finish_upload{0};
std::vector<std::jthread> workers;
for (const auto page : {address, address + region_size}) {
workers.emplace_back([&, page] {
start.acquire();
uint32_t ranges = 0;
tracker.ForEachUploadRange(
page, page_size, true,
[&](uint64_t upload_address, uint64_t upload_size) noexcept {
Check(upload_address == page && upload_size == page_size,
"concurrent cold upload lost its dirty page");
ranges++;
},
[&]() noexcept {
Check(ranges == 1, "concurrent cold upload skipped its dirty page");
upload_entered.release();
finish_upload.acquire();
});
});
}
start.release(2);
const bool first_entered = upload_entered.try_acquire_for(std::chrono::seconds(5));
const bool second_entered = upload_entered.try_acquire_for(std::chrono::seconds(5));
finish_upload.release(2);
for (auto &worker : workers) {
worker.join();
}
Check(first_entered && second_entered,
"cold writable uploads serialized disjoint regions");
for (const auto page : {address, address + region_size}) {
Check(!tracker.IsRegionCpuModified(page, page_size) &&
tracker.IsRegionGpuModified(page, page_size) &&
Protection(reinterpret_cast<void *>(page)) == PAGE_NOACCESS,
"concurrent cold upload did not retain GPU ownership");
tracker.UnmarkRegionAsGpuModified(page, page_size);
}
tracker.UntrackMemory(address, region_size * 2);
Release(memory);
}
@@ -1137,6 +1209,7 @@ int main(int argc, char **argv) {
TestExactDirtyIntervalsSharingTrackerPage();
TestGpuDownloadProtectionMirrors();
TestCrossRegionUpload();
TestConcurrentColdUploads();
TestUploadDoesNotSerializeDisjointRegion();
TestDownloadDoesNotSerializeDisjointRegion();
TestGpuUnmarkUsesRegionMask();
+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;
+38 -2
View File
@@ -1,12 +1,15 @@
#include "common/virtualMemory.h"
#include "graphics/host_gpu/pageManager.h"
#include <barrier>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
@@ -167,6 +170,7 @@ struct ProtectionCall {
uint64_t size;
};
std::vector<ProtectionCall> g_protection_ranges;
std::mutex g_protection_log_mutex;
bool ProtectAddressSpace(uint64_t vaddr, uint64_t size,
Common::VirtualMemory::Mode mode) {
@@ -177,8 +181,11 @@ bool ProtectAddressSpace(uint64_t vaddr, uint64_t size,
protection = PAGE_READWRITE;
}
DWORD old_protection = 0;
g_protection_calls++;
g_protection_ranges.push_back({vaddr, size});
{
std::lock_guard lock(g_protection_log_mutex);
g_protection_calls++;
g_protection_ranges.push_back({vaddr, size});
}
return VirtualProtect(reinterpret_cast<void *>(vaddr), size, protection,
&old_protection) != 0;
}
@@ -547,6 +554,34 @@ void TestReadWriteWatcherInteractions() {
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
void TestConcurrentSharedWatchers() {
constexpr size_t worker_count = 8;
PageManager manager;
auto *memory = Allocate(TRACKER_PAGE_SIZE);
const auto address = reinterpret_cast<uint64_t>(memory);
bool watched = false;
std::barrier phase(worker_count, [&]() noexcept {
watched = !watched;
Check(Protection(memory) == (watched ? PAGE_READONLY : PAGE_READWRITE),
"concurrent watchers lost protection or failed to restore writes");
});
std::vector<std::jthread> workers;
for (size_t i = 0; i < worker_count; i++) {
workers.emplace_back([&] {
for (size_t round = 0; round < 100; round++) {
manager.UpdatePageWatchers<true>(address, TRACKER_PAGE_SIZE);
phase.arrive_and_wait();
manager.UpdatePageWatchers<false>(address, TRACKER_PAGE_SIZE);
phase.arrive_and_wait();
}
});
}
for (auto &worker : workers) {
worker.join();
}
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
[[noreturn]] void RunDeathCase(const char *name) {
PageManager manager;
const auto page_size = manager.GetPageSize();
@@ -669,6 +704,7 @@ int main(int argc, char **argv) {
TestRegionMaskWatcherRanges();
TestRegionEndpointBatching();
TestReadWriteWatcherInteractions();
TestConcurrentSharedWatchers();
TestFatalPaths();
std::puts("PageManagerTests: all cases passed");
return 0;
File diff suppressed because it is too large. Load diff
+255
View File
@@ -24,6 +24,7 @@
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <semaphore>
#include <string>
#include <thread>
#include <utility>
@@ -60,6 +61,12 @@
#endif
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
namespace Libs::LibKernel::Memory {
bool TestWindowsBackingViewModes();
}
#endif
namespace Libs::Fiber {
struct FiberObject;
struct FiberOptParam;
@@ -458,6 +465,24 @@ void TestSparseBackingReadPreservesResidency() {
std::all_of(bytes.begin() + commit_size * 2, bytes.end(),
[](uint8_t value) { return value == 0xa7; }),
"sparse backing read did not copy resident pages and zero nonresident pages");
constexpr size_t prefix = 37, suffix = 53;
constexpr size_t clipped_size = prefix + commit_size + suffix;
std::fill(bytes.begin(), bytes.end(), 0x5a);
Check(test,
Libs::LibKernel::Memory::TryReadSparseBacking(
base + commit_size - prefix, bytes.data() + 1, clipped_size),
"sparse backing read rejected unaligned resident edges around a hole");
Check(test, bytes.front() == 0x5a && bytes[clipped_size + 1] == 0x5a,
"clipped sparse backing read overwrote destination canaries");
Check(test,
std::all_of(bytes.begin() + 1, bytes.begin() + 1 + prefix,
[](uint8_t value) { return value == 0x3c; }) &&
std::all_of(bytes.begin() + 1 + prefix, bytes.begin() + 1 + prefix + commit_size,
[](uint8_t value) { return value == 0; }) &&
std::all_of(bytes.begin() + 1 + prefix + commit_size,
bytes.begin() + 1 + clipped_size,
[](uint8_t value) { return value == 0xa7; }),
"clipped sparse backing read misplaced resident bytes or the intervening hole");
std::fill(bytes.begin(), bytes.end(), 0x5a);
Check(test, Libs::LibKernel::Memory::TryReadSparseBacking(base + commit_size, bytes.data(),
bytes.size()),
@@ -580,6 +605,107 @@ void TestSparseReadDuringDirectCommit() {
std::printf("[host] %-48s ok\n", test);
}
std::counting_semaphore<2>* g_backing_reads_entered = nullptr;
std::counting_semaphore<2>* g_continue_backing_reads = nullptr;
void ParkBackingRead(uintptr_t, size_t) {
g_backing_reads_entered->release();
g_continue_backing_reads->acquire();
}
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}}) {
const auto base = MapNamedFlexible(test, SceKernelPageSize, SceKernelProtCpuRw,
"concurrent_backing_reads");
std::memcpy(reinterpret_cast<void*>(base), &original, sizeof(original));
std::counting_semaphore<2> entered {0};
std::counting_semaphore<2> resume {0};
g_backing_reads_entered = &entered;
g_continue_backing_reads = &resume;
TestSetBackingReadCallback(ParkBackingRead);
std::array<uint64_t, 2> values {};
std::array<bool, 2> results {};
std::thread first([&] { results[0] = readers.first(base, &values[0], sizeof(values[0])); });
std::thread second(
[&] { results[1] = readers.second(base, &values[1], sizeof(values[1])); });
const bool concurrent = entered.try_acquire_for(std::chrono::seconds(5)) &&
entered.try_acquire_for(std::chrono::seconds(5));
// Release both threads even if an exclusive read lock prevents the second
// entry.
resume.release(2);
first.join();
second.join();
TestSetBackingReadCallback(nullptr);
CheckOk(test, KernelMunmap(base, SceKernelPageSize), "KernelMunmap");
Check(test, concurrent, "backing reads could not hold their locks concurrently");
Check(test, results[0] && results[1] && values[0] == original && values[1] == original,
"concurrent backing reads returned different contents");
}
std::printf("[host] %-48s ok\n", test);
}
void TestBackingReadExcludesWritesAndUnmap() {
using namespace Libs::LibKernel::Memory;
const char* test = "BackingReadExcludesWritesAndUnmap";
constexpr uint64_t original = 0x123456789abcdef0ull;
constexpr uint64_t replacement = 0xfedcba9876543210ull;
for (const auto read: {TryReadBacking, TryReadSparseBacking}) {
for (const bool unmap: {false, true}) {
const auto base = MapNamedFlexible(test, SceKernelPageSize, SceKernelProtCpuRw,
"backing_read_exclusion");
std::memcpy(reinterpret_cast<void*>(base), &original, sizeof(original));
std::counting_semaphore<2> entered {0};
std::counting_semaphore<2> resume {0};
g_backing_reads_entered = &entered;
g_continue_backing_reads = &resume;
TestSetBackingReadCallback(ParkBackingRead);
uint64_t snapshot = 0;
bool read_ok = false;
std::thread reader([&] { read_ok = read(base, &snapshot, sizeof(snapshot)); });
const bool reader_parked = entered.try_acquire_for(std::chrono::seconds(5));
std::binary_semaphore started {0};
std::binary_semaphore finished {0};
bool write_ok = false;
int unmap_result = OK;
std::thread mutation([&] {
started.release();
if (unmap) {
unmap_result = KernelMunmap(base, SceKernelPageSize);
} else {
write_ok = TryWriteBacking(base, &replacement, sizeof(replacement));
}
finished.release();
});
started.acquire();
const bool excluded = !finished.try_acquire_for(std::chrono::milliseconds(100));
resume.release();
reader.join();
mutation.join();
TestSetBackingReadCallback(nullptr);
uint64_t after = 0;
const bool still_backed = TryReadBacking(base, &after, sizeof(after));
if (!unmap) {
CheckOk(test, KernelMunmap(base, SceKernelPageSize), "KernelMunmap");
}
Check(test, reader_parked && excluded,
"backing write or unmap completed while a backing read held its "
"lock");
Check(test, read_ok && snapshot == original,
"backing mutation changed an in-progress read");
Check(test,
unmap ? unmap_result == OK && !still_backed
: write_ok && still_backed && after == replacement,
"backing mutation failed after the reader released its lock");
}
}
std::printf("[host] %-48s ok\n", test);
}
void TestGuestAddressSpaceHasNoFixedFallback() {
const char* test = "GuestAddressSpaceHasNoFixedFallback";
const auto unowned_address = reinterpret_cast<void*>(0x10000);
@@ -1355,6 +1481,118 @@ void TestPartialUnmapPreservesHostPermissions() {
}
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
void TestWindowsBackingViewPermissions() {
const char* test = "WindowsBackingViewPermissions";
Check(test, Libs::LibKernel::Memory::TestWindowsBackingViewModes(),
"backing view permissions differ from the requested mode");
std::printf("[host] %-48s ok\n", test);
}
#endif
void TestDirectMappingNamesTypesAndValidation() {
using namespace Libs::LibKernel::Memory;
const char* test = "DirectMappingNamesTypesAndValidation";
constexpr auto page = SceKernelPageSize;
int64_t physical = -1;
CheckOk(test, KernelAllocateDirectMemory(0, KernelGetDirectMemorySize(), page * 2, page,
SceKernelMtypeC, &physical),
"KernelAllocateDirectMemory");
void* named = nullptr;
const std::string long_name(KERNEL_MAXIMUM_NAME_LENGTH, 'x');
Check(test,
KernelMapNamedDirectMemory(nullptr, 0, SceKernelProtCpuExec, 0, -1, 0, long_name.c_str()) ==
Libs::LibKernel::KERNEL_ERROR_ENAMETOOLONG,
"named-map validation no longer checks the name first");
Check(test, KernelMapNamedDirectMemory(&named, page, SceKernelProtCpuRw, 0, physical, page,
nullptr) == Libs::LibKernel::KERNEL_ERROR_EFAULT && !named,
"null-name failure changed the output");
CheckOk(test, KernelReserveVirtualRange(&named, page * 2, 0, page), "KernelReserveVirtualRange");
const auto base = reinterpret_cast<uint64_t>(named);
const auto name = long_name.substr(1);
CheckOk(test, KernelMapNamedDirectMemory(&named, page, SceKernelProtCpuRw, SceKernelMapFixed,
physical, page, name.c_str()),
"KernelMapNamedDirectMemory");
void* second = reinterpret_cast<void*>(base + page);
CheckOk(test, KernelMapNamedDirectMemory(&second, page, SceKernelProtCpuRw, SceKernelMapFixed,
physical + page, page, "second"),
"KernelMapNamedDirectMemory(adjacent)");
ExpectRange(test, Query(test, base), base, base + page, SceKernelProtCpuRw,
0, 1, 0, 1, name.c_str(), physical);
ExpectRange(test, Query(test, base + page), base + page, base + page * 2, SceKernelProtCpuRw,
0, 1, 0, 1, "second", physical + page);
void* typed = nullptr;
constexpr int type = 2;
CheckOk(test, KernelMapDirectMemory2(&typed, page, type, SceKernelProtCpuRw, 0, physical, page),
"KernelMapDirectMemory2");
Check(test, Query(test, reinterpret_cast<uint64_t>(typed)).memory_type == type &&
Query(test, base).memory_type == SceKernelMtypeC,
"typed alias changed another mapping's type");
*static_cast<volatile uint64_t*>(typed) = 0x12345678;
Check(test, *static_cast<volatile uint64_t*>(named) == 0x12345678,
"typed mapping stopped aliasing its backing");
void* unchanged_type = nullptr;
CheckOk(test, KernelMapDirectMemory2(&unchanged_type, page, -1, SceKernelProtCpuRw, 0,
physical, page),
"KernelMapDirectMemory2(unchanged type)");
Check(test, Query(test, reinterpret_cast<uint64_t>(unchanged_type)).memory_type == SceKernelMtypeC,
"type -1 did not preserve the allocation's memory type");
void* plain = nullptr;
CheckOk(test, KernelMapDirectMemory(&plain, page, SceKernelProtCpuRw, 0, physical, page),
"KernelMapDirectMemory");
const auto plain_info = Query(test, reinterpret_cast<uint64_t>(plain));
Check(test, plain_info.memory_type == SceKernelMtypeC && plain_info.name[0] == '\0',
"plain mapping inherited another alias's type or name");
CheckOk(test, KernelReleaseDirectMemory(physical, page * 2), "KernelReleaseDirectMemory");
ExpectUnmapped(test, base);
ExpectUnmapped(test, reinterpret_cast<uint64_t>(typed));
ExpectUnmapped(test, reinterpret_cast<uint64_t>(plain));
ExpectUnmapped(test, reinterpret_cast<uint64_t>(unchanged_type));
std::printf("[host] %-48s ok\n", test);
}
void TestBatchMappingOperationsAndPartialFailure() {
using namespace Libs::LibKernel::Memory;
const char* test = "BatchMappingOperationsAndPartialFailure";
constexpr auto page = SceKernelPageSize;
int64_t physical = -1;
CheckOk(test, KernelAllocateDirectMemory(0, KernelGetDirectMemorySize(), page, page,
SceKernelMtypeC, &physical),
"KernelAllocateDirectMemory");
void* reservation = nullptr;
CheckOk(test, KernelReserveVirtualRange(&reservation, page * 2, 0, page), "KernelReserveVirtualRange");
const auto base = reinterpret_cast<uint64_t>(reservation);
void* flexible = reinterpret_cast<void*>(base + page);
std::array<KernelBatchMapEntry, 4> entries {{
{reservation, static_cast<uint64_t>(physical), page, SceKernelProtCpuRw, 0, 0, 0},
{flexible, 0, page, SceKernelProtCpuRw, 0, 0, 3},
{reservation, 0, page, SceKernelProtCpuRead, 0, 0, 2},
{flexible, 0, page, SceKernelProtCpuRead, 2, 0, 4},
}};
int processed = -1;
CheckOk(test, KernelBatchMap(entries.data(), entries.size(), &processed), "KernelBatchMap");
Check(test, processed == entries.size(), "batch did not process every operation");
ExpectRange(test, Query(test, base), base, base + page, SceKernelProtCpuRead,
0, 1, 0, 1, "anon", physical);
ExpectRange(test, Query(test, base + page), base + page, base + page * 2, SceKernelProtCpuRead,
1, 0, 0, 1, "anon");
std::array<KernelBatchMapEntry, 3> partial {{
{reservation, 0, page, 0, 0, 0, 1},
{nullptr, static_cast<uint64_t>(physical), page, SceKernelProtCpuRw, 0, 0, 0},
{flexible, 0, page, 0, 0, 0, 1},
}};
Check(test, KernelBatchMap(partial.data(), partial.size(), &processed) ==
Libs::LibKernel::KERNEL_ERROR_EINVAL && processed == 1,
"failed fixed mapping did not preserve the completed prefix count");
ExpectUnmapped(test, base);
Check(test, Query(test, base + page).is_flexible == 1, "batch processed an entry after failure");
CheckOk(test, KernelBatchMap2(&partial.back(), 1, &processed, 0), "KernelBatchMap2(unmap)");
Check(test, processed == 1, "batch unmap count is wrong");
ExpectUnmapped(test, base + page);
CheckOk(test, KernelReleaseDirectMemory(physical, page), "KernelReleaseDirectMemory");
std::printf("[host] %-48s ok\n", test);
}
void TestDirectMapValidationBeforeOwnerMutation() {
const char* test = "DirectMapValidationBeforeOwnerMutation";
int64_t invalid = -1;
@@ -4178,6 +4416,16 @@ void TestSmallFiberStacksAndMigration() {
int main(int argc, char** argv) {
InitSubsystems();
if (argc == 2 && std::strcmp(argv[1], "--backing-transfers-only") == 0) {
RunTest(TestConcurrentBackingReads);
RunTest(TestBackingReadExcludesWritesAndUnmap);
RunTest(TestSparseBackingReadPreservesResidency);
RunTest(TestSparseReadDuringDirectCommit);
RunTest(TestDirectMapQueryOffsetAndPartialMunmap);
RunTest(TestFlexibleMemoryUsesSharedBacking);
RunTest(TestFlexibleMemoryReuseIsZeroFilled);
return g_failed_tests == 0 ? 0 : 1;
}
#if defined(__linux__)
if (argc == 2 && std::strcmp(argv[1], "--fixed-direct-replacement-only") == 0) {
RunTest(TestFixedDirectReplacementPreservesAccess);
@@ -4226,6 +4474,8 @@ int main(int argc, char** argv) {
RunTest(TestGuestAddressSpaceOwnsReservationsBeforeBacking);
RunTest(TestSparseBackingReadPreservesResidency);
RunTest(TestSparseReadDuringDirectCommit);
RunTest(TestConcurrentBackingReads);
RunTest(TestBackingReadExcludesWritesAndUnmap);
RunTest(TestGuestAddressSpaceHasNoFixedFallback);
RunTest(TestGuestFreeRangeSearchDoesNotUnderflow);
RunTest(TestFlexibleMemoryCapacityIsBootFixed);
@@ -4249,6 +4499,11 @@ int main(int argc, char** argv) {
#if defined(__linux__)
RunTest(TestPartialUnmapPreservesHostPermissions);
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
RunTest(TestWindowsBackingViewPermissions);
#endif
RunTest(TestDirectMappingNamesTypesAndValidation);
RunTest(TestBatchMappingOperationsAndPartialFailure);
RunTest(TestDirectMapValidationBeforeOwnerMutation);
RunTest(TestDirectReleaseRollbackRestoresOwnerMapping);
RunTest(TestDirectReleaseContracts);
+227
View File
@@ -1,6 +1,8 @@
#include "common/hostException.h"
#include "loader/guestInstructionPatcher.h"
#include "loader/x64InstructionEmulator.h"
#include <array>
#include <atomic>
#include <bit>
#include <cstddef>
@@ -82,6 +84,53 @@ constexpr uint8_t kUd2[] = {
0xc3, // ret
};
// VRSQRTPS xmm1, xmm0 as the AMD CPU patcher leaves it when it cannot relocate the instruction: a
// reserved VEX.vvvv bit is cleared (c5 f8 -> c5 f0), which raises #UD on every host. Rosetta aborted
// the guest on exactly this encoding until the emulator handled it there too.
constexpr uint8_t kTrappedVrsqrtpsXmm1Xmm0[] = {
0x0f, 0x10, 0x06, // movups (%rsi), %xmm0
0xc5, 0xfc, 0x10, 0x0e, // vmovups ymm1, [rsi]: dirty the upper half of ymm1
0xc5, 0xf0, 0x52, 0xc8, // vrsqrtps xmm1, xmm0 with the vvvv trap mark
0x0f, 0x11, 0x0f, // movups %xmm1, (%rdi)
0xc4, 0xe3, 0x7d, 0x19, 0x4f, 0x10, 0x01, // vextractf128 [rdi + 16], ymm1, 1
0xc5, 0xf8, 0x77, // vzeroupper
0xc3, // ret
};
// The same sequence with the ordinary VEX.128 VRSQRTPS encoding (vvvv = 1111), as it appears in guest
// code before the patcher marks it as a trap.
constexpr uint8_t kPlainVrsqrtpsXmm1Xmm0[] = {
0x0f, 0x10, 0x06, // movups (%rsi), %xmm0
0xc5, 0xfc, 0x10, 0x0e, // vmovups ymm1, [rsi]: dirty the upper half of ymm1
0xc5, 0xf8, 0x52, 0xc8, // vrsqrtps xmm1, xmm0
0x0f, 0x11, 0x0f, // movups %xmm1, (%rdi)
0xc4, 0xe3, 0x7d, 0x19, 0x4f, 0x10, 0x01, // vextractf128 [rdi + 16], ymm1, 1
0xc5, 0xf8, 0x77, // vzeroupper
0xc3, // ret
};
// The same again behind a never taken indirect branch. A function with an indirect branch does not
// let the patcher relocate neighbours, and the 4-byte VRSQRTPS cannot hold a jump on its own, so it
// is left in place with the trap mark.
constexpr uint8_t kPlainVrsqrtpsIndirectBranch[] = {
0x31, 0xc0, // xor %eax, %eax
0x85, 0xc0, // test %eax, %eax
0x74, 0x02, // jz over
0xff, 0xe0, // jmp *%rax (never taken)
0x0f, 0x10, 0x06, // movups (%rsi), %xmm0
0xc5, 0xfc, 0x10, 0x0e, // vmovups ymm1, [rsi]: dirty the upper half of ymm1
0xc5, 0xf8, 0x52, 0xc8, // vrsqrtps xmm1, xmm0
0x0f, 0x11, 0x0f, // movups %xmm1, (%rdi)
0xc4, 0xe3, 0x7d, 0x19, 0x4f, 0x10, 0x01, // vextractf128 [rdi + 16], ymm1, 1
0xc5, 0xf8, 0x77, // vzeroupper
0xc3, // ret
};
constexpr uint8_t kMwaitx[] = {
0x0f, 0x01, 0xfb, // mwaitx
0xc3, // ret
};
constexpr size_t kPageSize = 4096;
std::atomic<int> g_illegal_hits {0};
@@ -233,6 +282,13 @@ void TestInsertqRegister() {
}
}
void TestMwaitxEmulated() {
auto fn = MapCode<VoidBlobFn>(kMwaitx);
const int before = IllegalHits();
fn();
Check(IllegalHits() > before, "host raises SIGILL for MWAITX");
}
void TestUnknownInstructionRefused() {
g_refused.store(false, std::memory_order_relaxed);
g_refuse_skip.store(2, std::memory_order_relaxed);
@@ -244,6 +300,170 @@ void TestUnknownInstructionRefused() {
g_refuse_skip.store(0, std::memory_order_relaxed);
}
// The loader rewrites SSE4a instructions into native trampolines when the patcher is enabled. The
// trampoline area lives right behind the code page, like it does behind a mapped guest module.
struct PatchedCode {
void *code = nullptr;
Loader::GuestInstructionPatchResult result {};
};
template <size_t N>
PatchedCode MapPatchedCode(const uint8_t (&blob)[N], bool emulate_amd) {
void *area = ::mmap(nullptr, 2 * kPageSize, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANON, -1, 0);
Check(area != MAP_FAILED, "map RWX code and trampoline area");
std::memcpy(area, blob, N);
auto *trampoline = static_cast<uint8_t *>(area) + kPageSize;
Loader::RegisterGuestInstructionPatchModule(area, kPageSize, trampoline, kPageSize);
const std::array<uintptr_t, 1> function_starts {reinterpret_cast<uintptr_t>(area)};
// A host without SSE4a, RDPID and CLWB, which is what Rosetta presents.
const Loader::GuestInstructionHostFeatures host {false, false, false};
PatchedCode patched;
patched.code = area;
patched.result = Loader::PatchGuestInstructions(reinterpret_cast<uintptr_t>(area), N, function_starts,
false, emulate_amd, host);
Loader::UnregisterGuestInstructionPatchModule(area);
return patched;
}
using PatchCounts = Loader::InstructionPatchCounts Loader::GuestInstructionPatchResult::*;
// Runs the blob through the SIGILL emulator (reference) and through the patched copy, which must
// produce the same bytes. EXTRQ becomes a native trampoline without a single SIGILL. INSERTQ is
// marked as a trap by the patcher and keeps going through the signal emulator.
template <typename Fn, size_t N, typename Invoke>
void CheckPatchedMatchesEmulator(const uint8_t (&blob)[N], PatchCounts counts, bool native,
Invoke invoke, const char *text) {
uint64_t reference[2] {};
const int before_reference = IllegalHits();
invoke(MapCode<Fn>(blob), reference);
Check(IllegalHits() > before_reference, text);
const auto unpatched = MapPatchedCode(blob, false);
Check((unpatched.result.*counts).found == 0, "patcher stays off without emulate_amd");
uint64_t unpatched_out[2] {};
const int before_off = IllegalHits();
invoke(reinterpret_cast<Fn>(unpatched.code), unpatched_out);
Check(IllegalHits() > before_off, "unpatched copy still raises SIGILL");
const auto patched = MapPatchedCode(blob, true);
Check((patched.result.*counts).found == 1, text);
Check((patched.result.*counts).native == (native ? 1u : 0u), "native trampoline count");
Check((patched.result.*counts).trapped == (native ? 0u : 1u), "trapped (signal path) count");
uint64_t out[2] {};
const int before = IllegalHits();
invoke(reinterpret_cast<Fn>(patched.code), out);
if (native) {
Check(IllegalHits() == before, "patched code raises no SIGILL");
} else {
Check(IllegalHits() > before, "trapped instruction still goes through the signal emulator");
}
Check(out[0] == reference[0] && out[1] == reference[1], "patched result equals emulated result");
}
void TestPatchedExtrqImmediate() {
CheckPatchedMatchesEmulator<BlobFn>(
kExtrqXmm2Len40Index0, &Loader::GuestInstructionPatchResult::extrq, true,
[](BlobFn fn, uint64_t *out) { fn(out, 0.0, 0.0, std::bit_cast<double>(0xaabbccddeeff1122ull)); },
"EXTRQ immediate found by the patcher");
}
void TestPatchedExtrqRexHigh() {
CheckPatchedMatchesEmulator<LoadBlobFn>(
kExtrqXmm10Len40Index0, &Loader::GuestInstructionPatchResult::extrq, true,
[](LoadBlobFn fn, uint64_t *out) {
const uint64_t in[2] {0xaabbccddeeff1122ull, 0x5555555555555555ull};
fn(out, in);
},
"REX EXTRQ found by the patcher");
}
void TestPatchedInsertqImmediate() {
CheckPatchedMatchesEmulator<BlobFn>(
kInsertqXmm2Xmm0Len32Index0, &Loader::GuestInstructionPatchResult::insertq, false,
[](BlobFn fn, uint64_t *out) {
fn(out, std::bit_cast<double>(0x1122334455667788ull), 0.0,
std::bit_cast<double>(0x0123456789abcdefull));
},
"INSERTQ immediate found by the patcher");
}
void TestPatchedInsertqIndexedRexHigh() {
CheckPatchedMatchesEmulator<PairBlobFn>(
kInsertqXmm10Xmm9Len16Index8, &Loader::GuestInstructionPatchResult::insertq, false,
[](PairBlobFn fn, uint64_t *out) {
const uint64_t src[2] {0x8877665544332211ull, 0xdeadbeefcafebabeull};
const uint64_t dest[2] {0xaaaaaaaaaaaaaaaaull, 0xbbbbbbbbbbbbbbbbull};
fn(out, src, dest);
},
"indexed REX INSERTQ found by the patcher");
}
void TestPatchedInsertqRegister() {
CheckPatchedMatchesEmulator<PairBlobFn>(
kInsertqXmm10Xmm9, &Loader::GuestInstructionPatchResult::insertq, false,
[](PairBlobFn fn, uint64_t *out) {
const uint64_t src[2] {0x0123456789abcdefull, 0xdeadbeef0000c4c8ull};
const uint64_t dest[2] {0x8877665544332211ull, 0xfedcba9876543210ull};
fn(out, src, dest);
},
"register INSERTQ found by the patcher");
}
// The ordinary VEX.128 VRSQRTPS must be found by the patcher and give the reference result, either
// through a native trampoline (no SIGILL) or, when it cannot be relocated, marked as a trap (the
// vvvv bit is cleared in place) and emulated from the signal handler.
template <size_t N>
void CheckPatchedReciprocalSquareRoot(const uint8_t (&blob)[N], bool native) {
const uint32_t in_bits[8] {std::bit_cast<uint32_t>(4.0f), 0x00000000u, 0xbf800000u, 0x7f800000u,
0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u};
const auto Run = [&](LoadBlobFn fn, uint32_t (&out_bits)[8]) {
fn(reinterpret_cast<uint64_t *>(out_bits), reinterpret_cast<const uint64_t *>(in_bits));
};
uint32_t reference[8] {};
const int before_reference = IllegalHits();
Run(MapCode<LoadBlobFn>(kTrappedVrsqrtpsXmm1Xmm0), reference);
Check(IllegalHits() > before_reference, "host raises SIGILL for the pre-marked VRSQRTPS");
const auto unpatched = MapPatchedCode(blob, false);
Check(unpatched.result.reciprocal_sqrt.found == 0, "patcher leaves VRSQRTPS alone without emulate_amd");
const auto patched = MapPatchedCode(blob, true);
const auto &counts = patched.result.reciprocal_sqrt;
Check(counts.found == 1, "patcher finds the ordinary VEX.128 VRSQRTPS");
Check(counts.native == (native ? 1u : 0u) && counts.trapped == (native ? 0u : 1u),
"VRSQRTPS native trampoline / trap count");
const auto *code = static_cast<const uint8_t *>(patched.code);
size_t site = 0;
while (site + 2 < N && !(blob[site] == 0xc5 && blob[site + 1] == 0xf8 && blob[site + 2] == 0x52)) {
++site;
}
Check(site + 2 < N, "locate VRSQRTPS in the test blob");
if (!native) {
Check(code[site] == 0xc5 && code[site + 1] == 0xf0 && code[site + 2] == 0x52,
"the trap mark clears a reserved VEX.vvvv bit (c5 f8 -> c5 f0)");
} else {
Check(code[site] != 0xc5 || code[site + 1] != 0xf8, "native trampoline replaced the VRSQRTPS");
}
uint32_t out[8] {};
const int before = IllegalHits();
Run(reinterpret_cast<LoadBlobFn>(patched.code), out);
if (native) {
Check(IllegalHits() == before, "natively patched VRSQRTPS raises no SIGILL");
} else {
Check(IllegalHits() > before, "marked VRSQRTPS goes through the signal emulator");
}
Check(std::memcmp(out, reference, sizeof(out)) == 0, "patched VRSQRTPS result equals the emulated reference");
Check(std::bit_cast<float>(out[0]) == 0.5f && out[1] == 0x7f800000u && out[2] == 0xffc00000u && out[3] == 0,
"patched VRSQRTPS produces the exact reciprocal square roots");
Check(out[4] == 0 && out[5] == 0 && out[6] == 0 && out[7] == 0, "patched VEX.128 form zeroes the upper half");
}
void TestPatchedReciprocalSquareRoot() {
CheckPatchedReciprocalSquareRoot(kPlainVrsqrtpsXmm1Xmm0, true);
CheckPatchedReciprocalSquareRoot(kPlainVrsqrtpsIndirectBranch, false);
}
} // namespace
int main() {
@@ -255,7 +475,14 @@ int main() {
TestRexHighRegisterExtrq();
TestRexHighRegisterInsertqWithIndex();
TestInsertqRegister();
TestMwaitxEmulated();
TestUnknownInstructionRefused();
TestPatchedExtrqImmediate();
TestPatchedExtrqRexHigh();
TestPatchedInsertqImmediate();
TestPatchedInsertqIndexedRexHigh();
TestPatchedInsertqRegister();
TestPatchedReciprocalSquareRoot();
std::printf("macosSse4aTests: all passed\n");
return 0;
}
+136 -37
View File
@@ -6539,6 +6539,7 @@ void TestPerspectiveCentroidInputs() {
ShaderMapUserData(regs.ps_regs.data_addr, mapped);
HW::ShaderRegisters sh{};
sh.ps_input_ena = sh.ps_input_addr = inputs;
sh.m_cbShaderMask = 0xf;
const std::array<Prospero::ColorComponentMapping, 8> mappings{};
ShaderPixelInputInfo pixel{};
(void)PrepareProgram(regs, sh, mappings, pixel);
@@ -6570,6 +6571,47 @@ void TestPerspectiveCentroidInputs() {
}
}
void TestPerspectiveSampleInputs() {
for (const uint32_t inputs : {1u, 3u, 5u, 7u}) {
const std::vector<uint32_t> shader = {
EncodeExp0(0x00, 0xf),
EncodeExp1(0, 1, inputs == 1 ? 0 : 2, inputs == 1 ? 1 : 3),
EncodeSopp(0x01)};
HW::PixelShaderInfo regs{};
regs.ps_regs.data_addr = reinterpret_cast<uint64_t>(shader.data());
ShaderMappedData mapped{};
mapped.code_size_bytes = shader.size() * sizeof(uint32_t);
ShaderMapUserData(regs.ps_regs.data_addr, mapped);
HW::ShaderRegisters sh{};
sh.ps_input_ena = sh.ps_input_addr = inputs;
sh.m_cbShaderMask = 0xf;
const std::array<Prospero::ColorComponentMapping, 8> mappings{};
ShaderPixelInputInfo pixel{};
(void)PrepareProgram(regs, sh, mappings, pixel);
Check(pixel.ps_perspective_sample_vgpr == 0 &&
pixel.ps_system_input_base == 2u * std::popcount(inputs),
"perspective sample pair did not occupy the first enabled VGPRs");
const auto key = MakeStageStaticKey(pixel);
pixel.ps_perspective_sample_vgpr = UINT32_MAX;
Check(key != MakeStageStaticKey(pixel), "sample input is missing from shader key");
pixel.ps_perspective_sample_vgpr = 0;
auto options = MakeCompileOptions(ShaderType::Pixel);
options.input_info.pixel = &pixel;
const auto result = RecompileForTest(shader, options);
const auto source = DisassembleSpirvBinary(result.spirv);
Check(SpirvContainsCapability(result.spirv, 52u) &&
SpirvHasDecorationValue(result.spirv, 11u, 18u) &&
source.find("InterpolateAtSample %gl_BaryCoordKHR") != std::string::npos &&
SpirvSourceHasInstructionUsing(source, "OpLoad", "%int %gl_SampleID") &&
SpirvSourceHasInstructionUsing(source, "OpCompositeExtract", " 1") &&
SpirvSourceHasInstructionUsing(source, "OpCompositeExtract", " 2"),
"sample I/J did not evaluate BaryCoordKHR Y/Z at SampleId");
Check(!SpirvHasDecorationValueWithDecoration(result.spirv, 11u, 5286u, 17u),
"sample interpolation changed the shared barycentric input location");
CheckSpirvBinaryValidates(result.spirv);
}
}
void TestPsInputCountRegisterDecode() {
HW::Context context;
// NUM_INTERP is 3 while bit 14 is an independent control flag that must be
@@ -10573,6 +10615,32 @@ void TestMergedShaderUserDataSnapshot() {
regs.gs_user_sgpr.value[HW::UserSgprInfo::SGPRS_MAX - 1]++;
Check(full.user_data.back() == 0x10001000u + HW::UserSgprInfo::SGPRS_MAX - 1,
"merged shader parameters borrowed mutable register storage");
regs.gs_regs.rsrc2.lds_size = 64;
regs.gs_regs.rsrc1.gs_vgpr_component_count = 1;
context.SetGsMaxVertOut(0);
for (const uint32_t wave_size : {32u, 64u}) {
context.SetShaderStages(wave_size == 32 ? 0x00400000u : 0u);
for (const uint16_t capacity : {64u, 128u, 256u}) {
context.SetMaxOutputPerSubgroup(capacity);
user_config.SetGeControl({capacity, capacity});
ShaderVertexInputInfo input{};
const auto params = PrepareProgram(regs, context, user_config, input);
Check(input.logical_stage == ShaderType::Mesh && input.mesh.wave_size == wave_size &&
input.mesh.lds_size_dwords == 8192 && input.mesh.max_vertices == capacity &&
input.mesh.max_primitives == capacity / 3 &&
input.mesh.primitives_per_group == capacity / 3 &&
input.mesh.vertices_per_group == capacity / 3 * 3 &&
input.mesh.threads_num[0] == capacity && params.back_code.empty() &&
params.user_data_count == 8u + HW::UserSgprInfo::SGPRS_MAX &&
params.user_data[0] == 0 && params.user_data[1] == 0 &&
std::equal(std::begin(regs.gs_user_sgpr.value), std::end(regs.gs_user_sgpr.value),
params.user_data.begin() + 8),
"non-amplifying NGG shader lost its workgroup, GE bounds, or native user SGPRs");
}
}
regs.gs_regs.rsrc2.lds_size = 0;
regs.gs_regs.rsrc1.gs_vgpr_component_count = 3;
context.SetShaderStages(0x20);
context.SetMaxOutputPerSubgroup(256);
context.SetGsMaxVertOut(8);
user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriFan);
@@ -10830,7 +10898,7 @@ void TestMeshInputAssembly() {
struct Case {
Prospero::PrimitiveType topology;
uint32_t capacity, count, group, lane, width, address_low, base_vertex;
uint32_t wave_info, first, second, third, byte_offset, vertex_id;
uint32_t wave_info, group_info, first, second, third, byte_offset, vertex_id;
bool fetch;
uint32_t wave_size = 64;
bool fast_launch = false;
@@ -10838,51 +10906,63 @@ void TestMeshInputAssembly() {
};
std::vector<Case> cases = {
{Prospero::PrimitiveType::kTriList, 14, 177, 14, 2, 2, 0x1002, UINT32_MAX,
0x40000309, 6, 7, 8, 340, 0xabcc, true},
0x40000309, 0x00c09000, 6, 7, 8, 340, 0xabcc, true},
{Prospero::PrimitiveType::kTriList, 14, 177, 14, 9, 2, 0x1002, 0,
0x40000309, 27, 28, 29, 356, 0, false},
0x40000309, 0x00c09000, 27, 28, 29, 356, 0, false},
{Prospero::PrimitiveType::kTriList, 8, 180, 0, 64, 2, 0x1002, 0,
0x41000000, 192, 193, 194, 128, 0, false},
0x41000000, 0x00806000, 192, 193, 194, 128, 0, false},
{Prospero::PrimitiveType::kTriList, 8, 180, 1, 1, 1, 0x1000, 5,
0x40000206, 3, 4, 5, 4, 0xb0, true},
0x40000206, 0x00806000, 3, 4, 5, 4, 0xb0, true},
{Prospero::PrimitiveType::kTriList, 8, 180, 1, 1, 4, 0x1000, 5,
0x40000206, 3, 4, 5, 28, 0xabcd0128, true},
0x40000206, 0x00806000, 3, 4, 5, 28, 0xabcd0128, true},
{Prospero::PrimitiveType::kTriStrip, 5, 8, 1, 1, 0, 0, 11,
0x40000305, 1, 2, 3, 0, 15, false},
0x40000305, 0x00c05000, 1, 2, 3, 0, 15, false},
{Prospero::PrimitiveType::kTriStrip, 5, 8, 0, 1, 0, 0, 11,
0x40000305, 2, 1, 3, 0, 12, false},
0x40000305, 0x00c05000, 2, 1, 3, 0, 12, false},
{Prospero::PrimitiveType::kTriFan, 32, 6, 0, 3, 2, 0x1002, 0,
0x40000406, 0, 4, 5, 8, 0x0123, true},
0x40000406, 0x01006000, 0, 4, 5, 8, 0x0123, true},
{Prospero::PrimitiveType::kTriFan, 5, 8, 1, 0, 2, 0x1002, 0,
0x40000305, 0, 1, 2, 0, 0xabcd, true},
0x40000305, 0x00c05000, 0, 1, 2, 0, 0xabcd, true},
{Prospero::PrimitiveType::kTriFan, 5, 8, 1, 1, 0, 0, 11,
0x40000305, 0, 2, 3, 0, 15, false},
0x40000305, 0x00c05000, 0, 2, 3, 0, 15, false},
{Prospero::PrimitiveType::kTriFan, 5, 7, 1, 3, 2, 0x1002, 11,
0x40000204, 0, 4, 5, 12, 0xabd8, true},
0x40000204, 0x00804000, 0, 4, 5, 12, 0xabd8, true},
{Prospero::PrimitiveType::kTriFan, 5, 7, 1, 4, 2, 0x1002, 0,
0x40000204, 0, 5, 6, 16, 0, false},
0x40000204, 0x00804000, 0, 5, 6, 16, 0, false},
{Prospero::PrimitiveType::kTriFan, 5, 8, 1, 64, 4, 0x1000, 0,
0x41000000, 0, 65, 66, 268, 0, false},
0x41000000, 0x00c05000, 0, 65, 66, 268, 0, false},
{Prospero::PrimitiveType::kLineList, 5, 17, 3, 0, 0, 0, 11,
0x40000204, 0, 1, 0, 0, 23, false},
0x40000204, 0x00804000, 0, 1, 0, 0, 23, false},
{Prospero::PrimitiveType::kLineList, 6, 17, 2, 4, 2, 0x1002, 11,
0x40000205, 8, 9, 0, 32, 0xabd8, true},
0x40000205, 0x00805000, 8, 9, 0, 32, 0xabd8, true},
{Prospero::PrimitiveType::kLineList, 6, 17, 2, 5, 2, 0x1002, 0,
0x40000205, 10, 11, 0, 36, 0, false},
0x40000205, 0x00805000, 10, 11, 0, 36, 0, false},
{Prospero::PrimitiveType::kLineList, 8, 180, 1, 64, 4, 0x1000, 0,
0x41000000, 128, 129, 0, 288, 0, false},
0x41000000, 0x01008000, 128, 129, 0, 288, 0, false},
{Prospero::PrimitiveType::kPointList, 12, 265, 22, 0, 2, 0x1002, 0,
0x40000101, 0, 0, 0, 528, 0xabcd, true},
0x40000101, 0x00401000, 0, 0, 0, 528, 0xabcd, true},
{Prospero::PrimitiveType::kPointList, 12, 265, 22, 1, 2, 0x1002, 0,
0x40000101, 1, 0, 0, 532, 0, false},
0x40000101, 0x00401000, 1, 0, 0, 532, 0, false},
{Prospero::PrimitiveType::kPointList, 1, 1, 0, 0, 0, 0, 11,
0x40000101, 0, 0, 0, 0, 11, false},
0x40000101, 0x00401000, 0, 0, 0, 0, 11, false},
{Prospero::PrimitiveType::kPointList, 12, 265, 1, 1, 4, 0x1000, 5,
0x40000c0c, 1, 0, 0, 52, 0xabcd0128, true},
0x40000c0c, 0x0300c000, 1, 0, 0, 52, 0xabcd0128, true},
{Prospero::PrimitiveType::kPointList, 12, 265, 1, 64, 4, 0x1000, 0,
0x41000000, 64, 0, 0, 304, 0, false},
0x41000000, 0x0300c000, 64, 0, 0, 304, 0, false},
{Prospero::PrimitiveType::kTriStrip, 40, 40, 0, 32, 0, 0, 11,
0x81000608, 32, 33, 34, 0, 43, false, 32},
0x81000608, 0x09828000, 32, 33, 34, 0, 43, false, 32},
{Prospero::PrimitiveType::kTriList, 64, 64, 0, 0, 0, 0, 11,
0x1000153f, 0x0543f000, 0, 1, 2, 0, 11, false, 64, false, 64},
{Prospero::PrimitiveType::kTriList, 128, 128, 0, 64, 0, 0, 11,
0x2100003e, 0x0a87e000, 192, 193, 194, 0, 75, false, 64, false, 128},
{Prospero::PrimitiveType::kTriList, 256, 256, 0, 192, 0, 0, 11,
0x4300003f, 0x154ff000, 576, 577, 578, 0, 203, false, 64, false, 256},
{Prospero::PrimitiveType::kTriList, 64, 64, 0, 32, 0, 0, 11,
0x2100001f, 0x0543f000, 96, 97, 98, 0, 43, false, 32, false, 64},
{Prospero::PrimitiveType::kTriList, 128, 128, 0, 32, 0, 0, 11,
0x41000a20, 0x0a87e000, 96, 97, 98, 0, 43, false, 32, false, 128},
{Prospero::PrimitiveType::kTriList, 256, 256, 0, 64, 0, 0, 11,
0x82001520, 0x154ff000, 192, 193, 194, 0, 75, false, 32, false, 256},
};
for (const uint32_t wave_size : {32u, 64u}) {
for (const uint32_t threads : {wave_size, 2u * wave_size}) {
@@ -10893,7 +10973,7 @@ void TestMeshInputAssembly() {
(lane < wave_size ? 0x101u : 0u);
const uint32_t base_vertex = group == 11u ? UINT32_MAX : 11u;
cases.push_back({Prospero::PrimitiveType::kPointList, 1, 12, group, lane, 0, 0,
base_vertex, wave_info, 0, 0, 0, 0, group + base_vertex, false,
base_vertex, wave_info, 0x00401000, 0, 0, 0, 0, group + base_vertex, false,
wave_size, true, threads});
}
}
@@ -10956,15 +11036,16 @@ void TestMeshInputAssembly() {
ConstantPropagationPass(program.blocks);
}
std::array<uint32_t, 9> vgprs{};
uint32_t sgpr3 = 0;
std::array<uint32_t, 4> sgprs{};
for (const auto &inst : *program.blocks.front()) {
if (inst.GetOpcode() == ValueOpcode::SetVectorRegister) {
vgprs[RegIndex(inst.Arg(0).VectorRegister())] = inst.Arg(1).Resolve().U32();
} else if (inst.GetOpcode() == ValueOpcode::SetScalarRegister) {
sgpr3 = inst.Arg(1).Resolve().U32();
sgprs[RegIndex(inst.Arg(0).ScalarRegister())] = inst.Arg(1).Resolve().U32();
}
}
Check(sgpr3 == test.wave_info && vgprs[0] == ((test.first << 2) | (test.second << 18)) &&
Check(sgprs[2] == test.group_info && sgprs[3] == test.wave_info &&
vgprs[0] == ((test.first << 2) | (test.second << 18)) &&
vgprs[1] == test.third * 4 && vgprs[5] == test.vertex_id &&
vgprs[test.fast_launch ? 6 : 8] == 9u,
"mesh prolog changed input assembly, wave counts, vertex ID, or instance ID");
@@ -12981,9 +13062,23 @@ void TestRenderTargetReverseExportMapping() {
"inactive reverse MRT mapping was not normalized out of the shader "
"cache key");
sh.target_output_mode[0] = 4;
sh.m_cbShaderMask = 0xf;
PrepareProgram(regs, sh, mappings, compiled_info);
Check(compiled_info.target_export_mapping[0] == gr32.export_mapping,
"active reverse MRT mapping was lost before shader specialization");
const auto first_slot_key = MakeStageStaticKey(compiled_info);
sh.m_cbShaderMask = 0xf000;
mappings[3] = gr32.export_mapping;
PrepareProgram(regs, sh, mappings, compiled_info);
Check(compiled_info.target_shader_mask == sh.m_cbShaderMask &&
compiled_info.target_export_mapping[0] == gr32.export_mapping &&
first_slot_key != MakeStageStaticKey(compiled_info),
"compact MRT ordinal lost its physical export mapping or cache identity");
const auto sparse_slot_key = MakeStageStaticKey(compiled_info);
sh.m_cbShaderMask = 0x1000;
PrepareProgram(regs, sh, mappings, compiled_info);
Check(sparse_slot_key == MakeStageStaticKey(compiled_info),
"component-only shader mask changes introduced a duplicate shader variant");
}
void TestBlendMappingClassification() {
@@ -14675,13 +14770,14 @@ void TestNewShaderRecompilerSpirvSizeBaselines() {
EncodeSopp(0x01),
};
const auto structured_result = compile("structured-phi", structured_phi,
{.words = 140,
.instructions = 41,
{.words = 174,
.instructions = 49,
.phis = 1,
.labels = 8,
.loop_merges = 1,
.branches = 6,
.conditional_branches = 1});
.conditional_branches = 1,
.ballots = 2});
const auto structured_metrics = MeasureSpirv(structured_result.spirv);
Check((structured_result.ir_dump.find("Phi") != std::string::npos),
"structured Phi size fixture no longer contains an IR Phi");
@@ -14692,13 +14788,14 @@ void TestNewShaderRecompilerSpirvSizeBaselines() {
CheckSpirvPhiParents(structured_result.spirv);
const auto structured_repeat =
compile("structured-phi-repeat", structured_phi,
{.words = 140,
.instructions = 41,
{.words = 174,
.instructions = 49,
.phis = 1,
.labels = 8,
.loop_merges = 1,
.branches = 6,
.conditional_branches = 1});
.conditional_branches = 1,
.ballots = 2});
Check(structured_repeat.spirv == structured_result.spirv,
"deferred Phi patching is not deterministic");
@@ -14862,8 +14959,8 @@ void TestNewShaderRecompilerSpirvSizeBaselines() {
EncodeSopp(0x01),
};
const auto dispatcher_result = compile("dispatcher", dispatcher,
{.words = 242,
.instructions = 67,
{.words = 255,
.instructions = 70,
.variables = 3,
.function_variables = 3,
.loads = 3,
@@ -14874,7 +14971,8 @@ void TestNewShaderRecompilerSpirvSizeBaselines() {
.selection_merges = 1,
.branches = 10,
.conditional_branches = 1,
.switches = 1});
.switches = 1,
.ballots = 1});
Check(dispatcher_result.program.dispatcher_fallback &&
(dispatcher_result.ir_dump.find("Phi") != std::string::npos) &&
SpirvInstructionOpcodeCount(dispatcher_result.spirv, 245u) == 2u &&
@@ -15054,6 +15152,7 @@ int main() {
TestNewShaderRecompilerStageInputInfo();
TestCustomVintrpMovTranslation();
TestPerspectiveCentroidInputs();
TestPerspectiveSampleInputs();
TestGraphicsCreateInterpolantMapping();
TestNewShaderRecompilerPixelPipelineEntry();
TestComputeLdsAllocationIdentity();