Compare commits

..

48 Commits

Author SHA1 Message Date
NeySlim 107d5f750a libNet: finish NpCommerce dialogs at open (#1166)
* libNet: finish NpCommerce dialogs at open

* libNet: complete NpCommerce dialog cancellation

---------

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

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

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

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

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

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

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

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

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

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

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

* Remove APR path tests from this change

* Remove unrelated formatting changes

* Remove native path explanatory comments

---------

Co-authored-by: overlordxrz-source <overlordxrz-source@users.noreply.github.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-08 19:27:10 +04:00
Romain Hedouin 4e14eed770 kernel: start the semaphore wait timer after the early returns (#1173)
Wait() read the clock and looked up the thread id before checking for an available token. Both are only needed once the thread has to block.
2026-10-08 19:21:36 +04:00
nmzik aa3b2cd57f Implement AGC async rewind imports 2026-10-08 17:43:02 +04:00
Dimitar Vagalinski 4865dd92ca libNet: answer sceNpGetAccountLanguage2 as a signed-out user (#1186)
* libNet: answer sceNpGetAccountLanguage2 as a signed-out user

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

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

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

* libNet: reject missing account language requests

---------

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

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

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

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

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

* tests: controller_settings covers scePadGetTriggerEffectState

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Pad: correct trigger effect state reporting

---------

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 03:31:46 +04:00
Romain Hedouin 79a91ecfaf graphics: cache the macOS thread id in TrackingSpinLock (#1169)
Every lock and unlock called pthread_mach_thread_np. Cache it per thread like the Linux branch does.
2026-10-08 03:18:49 +04:00
nmzik 420bb51d6c fix regression 9f83fd5f0c 2026-10-08 02:42:14 +04:00
nmzik c86c8b4d93 audio: mix AudioOut2 objects and apply port gains
Co-authored-by: chenxiao07 <chenxiao07@gmail.com>
2026-10-08 01:47:34 +04:00
chenxiao07 e43aadf75f ampr: assign unique IDs to resolved file paths 2026-10-08 01:18:40 +04:00
shadowbeat070 9f25e3c23e shader: support partial SDWA multiply and byte-source moves 2026-10-08 01:10:49 +04:00
shadowbeat070 7881c69a54 shader: decode v_cmp_class_f16 2026-10-08 01:10:49 +04:00
shadowbeat070 9e666e4dd2 shader: support scalar byte sign extension 2026-10-08 01:10:49 +04:00
Nawid3333 c3a12397b0 tests: use function pointers in the concurrent backing read pairs (#1163)
TestConcurrentBackingReads (7e011fa5) builds its reader pairs with
std::pair {TryReadBacking, TryReadSparseBacking}. Class template argument
deduction then also considers pair's const T1& / const T2& constructors, which
deduce function types, and libstdc++ and libc++ reject a pair member of
function type ("data member instantiated with function type"), so
virtual_memory_allocation_tests no longer compiles on Linux and macOS. MSVC's
library accepts it, which is why Windows still builds.

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 00:56:44 +04:00
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
dcave14 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
78 changed files with 4041 additions and 756 deletions
+6 -9
View File
@@ -674,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
+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();
}
@@ -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[] = {
@@ -296,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);
@@ -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,
@@ -399,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,
@@ -455,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,
@@ -490,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,
@@ -36,7 +36,7 @@ constexpr Vop2OpcodeInfo VOP2_OPCODE_LIST[] = {
{0x05u, Opcode::V_SUBREV_F32},
{0x08u, Opcode::V_MUL_F32, Vop2SdwaProfile::Float32},
{0x09u, Opcode::V_MUL_I32_I24, Vop2SdwaProfile::IntegerPartialDestination},
{0x0bu, Opcode::V_MUL_U32_U24, Vop2SdwaProfile::IntegerFullDestination},
{0x0bu, Opcode::V_MUL_U32_U24, Vop2SdwaProfile::IntegerPartialDestination},
{0x0fu, Opcode::V_MIN_F32},
{0x10u, Opcode::V_MAX_F32},
{0x11u, Opcode::V_MIN_I32},
@@ -245,7 +245,8 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0x88u, Opcode::V_CMP_CLASS_F32}, {0x89u, Opcode::V_CMP_LT_I16},
{0x8au, Opcode::V_CMP_EQ_I16}, {0x8bu, Opcode::V_CMP_LE_I16},
{0x8cu, Opcode::V_CMP_GT_I16}, {0x8du, Opcode::V_CMP_NE_I16},
{0x8eu, Opcode::V_CMP_GE_I16}, {0x91u, Opcode::V_CMPX_LT_I32},
{0x8eu, Opcode::V_CMP_GE_I16}, {0x8fu, Opcode::V_CMP_CLASS_F16, false},
{0x91u, Opcode::V_CMPX_LT_I32},
{0x92u, Opcode::V_CMPX_EQ_I32}, {0x93u, Opcode::V_CMPX_LE_I32},
{0x94u, Opcode::V_CMPX_GT_I32}, {0x95u, Opcode::V_CMPX_NE_I32},
{0x96u, Opcode::V_CMPX_GE_I32}, {0x98u, Opcode::V_CMPX_CLASS_F32},
@@ -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},
@@ -919,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;
@@ -1367,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;
@@ -1486,7 +1498,7 @@ bool HasUnsupportedNativeVop3Modifiers(Opcode opcode, bool permlane, bool mad_mi
// 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)) {
@@ -1555,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:
@@ -1712,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);
@@ -1823,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);
@@ -980,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:
@@ -995,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:
@@ -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;
@@ -166,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);
@@ -347,6 +374,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false, true); return;
case O::V_CMP_CLASS_F32: EmitFloatClassCompare(inst, false); return;
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst, true); return;
case O::V_CMP_CLASS_F16: EmitFloatClassCompare(inst, false, true); return;
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true, true); return;
case O::V_CVT_F32_UBYTE0: V_CVT_F32_UBYTE(inst, 0); return;
@@ -395,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;
+6 -6
View File
@@ -198,12 +198,6 @@ KernelSemaPrivate::Result KernelSemaPrivate::Wait(int need_count, uint32_t* ptr_
infinitely = false;
}
uint32_t elapsed = 0;
Common::Timer t;
t.Start();
int id = Common::Thread::GetThreadIdUnique();
if (m_count >= need_count) {
m_count -= need_count;
if (ptr_micros != nullptr) {
@@ -216,6 +210,12 @@ KernelSemaPrivate::Result KernelSemaPrivate::Wait(int need_count, uint32_t* ptr_
return Result::TimedOut;
}
uint32_t elapsed = 0;
Common::Timer t;
t.Start();
int id = Common::Thread::GetThreadIdUnique();
WaitingThread waiter {};
waiter.id = id;
waiter.priority = (m_fifo_order ? 0 : Libs::LibKernel::PthreadGetCurrentPriorityForKernel());
@@ -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>
+4
View File
@@ -119,8 +119,10 @@ 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;
@@ -152,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;
+4
View File
@@ -12,8 +12,10 @@ QVariantMap Configuration::GameSettings() const {
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);
@@ -55,8 +57,10 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
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) {
@@ -275,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();
@@ -434,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();
+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);
}
+74 -56
View File
@@ -1,4 +1,5 @@
#include "common/abi.h"
#include "common/assert.h"
#include "common/dateTime.h"
#include "common/file.h"
#include "common/logging/log.h"
@@ -18,6 +19,7 @@
#include <cstdint>
#include <cstring>
#include <deque>
#include <filesystem>
#include <limits>
#include <mutex>
#include <string>
@@ -47,14 +49,15 @@ struct ResolvedPathInfo {
uint32_t file_id = 0xffffffffu;
uint64_t file_size = 0;
bool is_dir = false;
std::string host_path;
std::filesystem::path host_path;
};
static std::mutex g_mutex;
static uint32_t g_next_submission_id = 1;
static uint32_t g_next_file_id = 1;
static std::unordered_map<uint32_t, bool> g_submissions;
static std::condition_variable g_submission_cv;
static std::unordered_map<uint32_t, std::string> g_files;
static std::unordered_map<uint32_t, std::filesystem::path> g_files;
static std::unordered_map<uint32_t, uint64_t> g_file_sizes;
static std::unordered_map<std::string, ResolvedPathInfo> g_resolved_paths;
@@ -92,15 +95,6 @@ static bool WriteGuest(uint64_t addr, const T& value) {
return WriteGuestBytes(addr, &value, sizeof(T));
}
static uint32_t ComputeFileId(const char* guest_path) {
uint32_t hash = 2166136261u;
for (auto* p = reinterpret_cast<const uint8_t*>(guest_path); p != nullptr && *p != 0; ++p) {
hash ^= *p;
hash *= 16777619u;
}
return hash & static_cast<uint32_t>(std::numeric_limits<int32_t>::max());
}
static int ReadGuestCString(uint64_t addr, char* out, size_t out_size) {
if (addr == 0) {
return LibKernel::KERNEL_ERROR_EFAULT;
@@ -120,19 +114,7 @@ static int ReadGuestCString(uint64_t addr, char* out, size_t out_size) {
return LibKernel::KERNEL_ERROR_ENAMETOOLONG;
}
static void RegisterHostPathLocked(uint32_t file_id, const std::string& host_path,
uint64_t file_size, bool is_dir) {
g_files[file_id] = host_path;
g_file_sizes[file_id] = is_dir ? 0 : file_size;
}
static void RegisterHostPath(uint32_t file_id, const std::string& host_path, uint64_t file_size,
bool is_dir) {
std::scoped_lock lock(g_mutex);
RegisterHostPathLocked(file_id, host_path, file_size, is_dir);
}
static bool TryGetHostPath(uint32_t file_id, std::string* out) {
static bool TryGetHostPath(uint32_t file_id, std::filesystem::path* out) {
std::scoped_lock lock(g_mutex);
const auto it = g_files.find(file_id);
if (it == g_files.end()) {
@@ -152,7 +134,7 @@ static bool TryGetHostFileSize(uint32_t file_id, uint64_t* out) {
return true;
}
static int GetHostPathStat(const std::string& host_path, LibKernel::FileSystem::FileStat* st) {
static int GetHostPathStat(const std::filesystem::path& host_path, LibKernel::FileSystem::FileStat* st) {
if (st == nullptr) {
return LibKernel::KERNEL_ERROR_EINVAL;
}
@@ -213,8 +195,7 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
if (!found) {
const auto real_path = LibKernel::FileSystem::GetRealFilename(path);
info.file_id = AprShared::ComputeFileId(guest_path);
info.host_path = Common::PathToString(real_path);
info.host_path = real_path;
if (Common::File::IsDirectoryExisting(real_path)) {
info.is_dir = true;
@@ -232,16 +213,21 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
if (!inserted) {
info = it->second;
} else if (info.result == OK) {
RegisterHostPathLocked(info.file_id, info.host_path, info.file_size, info.is_dir);
// Allocate once under the cache lock; distinct paths can share a hash.
if (g_next_file_id == 0xffffffffu) {
info.result = it->second.result = LibKernel::KERNEL_ERROR_ENFILE;
} else {
info.file_id = it->second.file_id = g_next_file_id++;
g_files.emplace(info.file_id, info.host_path);
g_file_sizes.emplace(info.file_id, info.is_dir ? 0 : info.file_size);
}
} else if (info.result == LibKernel::KERNEL_ERROR_ENOENT) {
log_missing = true;
}
}
if (log_missing) {
LOGF("\tAPR resolve missing path: %s -> %s\n", guest_path, info.host_path.c_str());
LOGF("\tAPR resolve missing path: %s -> %s\n", guest_path, Common::PathToString(info.host_path).c_str());
}
} else if (info.result == OK) {
AprShared::RegisterHostPath(info.file_id, info.host_path, info.file_size, info.is_dir);
}
if (info.result != OK) {
@@ -423,7 +409,7 @@ static int KYTY_SYSV_ABI GetFileStat(uint32_t file_id, LibKernel::FileSystem::Fi
return KernelSyscallResult(LibKernel::KERNEL_ERROR_EINVAL);
}
std::string host_path;
std::filesystem::path host_path;
if (!AprShared::TryGetHostPath(file_id, &host_path)) {
LOGF("\tAPR stat failed for unknown file id: 0x%08" PRIx32 "\n", file_id);
return KernelSyscallResult(LibKernel::KERNEL_ERROR_ENOENT);
@@ -445,7 +431,7 @@ static int KYTY_SYSV_ABI GetFileSize(uint32_t file_id, uint64_t* size) {
return KernelSyscallResult(LibKernel::KERNEL_ERROR_EFAULT);
}
std::string host_path;
std::filesystem::path host_path;
if (!AprShared::TryGetHostPath(file_id, &host_path)) {
LOGF("\tAPR size failed for unknown file id: 0x%08" PRIx32 "\n", file_id);
return KernelSyscallResult(LibKernel::KERNEL_ERROR_ENOENT);
@@ -665,10 +651,15 @@ struct CommandBufferState {
uint64_t address = 0;
uint64_t value = 0;
};
struct WaitAddressCommand {
uint64_t address;
struct WaitCommand {
uint64_t source;
uint64_t value;
uint8_t compare;
bool is_counter;
};
struct WriteCounterCommand {
uint8_t index;
uint32_t value;
};
struct AmmMapCommand {
AmmCommandKind kind = AmmCommandKind::MapAuto;
@@ -680,7 +671,7 @@ struct CommandBufferState {
uint8_t gpu_mask_id = 0;
};
using CommandData = std::variant<ReadFileCommand, KernelEventCommand, WriteAddressCommand,
AmmMapCommand, WaitAddressCommand>;
AmmMapCommand, WaitCommand, WriteCounterCommand>;
struct Command {
uint64_t record_offset;
CommandData data;
@@ -705,6 +696,8 @@ static_assert(sizeof(AmmUsageStatsData) == 0x18);
static std::mutex g_command_buffer_mutex;
static std::unordered_map<uint64_t, CommandBufferState> g_command_buffers;
static std::unordered_map<uint64_t, uint64_t> g_command_buffer_aliases;
// AMM and APR share 256 32-bit counters; only the first 128 are application-writable.
static std::array<std::atomic<uint32_t>, 256> g_counters {};
using CommandBufferIterator = std::unordered_map<uint64_t, CommandBufferState>::iterator;
static void RegisterCommandBufferAliasLocked(uint64_t command_buffer, uint64_t buffer) {
@@ -1169,13 +1162,13 @@ static bool AppendCommandRecord(uint64_t command_buffer, CommandBufferState::Com
return CommitCommandBufferRecord(command_buffer, &state, record_size);
}
static int ReadHostFileToGuest(const std::string& host_path, uint64_t file_offset,
static int ReadHostFileToGuest(const std::filesystem::path& host_path, uint64_t file_offset,
uint64_t destination, uint64_t size, uint64_t* bytes_read);
static int ExecuteCommand(const CommandBufferState::Command& entry) {
if (const auto* payload = std::get_if<CommandBufferState::ReadFileCommand>(&entry.data)) {
const auto& command = *payload;
std::string host_path;
std::filesystem::path host_path;
if (!AprShared::TryGetHostPath(command.file_id, &host_path)) {
LOGF("\tAPR submit failed for unknown file id: 0x%08" PRIx32 "\n", command.file_id);
return LibKernel::KERNEL_ERROR_ENOENT;
@@ -1186,7 +1179,7 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) {
command.size, &bytes_read);
if (result != OK) {
LOGF("\tAPR submit read failed: id=0x%08" PRIx32 ", result=0x%08" PRIx32 ", path=%s\n",
command.file_id, static_cast<uint32_t>(result), host_path.c_str());
command.file_id, static_cast<uint32_t>(result), Common::PathToString(host_path).c_str());
return result;
}
} else if (const auto* payload =
@@ -1207,6 +1200,9 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) {
const auto& command = *payload;
std::atomic_ref(*reinterpret_cast<uint64_t*>(command.address))
.store(command.value, std::memory_order_release);
} else if (const auto* command =
std::get_if<CommandBufferState::WriteCounterCommand>(&entry.data)) {
g_counters[command->index].store(command->value, std::memory_order_release);
} else if (const auto* payload = std::get_if<CommandBufferState::AmmMapCommand>(&entry.data)) {
const auto& command = *payload;
int result = OK;
@@ -1235,17 +1231,20 @@ struct Submission {
uint32_t id = 0;
};
static bool IsWaitSatisfied(const CommandBufferState::WaitAddressCommand& wait) {
const uint64_t value =
std::atomic_ref(*reinterpret_cast<uint64_t*>(wait.address)).load(std::memory_order_acquire);
static bool IsWaitSatisfied(const CommandBufferState::WaitCommand& wait) {
const uint64_t value = wait.is_counter
? g_counters[wait.source].load(std::memory_order_acquire)
: std::atomic_ref(*reinterpret_cast<uint64_t*>(wait.source))
.load(std::memory_order_acquire);
const uint64_t sign = uint64_t {1} << (wait.is_counter ? 31 : 63);
switch (wait.compare) {
case 0: return value == wait.value;
case 1: return value > wait.value;
case 2: return value < wait.value;
case 3: return value != wait.value;
case 4: return static_cast<int64_t>(value - wait.value) >= 0;
case 5: return static_cast<int64_t>(value) > static_cast<int64_t>(wait.value);
default: return static_cast<int64_t>(value) < static_cast<int64_t>(wait.value);
case 4: return ((value - wait.value) & sign) == 0;
case 5: return (value ^ sign) > (wait.value ^ sign);
default: return (value ^ sign) < (wait.value ^ sign);
}
}
@@ -1279,7 +1278,7 @@ private:
pending = true;
auto& submission = queue.front();
if (submission.cursor < submission.commands.size()) {
const auto* wait = std::get_if<CommandBufferState::WaitAddressCommand>(
const auto* wait = std::get_if<CommandBufferState::WaitCommand>(
&submission.commands[submission.cursor].data);
if (wait != nullptr && !IsWaitSatisfied(*wait)) {
continue;
@@ -1382,7 +1381,7 @@ static bool AdvanceCommandBuffer(uint64_t command_buffer, uint64_t record_size)
return CommitCommandBufferRecord(command_buffer, &state, record_size);
}
static int ReadHostFileToGuest(const std::string& host_path, uint64_t file_offset,
static int ReadHostFileToGuest(const std::filesystem::path& host_path, uint64_t file_offset,
uint64_t destination, uint64_t size, uint64_t* bytes_read) {
if (bytes_read == nullptr) {
return LibKernel::KERNEL_ERROR_EINVAL;
@@ -1398,7 +1397,7 @@ static int ReadHostFileToGuest(const std::string& host_path, uint64_t file_offse
Common::File file;
if (!file.Open(host_path, Common::File::Mode::Read)) {
LOGF("\tAPR read missing host file: %s\n", host_path.c_str());
LOGF("\tAPR read missing host file: %s\n", Common::PathToString(host_path).c_str());
return LibKernel::KERNEL_ERROR_ENOENT;
}
const auto file_size = file.Size();
@@ -1927,18 +1926,27 @@ static int KYTY_SYSV_ABI CommandBufferWaitOnAddress(void* command_b
return LibKernel::KERNEL_ERROR_EINVAL;
}
return AppendCommandRecord(reinterpret_cast<uint64_t>(command_buffer),
CommandBufferState::WaitAddressCommand {
reinterpret_cast<uint64_t>(address), value, compare},
CommandBufferState::WaitCommand {
reinterpret_cast<uint64_t>(address), value, compare, false},
0x20)
? OK
: LibKernel::KERNEL_ERROR_EBUSY;
}
static int KYTY_SYSV_ABI CommandBufferWaitOnCounter(void* command_buffer, uint8_t, uint8_t,
uint64_t, uint8_t, uint8_t, uint64_t, uint8_t) {
static int KYTY_SYSV_ABI CommandBufferWaitOnCounter(void* command_buffer, uint8_t index,
uint8_t access, uint64_t value, uint8_t compare,
uint8_t mask_op, uint64_t, uint8_t flush) {
PRINT_NAME();
return AppendNoOpCommand(command_buffer, 0x20);
if (command_buffer == nullptr || access > 7 || compare > 6 || mask_op > 1 || flush > 1) {
return LibKernel::KERNEL_ERROR_EINVAL;
}
EXIT_NOT_IMPLEMENTED(access != 1 || mask_op != 0 || flush != 0 || value > UINT32_MAX);
return AppendCommandRecord(reinterpret_cast<uint64_t>(command_buffer),
CommandBufferState::WaitCommand {index, value, compare, true},
0x20)
? OK
: LibKernel::KERNEL_ERROR_EBUSY;
}
static int AppendWriteAddressCommand(void* command_buffer, volatile uint64_t* address,
@@ -1970,11 +1978,21 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressOnCompletion(void*
return AppendWriteAddressCommand(command_buffer, address, value);
}
static int KYTY_SYSV_ABI CommandBufferWriteCounter(void* command_buffer, uint8_t, uint8_t, uint64_t,
uint8_t, uint32_t) {
static int KYTY_SYSV_ABI CommandBufferWriteCounter(void* command_buffer, uint8_t index,
uint8_t access, uint64_t value,
uint8_t operation, uint32_t mode) {
PRINT_NAME();
return AppendNoOpCommand(command_buffer, 0x20);
if (command_buffer == nullptr || index >= 128 || access > 7 || operation > 4 || mode > 1) {
return LibKernel::KERNEL_ERROR_EINVAL;
}
EXIT_NOT_IMPLEMENTED(access != 1 || operation != 0 || mode != 0);
return AppendCommandRecord(reinterpret_cast<uint64_t>(command_buffer),
CommandBufferState::WriteCounterCommand {
index, static_cast<uint32_t>(value)},
0x20)
? OK
: LibKernel::KERNEL_ERROR_EBUSY;
}
static int KYTY_SYSV_ABI CommandBufferWriteCounterOnCompletion(void* command_buffer, uint8_t,
+2
View File
@@ -6,6 +6,7 @@
#include "common/stringUtils.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "libs/systemService.h"
#include "loader/symbolDatabase.h"
#include "loader/systemContent.h"
@@ -96,6 +97,7 @@ int KYTY_SYSV_ABI AppContentInitialize(const AppContentInitParam* init_param,
LOGF("\t TITLE_ID missing\n");
}
SystemService::NotifyEntitlementUpdate();
return OK;
}
+219 -100
View File
@@ -11,6 +11,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstring>
#include <vector>
@@ -170,12 +171,14 @@ static std::atomic_uint64_t g_audioout2_next_port {1};
static AudioOut2UserHandle g_audioout2_next_user = 1;
struct AudioOut2ContextState {
bool used = false;
AudioOut2ContextHandle handle = 0;
uint32_t queue_depth = 4;
uint32_t queued = 0;
uint32_t num_grains = 512;
uint64_t last_update = 0;
bool used = false;
AudioOut2ContextHandle handle = 0;
uint32_t queue_depth = 4;
uint32_t queued = 0;
uint32_t num_grains = 512;
uint64_t last_update = 0;
int object_audio_handle = 0;
std::vector<float> object_mix;
};
struct AudioOut2PortStateEntry {
@@ -189,9 +192,11 @@ struct AudioOut2PortStateEntry {
uint32_t samples_num = 512;
AudioInternal::Format audio_format = AudioInternal::Format::Unknown;
int audio_handle = 0;
std::array<float, 16> gains {};
uint32_t ambisonics = UINT32_MAX;
bool attributes_locked = false;
std::vector<uint8_t> pcm_data;
};
struct AudioOut2SpeakerArrayState {
bool used = false;
uint32_t num_speakers = 0;
@@ -207,9 +212,8 @@ struct AudioOut2LatencyState {
uint32_t latency_us = 0;
};
static Common::Mutex g_audioout2_context_mutex;
static Common::Mutex g_audioout2_state_mutex;
static std::array<AudioOut2ContextState, 16> g_audioout2_contexts;
static Common::Mutex g_audioout2_port_mutex;
static std::array<AudioOut2PortStateEntry, 256> g_audioout2_ports;
static std::vector<AudioOut2UserHandle> g_audioout2_users;
static Common::Mutex g_audioout2_speaker_array_mutex;
@@ -223,7 +227,11 @@ static constexpr int AUDIO_OUT2_ERROR_INVALID_PARAM = -2144960511;
static constexpr int AUDIO_OUT2_ERROR_BUSY = -2144960505; /* 0x80268007 */
static constexpr int AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM = -2144959999; /* 0x80268201 */
static constexpr int AUDIO_OUT2_ERROR_MASTERING_INVALID_STATES_ID = -2144959996; /* 0x80268204 */
static constexpr int AUDIO_OUT2_ERROR_OUT_OF_RESOURCE = -2144960510; /* 0x80268002 */
static constexpr int AUDIO_OUT2_ERROR_INVALID_PORT = -2144960503; /* 0x80268009 */
static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM = 0;
static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN = 1;
static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS = 8;
static constexpr uint32_t AUDIO_OUT2_MASTERING_OUTPUT_RECORDING = 2;
static constexpr uint32_t AUDIO_OUT2_MASTERING_STATES_ID_DEFAULT = 1;
static constexpr uint32_t AUDIO_OUT2_MASTERING_STATES_ID_V2 = 2;
@@ -350,37 +358,80 @@ static size_t audioout2_pcm_size(const AudioOut2PortStateEntry& state) {
audioout2_data_format_channels(state.data_format) * bytes_per_sample;
}
static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) {
Common::LockGuard lock(g_audioout2_port_mutex);
static bool audioout2_context_has_queueable_device(const AudioOut2ContextState& context) {
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
!state.pcm_data.empty() && AudioInternal::AudioOutHasDevice(state.audio_handle)) {
return true;
if (state.used && state.context == context.handle && !state.pcm_data.empty()) {
const int handle = audioout2_port_type_is_object(state.port_type)
? context.object_audio_handle
: state.audio_handle;
if (AudioInternal::AudioOutHasDevice(handle)) {
return true;
}
}
}
return false;
}
static void audioout2_mix_object(AudioOut2ContextState& context,
const AudioOut2PortStateEntry& object) {
auto& mix = context.object_mix;
if (mix.empty()) {
mix.resize(static_cast<size_t>(context.num_grains) * 2, 0.0f);
}
// Center ordinary objects. First-order stereo ambisonics uses left/right-facing
// cardioids; FuMa W has a different normalization from ACN/SN3D W.
constexpr float half_power = 0.70710678f;
float left = half_power, right = half_power;
switch (object.ambisonics) {
case UINT32_MAX:
case 64: break;
case 0: left = right = 1.0f; break;
case 2:
case 65: right = -half_power; break;
default: return;
}
left *= object.gains[0];
right *= object.gains[0];
for (uint32_t frame = 0; frame < context.num_grains; frame++) {
const float sample =
object.audio_format == AudioInternal::Format::FloatMono
? reinterpret_cast<const float*>(object.pcm_data.data())[frame]
: reinterpret_cast<const int16_t*>(object.pcm_data.data())[frame] / 32768.0f;
mix[frame * 2] += sample * left;
mix[frame * 2 + 1] += sample * right;
}
}
static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, bool blocking) {
// The backend consumes the buffers synchronously, but can block while pacing SDL's queue.
// Keep both PCM storage and port handles alive until it returns.
Common::LockGuard lock(g_audioout2_state_mutex);
auto* context = audioout2_find_context_locked(ctx);
if (context == nullptr) {
return;
}
context->object_mix.clear();
std::vector<AudioInternal::OutputParam> params;
params.reserve(AudioInternal::OUT_PORTS_MAX);
Common::LockGuard lock(g_audioout2_port_mutex);
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
!state.pcm_data.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data.data()});
if (!state.used || state.context != ctx || state.pcm_data.empty()) {
continue;
}
if (audioout2_port_type_is_object(state.port_type)) {
audioout2_mix_object(*context, state);
} else if (state.audio_handle > 0 && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back({state.audio_handle, state.pcm_data.data(), state.gains.data()});
}
}
if (!context->object_mix.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back({context->object_audio_handle, context->object_mix.data()});
}
if (!params.empty()) {
(void)AudioInternal::AudioOutOutputs(params.data(), static_cast<uint32_t>(params.size()),
blocking);
}
}
static void audioout2_close_audio_handle(int audio_handle) {
if (audio_handle > 0) {
AudioInternal::AudioOutClose(audio_handle);
@@ -431,52 +482,58 @@ int KYTY_SYSV_ABI AudioOut2ContextCreate(const AudioOut2ContextParam* params, vo
EXIT_NOT_IMPLEMENTED(params == nullptr);
EXIT_NOT_IMPLEMENTED(ctx == nullptr);
*ctx = g_audioout2_next_context.fetch_add(1, std::memory_order_relaxed);
g_audioout2_context_mutex.Lock();
auto* state = audioout2_find_context_locked(0);
if (state == nullptr) {
for (auto& candidate: g_audioout2_contexts) {
if (!candidate.used) {
state = &candidate;
break;
}
}
const auto num_grains = params->num_grains == 0 ? 512u : params->num_grains;
const int object_audio_handle =
params->max_object_ports == 0
? 0
: AudioInternal::AudioOutOpen(AUDIO_OUT_PORT_TYPE_MAIN, num_grains, 48000,
AudioInternal::Format::FloatStereo);
if (params->max_object_ports != 0 && object_audio_handle == 0) {
return AUDIO_OUT2_ERROR_OUT_OF_RESOURCE;
}
EXIT_NOT_IMPLEMENTED(state == nullptr);
*state = AudioOut2ContextState {};
state->used = true;
state->handle = *ctx;
state->queue_depth = (params->queue_depth == 0 ? 4u : params->queue_depth);
state->queued = 0;
state->num_grains = (params->num_grains == 0 ? 512u : params->num_grains);
state->last_update = LibKernel::KernelGetProcessTime();
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Lock();
auto it = std::find_if(g_audioout2_contexts.begin(), g_audioout2_contexts.end(),
[](const auto& state) { return !state.used; });
if (it == g_audioout2_contexts.end()) {
g_audioout2_state_mutex.Unlock();
audioout2_close_audio_handle(object_audio_handle);
return AUDIO_OUT2_ERROR_OUT_OF_RESOURCE;
}
*ctx = g_audioout2_next_context.fetch_add(1, std::memory_order_relaxed);
auto* state = &*it;
*state = AudioOut2ContextState {};
state->used = true;
state->handle = *ctx;
state->queue_depth = params->queue_depth == 0 ? 4u : params->queue_depth;
state->num_grains = num_grains;
state->last_update = LibKernel::KernelGetProcessTime();
state->object_audio_handle = object_audio_handle;
g_audioout2_state_mutex.Unlock();
LOGF("\t buffer = 0x%016" PRIx64 "\n"
"\t buffer_size = 0x%016" PRIx64 "\n"
"\t ctx = 0x%016" PRIx64 "\n"
"\t queue_depth = %" PRIu32 ", num_grains = %" PRIu32 "\n",
reinterpret_cast<uint64_t>(buffer), static_cast<uint64_t>(buffer_size), *ctx,
state->queue_depth, state->num_grains);
(params->queue_depth == 0 ? 4u : params->queue_depth), num_grains);
return OK;
}
int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
PRINT_NAME();
LOGF("\t ctx = 0x%016" PRIx64 "\n", ctx);
g_audioout2_context_mutex.Lock();
std::array<int, 257> audio_handles {};
size_t audio_handles_num = 0;
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
if (state->object_audio_handle > 0) {
audio_handles[audio_handles_num++] = state->object_audio_handle;
}
*state = AudioOut2ContextState {};
}
g_audioout2_context_mutex.Unlock();
std::array<int, 256> audio_handles {};
size_t audio_handles_num = 0;
g_audioout2_port_mutex.Lock();
for (auto& port_state: g_audioout2_ports) {
if (port_state.used && port_state.context == ctx) {
if (port_state.audio_handle > 0 && audio_handles_num < audio_handles.size()) {
@@ -485,7 +542,7 @@ int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
port_state = AudioOut2PortStateEntry {};
}
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
for (size_t i = 0; i < audio_handles_num; i++) {
audioout2_close_audio_handle(audio_handles[i]);
@@ -504,11 +561,16 @@ int KYTY_SYSV_ABI AudioOut2ContextSetAttributes(AudioOut2ContextHandle ctx,
}
int KYTY_SYSV_ABI AudioOut2ContextAdvance(AudioOut2ContextHandle ctx) {
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
audioout2_update_context_locked(state);
}
g_audioout2_context_mutex.Unlock();
for (auto& port: g_audioout2_ports) {
if (port.used && port.context == ctx) {
port.attributes_locked = false;
}
}
g_audioout2_state_mutex.Unlock();
return OK;
}
@@ -520,11 +582,10 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
// Only a synchronous submission carrying PCM to a real device can rely on the SDL queue for
// pacing. Async pushes must retain queue-depth backpressure, and a handle without PCM (or a
// vibration/failed-open handle) has no downstream operation that can block this call.
const bool use_device_clock =
blocking != 0 && audioout2_context_has_queueable_device(ctx);
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
const bool use_device_clock =
blocking != 0 && audioout2_context_has_queueable_device(*state);
audioout2_update_context_locked(state);
sleep_micros = audioout2_grain_micros(state->num_grains);
if (state->queued < state->queue_depth || use_device_clock) {
@@ -534,12 +595,12 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
if (state->queued < state->queue_depth) {
state->queued++;
}
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
audioout2_queue_context_audio(ctx, blocking != 0);
return OK;
}
}
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
if (blocking == 0) {
return AUDIO_OUT2_ERROR_NOT_READY;
@@ -558,7 +619,7 @@ int KYTY_SYSV_ABI AudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint
*available_queues = 4;
}
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
audioout2_update_context_locked(state);
if (queue_level != nullptr) {
@@ -569,7 +630,7 @@ int KYTY_SYSV_ABI AudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint
(state->queued < state->queue_depth ? state->queue_depth - state->queued : 0);
}
}
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
return OK;
}
@@ -583,19 +644,25 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
const auto audio_format = audioout2_data_format_to_audio_format(params->data_format);
const auto audio_type = audioout2_port_type_to_audio_out_type(params->port_type);
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
const auto* context_state = audioout2_find_context_locked(ctx);
if (context_state == nullptr) {
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
const auto samples_num = context_state->num_grains == 0 ? 512u : context_state->num_grains;
if (audioout2_port_type_is_object(params->port_type) &&
(params->port_type != 0x100 || context_state->object_audio_handle == 0 ||
params->sampling_freq != 48000 ||
(audio_format != AudioInternal::Format::FloatMono &&
audio_format != AudioInternal::Format::Signed16bitMono))) {
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
const auto samples_num = context_state->num_grains;
g_audioout2_port_mutex.Lock();
if (std::find(g_audioout2_users.begin(), g_audioout2_users.end(), params->user_handle) ==
g_audioout2_users.end()) {
g_audioout2_port_mutex.Unlock();
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
AudioOut2PortStateEntry* port_state = nullptr;
@@ -616,9 +683,9 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
port_state->sampling_freq = params->sampling_freq;
port_state->samples_num = samples_num;
port_state->audio_format = audio_format;
port_state->gains.fill(1.0f);
}
g_audioout2_port_mutex.Unlock();
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
if (port_state == nullptr) {
return AUDIO_OUT2_ERROR_PORT_FULL;
@@ -632,12 +699,12 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
audio_format);
}
g_audioout2_port_mutex.Lock();
g_audioout2_state_mutex.Lock();
const bool reserved = port_state->used && port_state->handle == next_port;
if (reserved) {
port_state->audio_handle = audio_handle;
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
if (!reserved) {
audioout2_close_audio_handle(audio_handle);
return AUDIO_OUT2_ERROR_INVALID_PARAM;
@@ -663,12 +730,12 @@ int KYTY_SYSV_ABI AudioOut2PortDestroy(AudioOut2PortHandle port) {
LOGF("\t port = 0x%016" PRIx64 "\n", port);
int audio_handle = 0;
g_audioout2_port_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_port_locked(port); state != nullptr) {
audio_handle = state->audio_handle;
*state = AudioOut2PortStateEntry {};
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
audioout2_close_audio_handle(audio_handle);
@@ -677,36 +744,86 @@ int KYTY_SYSV_ABI AudioOut2PortDestroy(AudioOut2PortHandle port) {
int KYTY_SYSV_ABI AudioOut2PortSetAttributes(AudioOut2PortHandle port,
const AudioOut2Attribute* attributes, uint32_t num) {
EXIT_NOT_IMPLEMENTED(num != 0 && attributes == nullptr);
if (num != 0 && attributes == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
Common::LockGuard lock(g_audioout2_state_mutex);
auto* state = audioout2_find_port_locked(port);
if (state == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PORT;
}
const void* pcm_data = nullptr;
bool has_pcm = false;
const void* pcm_data = nullptr;
auto gains = state->gains;
auto ambisonics = state->ambisonics;
uint32_t seen = 0;
for (uint32_t i = 0; i < num; i++) {
if (attributes[i].attribute_id == AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM &&
attributes[i].value != nullptr && attributes[i].value_size >= sizeof(AudioOut2Pcm)) {
AudioOut2Pcm pcm {};
std::memcpy(&pcm, attributes[i].value, sizeof(AudioOut2Pcm));
pcm_data = pcm.data;
has_pcm = true;
const auto& attribute = attributes[i];
const auto id = attribute.attribute_id;
if (id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM && id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN &&
id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS) {
continue;
}
}
if (has_pcm) {
g_audioout2_port_mutex.Lock();
if (auto* state = audioout2_find_port_locked(port); state != nullptr) {
if (pcm_data != nullptr && state->audio_format != AudioInternal::Format::Unknown) {
const auto* bytes = static_cast<const uint8_t*>(pcm_data);
state->pcm_data.assign(bytes, bytes + audioout2_pcm_size(*state));
} else {
state->pcm_data.clear();
if ((seen & (1u << id)) != 0) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
seen |= 1u << id;
if (id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM && state->attributes_locked) {
continue;
}
if (attribute.value == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
switch (id) {
case AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM: {
if (attribute.value_size < sizeof(AudioOut2Pcm)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
AudioOut2Pcm pcm {};
std::memcpy(&pcm, attribute.value, sizeof(pcm));
pcm_data = pcm.data;
break;
}
case AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN: {
const auto channels = audioout2_data_format_channels(state->data_format);
if (attribute.value_size != channels * sizeof(float)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
std::memcpy(gains.data(), attribute.value, attribute.value_size);
for (uint32_t ch = 0; ch < channels; ch++) {
if (!std::isfinite(gains[ch]) || gains[ch] < 0.0f) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
}
break;
}
case AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS:
if (!audioout2_port_type_is_object(state->port_type) ||
attribute.value_size != sizeof(ambisonics)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
std::memcpy(&ambisonics, attribute.value, sizeof(ambisonics));
if (ambisonics != UINT32_MAX && ambisonics > 15 &&
(ambisonics < 64 || ambisonics > 99)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
break;
}
g_audioout2_port_mutex.Unlock();
}
state->gains = gains;
state->ambisonics = ambisonics;
if ((seen & (1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM)) != 0) {
if (pcm_data != nullptr && state->audio_format != AudioInternal::Format::Unknown) {
const auto* bytes = static_cast<const uint8_t*>(pcm_data);
state->pcm_data.assign(bytes, bytes + audioout2_pcm_size(*state));
} else {
state->pcm_data.clear();
}
state->attributes_locked = audioout2_port_type_is_object(state->port_type);
}
return OK;
}
int KYTY_SYSV_ABI AudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) {
PRINT_NAME();
@@ -719,11 +836,11 @@ int KYTY_SYSV_ABI AudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortS
state->reroute_counter = 0;
state->flags = 0;
g_audioout2_port_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* port_state = audioout2_find_port_locked(port); port_state != nullptr) {
state->num_channels = audioout2_data_format_channels(port_state->data_format);
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
// LOGF("\t port = 0x%016" PRIx64 "\n", port);
// LOGF("\t num_channels = %" PRIu8 "\n", state->num_channels);
@@ -743,7 +860,7 @@ int KYTY_SYSV_ABI AudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* han
if (handle == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
Common::LockGuard lock(g_audioout2_port_mutex);
Common::LockGuard lock(g_audioout2_state_mutex);
*handle = g_audioout2_next_user++;
g_audioout2_users.push_back(*handle);
LOGF("\t user_id = %" PRIu32 ", handle = 0x%016" PRIx64 "\n", user_id,
@@ -754,7 +871,7 @@ int KYTY_SYSV_ABI AudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* han
int KYTY_SYSV_ABI AudioOut2UserDestroy(AudioOut2UserHandle handle) {
PRINT_NAME();
LOGF("\t handle = 0x%016" PRIx64 "\n", static_cast<uint64_t>(handle));
Common::LockGuard lock(g_audioout2_port_mutex);
Common::LockGuard lock(g_audioout2_state_mutex);
const auto it = std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle);
if (it == g_audioout2_users.end()) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
@@ -773,13 +890,15 @@ int KYTY_SYSV_ABI AudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle
if (context_attributes == nullptr || port_attributes == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
Common::LockGuard lock(g_audioout2_port_mutex);
Common::LockGuard lock(g_audioout2_state_mutex);
if (std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle) ==
g_audioout2_users.end()) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
*context_attributes = 0;
*port_attributes = 1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM;
*port_attributes = (1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM) |
(1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN) |
(1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS);
return OK;
}
+7
View File
@@ -1974,6 +1974,12 @@ int64_t KYTY_SYSV_ABI fstat(int d, LibKernel::FileSystem::FileStat* sb) {
return POSIX_N_CALL(LibKernel::FileSystem::KernelFstat(d, sb));
}
int KYTY_SYSV_ABI fcntl(int d, int command, int arg) {
PRINT_NAME();
return POSIX_N_CALL(LibKernel::FileSystem::KernelFcntl(d, command, arg));
}
int KYTY_SYSV_ABI ftruncate(int d, int64_t length) {
PRINT_NAME();
@@ -2163,6 +2169,7 @@ LIB_DEFINE(InitLibKernel_1_Posix) {
LIB_FUNC("E6ao34wPw+U", stat);
LIB_FUNC("JGMio+21L4c", mkdir);
LIB_FUNC("ih4CD9-gghM", Posix::ftruncate);
LIB_FUNC("8nY19bKoiZk", Posix::fcntl);
LIB_FUNC("pDuPEf3m4fI", Posix::sem_init);
LIB_FUNC("cDW233RAwWo", Posix::sem_destroy);
LIB_FUNC("YCV5dGGBcCo", Posix::sem_wait);
+74 -3
View File
@@ -1497,13 +1497,43 @@ namespace LibNpCommerce {
LIB_VERSION("NpCommerce", 1, "NpCommerce", 1, 1);
constexpr int COMMERCE_STATUS_NONE = 0;
constexpr int COMMERCE_STATUS_INITIALIZED = 1;
constexpr int COMMERCE_STATUS_NONE = 0;
constexpr int COMMERCE_STATUS_INITIALIZED = 1;
constexpr int COMMERCE_STATUS_FINISHED = 3;
constexpr int COMMERCE_RESULT_USER_CANCELED = 1;
constexpr int COMMERCE_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u);
constexpr int COMMERCE_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u);
constexpr int COMMERCE_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u);
constexpr int COMMERCE_ERROR_PARAM_INVALID = static_cast<int>(0x80B8000Au);
constexpr int COMMERCE_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du);
static int g_commerce_status = COMMERCE_STATUS_NONE;
struct CommerceDialogParam {
uint8_t base_param[48];
int32_t size;
int32_t user_id;
int32_t mode;
uint32_t service_label;
const char* const* targets;
uint32_t num_targets;
uint32_t padding;
uint64_t features;
void* user_data;
uint8_t reserved[32];
};
struct CommerceDialogResult {
int32_t result;
bool authorized;
void* user_data;
uint8_t reserved[32];
};
static_assert(sizeof(CommerceDialogParam) == 128 && offsetof(CommerceDialogParam, user_data) == 88);
static_assert(sizeof(CommerceDialogResult) == 48 && offsetof(CommerceDialogResult, user_data) == 8);
static int g_commerce_status = COMMERCE_STATUS_NONE;
static CommerceDialogResult g_commerce_result {};
static int KYTY_SYSV_ABI NpCommerceDialogInitialize() {
PRINT_NAME();
@@ -1520,9 +1550,47 @@ static int KYTY_SYSV_ABI NpCommerceDialogTerminate() {
return COMMERCE_ERROR_NOT_INITIALIZED;
}
g_commerce_status = COMMERCE_STATUS_NONE;
g_commerce_result = {};
return OK;
}
static int KYTY_SYSV_ABI NpCommerceDialogOpen(const CommerceDialogParam* param) {
PRINT_NAME();
if (g_commerce_status == COMMERCE_STATUS_NONE) {
return COMMERCE_ERROR_NOT_INITIALIZED;
}
if (param == nullptr) {
return COMMERCE_ERROR_ARG_NULL;
}
g_commerce_result = {};
if (param->size == sizeof(CommerceDialogParam)) {
g_commerce_result.user_data = param->user_data;
}
g_commerce_status = COMMERCE_STATUS_FINISHED;
if (param->size != sizeof(CommerceDialogParam) || param->mode < 0 || param->mode > 5 ||
std::ranges::any_of(param->reserved, [](uint8_t value) { return value != 0; })) {
g_commerce_result.result = COMMERCE_ERROR_PARAM_INVALID;
return COMMERCE_ERROR_PARAM_INVALID;
}
g_commerce_result.result = COMMERCE_RESULT_USER_CANCELED;
return OK;
}
static int KYTY_SYSV_ABI NpCommerceDialogGetResult(CommerceDialogResult* result) {
PRINT_NAME();
if (g_commerce_status == COMMERCE_STATUS_NONE) {
return COMMERCE_ERROR_NOT_INITIALIZED;
}
if (result == nullptr) {
return COMMERCE_ERROR_ARG_NULL;
}
if (g_commerce_status != COMMERCE_STATUS_FINISHED) {
return COMMERCE_ERROR_NOT_FINISHED;
}
*result = g_commerce_result;
return result->result;
}
static int KYTY_SYSV_ABI NpCommerceDialogUpdateStatus() {
PRINT_NAME();
@@ -1532,6 +1600,8 @@ static int KYTY_SYSV_ABI NpCommerceDialogUpdateStatus() {
LIB_DEFINE(InitNet_1_NpCommerce) {
LIB_FUNC("0aR2aWmQal4", NpCommerceDialogInitialize);
LIB_FUNC("m-I92Ab50W8", NpCommerceDialogTerminate);
LIB_FUNC("DfSCDRA3EjY", NpCommerceDialogOpen);
LIB_FUNC("r42bWcQbtZY", NpCommerceDialogGetResult);
LIB_FUNC("LR5cwFMMCVE", NpCommerceDialogUpdateStatus);
}
@@ -1559,6 +1629,7 @@ LIB_DEFINE(InitNet_1_NpManager) {
LIB_FUNC("rbknaUjpqWo", NpManager::NpGetAccountIdA);
LIB_FUNC("JT+t00a3TxA", NpManager::NpGetAccountCountryA);
LIB_FUNC("+4DegjBqV1g", NpManager::NpGetAccountAge);
LIB_FUNC("3Tcz5bNCfZQ", NpManager::NpGetAccountLanguage2);
LIB_FUNC("GpLQDNKICac", NpManager::NpCreateRequest);
LIB_FUNC("eiqMCt9UshI", NpManager::NpCreateAsyncRequest);
LIB_FUNC("S7QTn72PrDw", NpManager::NpDeleteRequest);
+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;
}
+25 -11
View File
@@ -832,10 +832,6 @@ namespace Net {
LIB_NAME("Net", "Net");
struct NetEtherAddr {
uint8_t data[6] = {0};
};
#if defined(_WIN32)
using NativeSocket = SOCKET;
static constexpr NativeSocket INVALID_NATIVE_SOCKET = INVALID_SOCKET;
@@ -1564,14 +1560,12 @@ const char* KYTY_SYSV_ABI NetInetNtop(int af, const void* src, char* dst, uint32
int KYTY_SYSV_ABI NetEtherNtostr(const NetEtherAddr* n, char* str, size_t len) {
PRINT_NAME();
NetEtherAddr zero {};
if (n == nullptr || str == nullptr || len < 18) {
return NET_ERROR_EINVAL;
}
EXIT_NOT_IMPLEMENTED(len != 18);
EXIT_NOT_IMPLEMENTED(n == nullptr);
EXIT_NOT_IMPLEMENTED(str == nullptr);
EXIT_NOT_IMPLEMENTED(memcmp(n->data, zero.data, sizeof(zero.data)) != 0);
strcpy(str, "00:00:00:00:00:00"); // NOLINT
std::snprintf(str, len, "%02x:%02x:%02x:%02x:%02x:%02x", n->data[0], n->data[1],
n->data[2], n->data[3], n->data[4], n->data[5]);
return OK;
}
@@ -4112,6 +4106,26 @@ int KYTY_SYSV_ABI NpGetAccountAge(int req_id, int user_id, uint8_t* age) {
return np_error_signed_out;
}
int KYTY_SYSV_ABI NpGetAccountLanguage2(int req_id, int user_id, void* language_code) {
PRINT_NAME();
LOGF("\t req_id = %d\n", req_id);
LOGF("\t user_id = %d\n", user_id);
LOGF("\t language_code = 0x%016" PRIx64 "\n", reinterpret_cast<uint64_t>(language_code));
if (req_id <= 0 || language_code == nullptr) {
return np_error_invalid_argument;
}
std::lock_guard lock(g_np_request_mutex);
if (np_get_request_locked(req_id) == nullptr) {
return np_error_request_not_found;
}
return np_error_signed_out;
}
int KYTY_SYSV_ABI NpCreateRequest() {
PRINT_NAME();
+4 -1
View File
@@ -16,7 +16,9 @@ struct Lifecycle {
namespace Net {
struct NetEtherAddr;
struct NetEtherAddr {
uint8_t data[6] = {0};
};
union NetEpollData {
void* ptr;
@@ -185,6 +187,7 @@ int KYTY_SYSV_ABI NpGetOnlineId(int user_id, NpOnlineId* online_id);
int KYTY_SYSV_ABI NpGetAccountIdA(int user_id, uint64_t* account_id);
int KYTY_SYSV_ABI NpGetAccountCountryA(int user_id, void* country_code);
int KYTY_SYSV_ABI NpGetAccountAge(int req_id, int user_id, uint8_t* age);
int KYTY_SYSV_ABI NpGetAccountLanguage2(int req_id, int user_id, void* language_code);
int KYTY_SYSV_ABI NpCreateRequest();
int KYTY_SYSV_ABI NpCreateAsyncRequest(const NpCreateAsyncRequestParameter* param);
int KYTY_SYSV_ABI NpDeleteRequest(int req_id);
+10
View File
@@ -0,0 +1,10 @@
#ifndef EMULATOR_LIBS_SYSTEMSERVICE_H_
#define EMULATOR_LIBS_SYSTEMSERVICE_H_
namespace Libs::SystemService {
void NotifyEntitlementUpdate();
} // namespace Libs::SystemService
#endif
+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;
}
+352 -1
View File
@@ -45,6 +45,9 @@ void InitLibKernel_1_Apr(Loader::SymbolDatabase *symbols);
namespace Libs {
void InitLibKernel_1(Loader::SymbolDatabase *symbols);
void InitSysmodule_1(Loader::SymbolDatabase *symbols);
void InitSystemService_1(Loader::SymbolDatabase *symbols);
void InitAppContent_1(Loader::SymbolDatabase *symbols);
}
namespace Libs::LibAmpr {
@@ -59,6 +62,10 @@ namespace Libs::LibNpWebApi2 {
void InitNet_1_NpWebApi2(Loader::SymbolDatabase *symbols);
}
namespace Libs::LibNpCommerce {
void InitNet_1_NpCommerce(Loader::SymbolDatabase *symbols);
}
namespace {
namespace FileSystem = Libs::LibKernel::FileSystem;
@@ -121,6 +128,138 @@ void CheckSaveRename(const std::filesystem::path &root,
"renamed save contents");
}
void TestRandomDevices() {
for (const auto* path : {"/dev/urandom", "/dev/random"}) {
const int fd = FileSystem::KernelOpen(path, 0, 0);
FileSystem::FileStat stat {};
Check(fd >= 3 && FileSystem::KernelFstat(fd, &stat) == OK &&
(stat.st_mode & 0170000) == 0020000,
"entropy sources are character devices");
std::array<uint8_t, 32> entropy {};
Check(FileSystem::KernelRead(fd, entropy.data(), entropy.size()) == entropy.size(),
"character device supplies requested entropy");
Check(FileSystem::KernelClose(fd) == OK, "close entropy source");
}
}
void TestFileDescriptorFlags() {
Loader::SymbolDatabase symbols;
Libs::InitLibKernel_1(&symbols);
const auto* symbol = symbols.Find(
{"8nY19bKoiZk", "Posix", 1, "libkernel", 1, 1, Loader::SymbolType::Func});
Check(symbol != nullptr, "POSIX fcntl export resolves");
const auto fcntl = reinterpret_cast<int (KYTY_SYSV_ABI *)(int, int, int)>(symbol->vaddr);
Check(fcntl(std::numeric_limits<int>::min(), 1, 0) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF,
"invalid descriptor reports EBADF");
for (const auto* path : {"/dev/urandom", "/dev/random"}) {
const int fd = FileSystem::KernelOpen(path, 0, 0);
Check(fd >= 3, "open descriptor for flag checks");
Check(fcntl(fd, 1, 0) == 0 && fcntl(fd, 2, 1) == 0 && fcntl(fd, 1, 0) == 1,
"entropy descriptor retains close-on-exec flag");
Check(fcntl(fd, 2, 0) == 0 && fcntl(fd, 1, 0) == 0,
"close-on-exec flag can be cleared");
Check(fcntl(fd, -1, 0) == -1 && *Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
"unsupported fcntl command reports EINVAL");
Check(FileSystem::KernelClose(fd) == OK, "close entropy source");
Check(fcntl(fd, 1, 0) == -1 && *Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF,
"closed descriptor reports EBADF");
const int cloexec_fd = FileSystem::KernelOpen(path, 0x00100000, 0);
Check(cloexec_fd >= 3 && fcntl(cloexec_fd, 1, 0) == 1,
"O_CLOEXEC sets the descriptor flag on open");
Check(FileSystem::KernelClose(cloexec_fd) == OK, "close flagged entropy source");
}
}
void TestSysmoduleReferences() {
Loader::SymbolDatabase symbols;
Libs::InitSysmodule_1(&symbols);
using ModuleCall = int (KYTY_SYSV_ABI *)(uint16_t);
const auto resolve = [&](const char* nid) {
const auto* symbol = symbols.Find(
{nid, "Sysmodule", 1, "Sysmodule", 1, 1, Loader::SymbolType::Func});
Check(symbol != nullptr, "Sysmodule export resolves");
return reinterpret_cast<ModuleCall>(symbol->vaddr);
};
const auto load = resolve("g8cM39EUZ6o");
const auto unload = resolve("eR2bZFAAU0Q");
const auto is_loaded = resolve("fMP5NHUOaMk");
constexpr int unloaded = static_cast<int>(0x805a1001u);
Check(load(0x113) == OK && is_loaded(0xb4) == unloaded,
"loading entitlement access does not mark AppContent loaded");
Check(unload(0x113) == OK, "release independent module reference");
for (const uint16_t id : {0x113, 0xb4}) {
Check(is_loaded(id) == unloaded, "unloaded module allows guest initialization");
Check(unload(id) == unloaded, "unloading an absent module reports UNLOADED");
Check(load(id) == OK && load(id) == OK && is_loaded(id) == OK,
"repeated loads retain references");
Check(unload(id) == OK && is_loaded(id) == OK,
"one unload preserves the remaining reference");
Check(unload(id) == OK && is_loaded(id) == unloaded,
"last unload restores unloaded status");
}
const auto* internal_symbol = symbols.Find(
{"hHrGoGoNf+s", "Sysmodule", 1, "Sysmodule", 1, 1, Loader::SymbolType::Func});
Check(internal_symbol != nullptr, "internal Sysmodule load export resolves");
const auto internal_load =
reinterpret_cast<int (KYTY_SYSV_ABI *)(uint16_t, int, int, int, int*)>(
internal_symbol->vaddr);
int result = -1;
Check(internal_load(0xb4, 0, 0, 0, &result) == OK && result == OK &&
is_loaded(0xb4) == OK && unload(0xb4) == OK && is_loaded(0xb4) == unloaded,
"internal and public module operations share load state");
}
void TestSystemServiceEntitlementEvents() {
Loader::SymbolDatabase symbols;
Libs::InitSystemService_1(&symbols);
Libs::InitAppContent_1(&symbols);
const auto resolve = [&](const char* nid, const char* library, const char* module) {
const auto* symbol = symbols.Find(
{nid, library, 1, module, 1, 1, Loader::SymbolType::Func});
Check(symbol != nullptr, "entitlement event export resolves");
return symbol->vaddr;
};
const auto initialize = reinterpret_cast<int (KYTY_SYSV_ABI *)(const void*, void*)>(
resolve("R9lA82OraNs", "AppContent", "AppContentUtil"));
struct Status {
int32_t event_num;
bool overlay, background, vr;
uint8_t reserved[127];
};
struct Event {
int32_t type;
uint8_t data[8192];
};
static_assert(sizeof(Status) == 136 && sizeof(Event) == 8196);
const auto get_status = reinterpret_cast<int (KYTY_SYSV_ABI *)(Status*)>(
resolve("rPo6tV8D9bM", "SystemService", "SystemService"));
const auto receive = reinterpret_cast<int (KYTY_SYSV_ABI *)(Event*)>(
resolve("656LMQSrg6U", "SystemService", "SystemService"));
Status status {};
Event event {};
Check(get_status(&status) == OK && status.event_num == 0,
"SystemService starts without pending events");
std::array<uint8_t, 32> init {};
std::array<uint8_t, 40> boot {};
Check(initialize(init.data(), boot.data()) == OK,
"AppContent initialization generates an entitlement notification");
Check(get_status(&status) == OK && status.event_num == 1 &&
get_status(&status) == OK && status.event_num == 1,
"status queries preserve pending notifications");
Check(receive(nullptr) == Libs::SystemService::SYSTEM_SERVICE_ERROR_PARAMETER &&
get_status(&status) == OK && status.event_num == 1,
"invalid event output does not consume a notification");
std::memset(&event, 0xff, sizeof(event));
Check(receive(&event) == OK && event.type == 0x10000003 &&
std::all_of(std::begin(event.data), std::end(event.data),
[](uint8_t byte) { return byte == 0; }) &&
get_status(&status) == OK && status.event_num == 0,
"receive delivers the entitlement event with cleared payload exactly once");
Check(receive(&event) == Libs::SystemService::SYSTEM_SERVICE_ERROR_NO_EVENT,
"empty event queue reports NO_EVENT");
}
void TestSaveOpenVisibility() {
constexpr char Path[] = "/savedata0/visible-save.dat";
constexpr char Payload[] = "saved progress";
@@ -250,6 +389,91 @@ void TestAioBatches() {
Check(FileSystem::KernelClose(fd) == OK, "close AIO read fixture");
}
void TestNpCommerceDialog() {
Loader::SymbolDatabase symbols;
Libs::LibNpCommerce::InitNet_1_NpCommerce(&symbols);
const auto find = [&](const char *nid) {
const auto *symbol = symbols.FindByNid(nid, Loader::SymbolType::Func);
Check(symbol != nullptr, "NpCommerce dialog exports resolve");
return symbol->vaddr;
};
using Call = int (KYTY_SYSV_ABI *)();
using Open = int (KYTY_SYSV_ABI *)(const void *);
using GetResult = int (KYTY_SYSV_ABI *)(void *);
const auto initialize = reinterpret_cast<Call>(find("0aR2aWmQal4"));
const auto terminate = reinterpret_cast<Call>(find("m-I92Ab50W8"));
const auto update = reinterpret_cast<Call>(find("LR5cwFMMCVE"));
const auto open = reinterpret_cast<Open>(find("DfSCDRA3EjY"));
const auto get_result = reinterpret_cast<GetResult>(find("r42bWcQbtZY"));
struct BaseParam {
uint64_t size;
uint8_t reserved[36];
uint32_t magic;
};
struct Param {
BaseParam base;
int32_t size, user_id, mode;
uint32_t service_label;
const char *const *targets;
uint32_t num_targets, padding;
uint64_t features;
void *user_data;
uint8_t reserved[32];
} param {};
struct Result {
int32_t result;
bool authorized;
void *user_data;
uint8_t reserved[32];
} result {};
static_assert(sizeof(Param) == 128 && offsetof(Param, user_data) == 88);
static_assert(sizeof(Result) == 48 && offsetof(Result, user_data) == 8);
param.base.size = sizeof(BaseParam);
param.base.magic = static_cast<uint32_t>(0xC0D1A109u + reinterpret_cast<uintptr_t>(&param.base));
param.size = sizeof(param);
param.user_id = 1;
param.user_data = &symbols;
Check(open(&param) == static_cast<int>(0x80B80003u) &&
get_result(&result) == static_cast<int>(0x80B80003u),
"NpCommerce dialog calls require initialization");
Check(initialize() == OK && update() == 1 &&
get_result(&result) == static_cast<int>(0x80B80005u),
"NpCommerce dialog result requires completion");
Check(open(nullptr) == static_cast<int>(0x80B8000Du) && update() == 1 &&
get_result(nullptr) == static_cast<int>(0x80B8000Du),
"NpCommerce dialog rejects null arguments");
Check(open(&param) == OK && update() == 3, "NpCommerce dialog finishes without a store");
param.user_data = nullptr;
result.authorized = true;
Check(get_result(&result) == 1 && result.result == 1 && !result.authorized &&
result.user_data == &symbols,
"NpCommerce cancellation returns the original user data");
param.mode = 5;
Check(open(&param) == OK && get_result(&result) == 1 && !result.authorized &&
result.user_data == nullptr,
"NpCommerce dialog can reopen with fresh result data");
const auto invalid_param = [&] {
Check(open(&param) == static_cast<int>(0x80B8000Au) && update() == 3 &&
get_result(&result) == static_cast<int>(0x80B8000Au) &&
result.result == static_cast<int>(0x80B8000Au) && !result.authorized,
"NpCommerce dialog reports invalid parameters in the completion result");
};
param.mode = 99;
invalid_param();
param.mode = -1;
invalid_param();
param.mode = 0;
param.size = 64;
invalid_param();
param.size = sizeof(param);
param.reserved[0] = 1;
invalid_param();
param.reserved[0] = 0;
Check(open(&param) == OK && get_result(&result) == 1 && terminate() == OK && update() == 0 &&
get_result(&result) == static_cast<int>(0x80B80003u),
"NpCommerce dialog recovers from invalid parameters and terminates");
}
void TestNpWebApi2Memory() {
Loader::SymbolDatabase symbols;
Libs::LibNpWebApi2::InitNet_1_NpWebApi2(&symbols);
@@ -669,6 +893,10 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
using Offset = uint64_t (KYTY_SYSV_ABI *)(void *);
using WaitAddress = int (KYTY_SYSV_ABI *)(void *, volatile uint64_t *, uint64_t,
uint8_t, uint8_t);
using WaitCounter = int (KYTY_SYSV_ABI *)(void *, uint8_t, uint8_t, uint64_t,
uint8_t, uint8_t, uint64_t, uint8_t);
using WriteCounter = int (KYTY_SYSV_ABI *)(void *, uint8_t, uint8_t, uint64_t,
uint8_t, uint32_t);
using WriteAddress = int (KYTY_SYSV_ABI *)(void *, volatile uint64_t *, uint64_t);
using ReadFile = int (KYTY_SYSV_ABI *)(void *, uint64_t, uint64_t, uint32_t,
void *, uint64_t, uint64_t);
@@ -685,6 +913,8 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
const auto set_buffer = reinterpret_cast<SetBuffer>(find("N-FSPA4S3nI"));
const auto offset = reinterpret_cast<Offset>(find("GnxKOHEawhk"));
const auto wait_address = reinterpret_cast<WaitAddress>(find("DLfoNxTFNVk"));
const auto wait_counter = reinterpret_cast<WaitCounter>(find("cQb8Zr8Q0Y0"));
const auto write_counter = reinterpret_cast<WriteCounter>(find("jK+yuYCI7MA"));
const auto write_address = reinterpret_cast<WriteAddress>(find("sJXyWHjP-F8"));
const auto read_file = reinterpret_cast<ReadFile>(find("mQ16-QdKv7k"));
const auto write_event = reinterpret_cast<WriteKernelEvent>(find("H896Pt-yB4I"));
@@ -758,6 +988,59 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
"submission wait observes the completed result");
}
}
constexpr std::array counter_comparisons {
Comparison{1, 0, 0, 1}, Comparison{1, 0, 0, 1},
Comparison{4, 0x7ffffffe, 0x7fffffff, 0x80000000},
Comparison{5, 0xffffffff, 0, 1}};
for (const auto &comparison : counter_comparisons) {
reset(1);
auto *producer = buffers[0].header.data();
auto *consumer = buffers[1].header.data();
auto *lower = buffers[2].header.data();
constexpr uint8_t Counter = 127;
constexpr auto Invalid = Libs::LibKernel::KERNEL_ERROR_EINVAL;
Check(write_counter(producer, 128, 1, 1, 0, 0) == Invalid &&
write_counter(producer, Counter, 8, 1, 0, 0) == Invalid &&
write_counter(producer, Counter, 1, 1, 5, 0) == Invalid &&
write_counter(producer, Counter, 1, 1, 0, 2) == Invalid &&
wait_counter(consumer, Counter, 8, 0, 1, 0, 0, 0) == Invalid &&
wait_counter(consumer, Counter, 1, 0, 7, 0, 0, 0) == Invalid &&
wait_counter(consumer, Counter, 1, 0, 1, 2, 0, 0) == Invalid &&
wait_counter(consumer, Counter, 1, 0, 1, 0, 0, 2) == Invalid &&
offset(producer) == 0 && offset(consumer) == 0,
"invalid counter operations fail without appending a command");
uint64_t retired = 0, lower_done = 0;
std::array<char, 3> output {};
Result result {1234, 5678};
std::array<uint32_t, 4> ids {};
if (comparison.blocked != 0) {
auto *initializer = buffers[3].header.data();
Check(write_counter(initializer, Counter, 1, comparison.blocked, 0, 0) == OK &&
submit_amm(buffers[3].data.data(), static_cast<uint32_t>(offset(initializer)),
0, &ids[3]) == OK && wait_amm(ids[3]) == OK,
"initialize shared counter before dependent submissions");
}
Check(read_file(producer, reinterpret_cast<uint64_t>(&buffers[0].header[3]),
reinterpret_cast<uint64_t>(&buffers[0].header[4]), file_id,
output.data(), output.size(), 0) == OK &&
write_counter(producer, Counter, 1, comparison.released, 0, 0) == OK &&
wait_counter(consumer, Counter, 1, comparison.reference, comparison.compare,
0, 0, 0) == OK &&
write_counter(consumer, Counter, 1, 0, 0, 0) == OK &&
write_address(consumer, &retired, 1) == OK &&
write_address(lower, &lower_done, 1) == OK,
"build APR completion and dependent AMM counter reset");
Check(submit_amm(buffers[1].data.data(), static_cast<uint32_t>(offset(consumer)),
0, &ids[1]) == OK &&
submit_amm(buffers[2].data.data(), static_cast<uint32_t>(offset(lower)),
1, &ids[2]) == OK && wait_amm(ids[2]) == OK &&
lower_done == 1 && retired == 0,
"counter wait blocks AMM retirement while its lower priority progresses");
Check(submit_apr(producer, 3, &result, &ids[0]) == OK && wait_amm(ids[1]) == OK &&
wait_apr(ids[0]) == OK && result.result == OK && retired == 1 &&
std::memcmp(output.data(), "APR", 3) == 0,
"shared counter completion releases AMM only after APR read and resets for reuse");
}
reset(1);
namespace EventQueue = Libs::LibKernel::EventQueue;
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
@@ -857,6 +1140,36 @@ void CheckAprPaths(const std::filesystem::path &root) {
ids[0] == 0xffffffffu && ids[1] == expected_id && sizes[0] == 0 && sizes[1] == 3,
"APR foreach reports a missing path and continues to the valid file");
// These paths share the old 31-bit FNV-1a ID (0x122d1544).
const char *collision_paths[] = {"perf-audit/test_00015ddf.bin",
"perf-audit/test_000389b8.bin"};
Check(std::filesystem::create_directory(root / "perf-audit"), "create APR collision directory");
for (size_t i = 0; i < 2; ++i) {
Check(fixture.Create(root / collision_paths[i]), "create APR collision fixture");
fixture.Write(i == 0 ? "APR" : "OTHER", i == 0 ? 3 : 5);
fixture.Close();
}
uint32_t collision_ids[2] = {};
Check(resolve("/app0/", collision_paths, 2, collision_ids, sizes, &error_index) == OK &&
collision_ids[0] != collision_ids[1] && collision_ids[0] != 0xffffffffu &&
collision_ids[1] != 0xffffffffu && sizes[0] == 3 && sizes[1] == 5,
"APR assigns distinct valid IDs to colliding paths");
const auto *stat_symbol = symbols.FindByNid("ApkYaHb8Sek", Loader::SymbolType::Func);
const auto *size_symbol = symbols.FindByNid("WvEu7yl3Ivg", Loader::SymbolType::Func);
Check(stat_symbol && size_symbol, "APR file metadata exports are registered");
using Stat = int (KYTY_SYSV_ABI *)(uint32_t, FileSystem::FileStat *);
using Size = int (KYTY_SYSV_ABI *)(uint32_t, uint64_t *);
for (size_t i = 0; i < 2; ++i) {
FileSystem::FileStat stat {};
uint64_t size = 0;
Check(resolve("/app0/", &collision_paths[i], 1, ids, nullptr, &error_index) == OK &&
ids[0] == collision_ids[i] &&
reinterpret_cast<Stat>(stat_symbol->vaddr)(ids[0], &stat) == OK &&
reinterpret_cast<Size>(size_symbol->vaddr)(ids[0], &size) == OK &&
stat.st_size == static_cast<int64_t>(sizes[i]) && size == sizes[i],
"APR re-resolution preserves each file's ID, host path and cached size");
}
// PATH_MAX includes NUL; all components remain below NAME_MAX (255).
std::string longest = "/app0/";
for (int i = 0; i < 3; ++i) {
@@ -880,7 +1193,7 @@ void CheckAprPaths(const std::filesystem::path &root) {
Check(resolve(unterminated.data(), paths, 1, ids, sizes, &error_index) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_ENAMETOOLONG,
"APR rejects an unterminated prefix");
CheckAmprOrdering(symbols, expected_id);
CheckAmprOrdering(symbols, collision_ids[0]);
FileSystem::Umount("/app0");
}
@@ -1088,6 +1401,38 @@ void CheckSocketReceiveBuffer(int reader, int writer) {
}
#endif
void CheckEtherAddressFormatting() {
Loader::SymbolDatabase symbols;
Libs::LibNet::InitNet_1_Net(&symbols);
const auto *format_symbol = symbols.FindByNid("v6M4txecCuo", Loader::SymbolType::Func);
const auto *errno_symbol = symbols.FindByNid("HQOwnfMGipQ", Loader::SymbolType::Func);
Check(format_symbol && errno_symbol, "Ethernet formatting and errno exports resolve");
using Format = int (KYTY_SYSV_ABI *)(const Libs::Network::Net::NetEtherAddr *, char *, size_t);
using Errno = int *(KYTY_SYSV_ABI *)();
const auto format = reinterpret_cast<Format>(format_symbol->vaddr);
auto *net_errno = reinterpret_cast<Errno>(errno_symbol->vaddr)();
for (const auto address : {Libs::Network::Net::NetEtherAddr{},
Libs::Network::Net::NetEtherAddr{{0x01, 0x23, 0x45, 0xab, 0xcd, 0xef}}}) {
const auto *expected = address.data[0] == 0 ? "00:00:00:00:00:00" : "01:23:45:ab:cd:ef";
for (const size_t size : {18u, 127u}) {
std::array<char, 128> text;
text.fill('!');
Check(format(&address, text.data(), size) == OK &&
std::strcmp(text.data(), expected) == 0 && text[18] == '!',
"Ethernet formatting accepts exact and larger buffers");
}
}
const Libs::Network::Net::NetEtherAddr address{};
std::array<char, 18> text;
text.fill('!');
Check(format(&address, text.data(), 17) == Libs::Network::NET_ERROR_EINVAL &&
*net_errno == Libs::Posix::POSIX_EINVAL &&
std::all_of(text.begin(), text.end(), [](char c) { return c == '!'; }) &&
format(nullptr, text.data(), text.size()) == Libs::Network::NET_ERROR_EINVAL &&
format(&address, nullptr, text.size()) == Libs::Network::NET_ERROR_EINVAL,
"Ethernet formatting rejects invalid arguments without writing output");
}
void CheckSocketWakeup() {
namespace Net = Libs::Network::Net;
Loader::SymbolDatabase symbols;
@@ -1368,6 +1713,10 @@ int main(int, char**) {
TempDirectory temporary;
FileSystem::Initialize();
TestSysmoduleReferences();
TestSystemServiceEntitlementEvents();
TestRandomDevices();
TestFileDescriptorFlags();
CheckMountRoot(temporary.Path());
CheckUnmappedPaths(temporary.Path());
CheckArchiveMount(temporary.Path());
@@ -1382,7 +1731,9 @@ int main(int, char**) {
CheckSaveRename(temporary.Path(), "replacement-save");
FileSystem::Shutdown();
CheckSocketWakeup();
CheckEtherAddressFormatting();
TestNpWebApi2Memory();
TestNpCommerceDialog();
graphics.reset();
subsystems.Destroy();
+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);
+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();