Compare commits

...
Author SHA1 Message Date
Felipe 415387f00d Support multiple controllers for local multiplayer (#1583)
* Support multiple controllers for local co-op
2026-10-17 13:15:22 +02:00
890393fbe6 vulkan: start without fragmentShaderBarycentric on GPUs that lack it (#1590)
* vulkan: start without fragmentShaderBarycentric on GPUs that lack it

Pascal (GTX 10xx), RDNA 1 and older drivers have no
VK_KHR_fragment_shader_barycentric, so device selection rejected them
with 'Could not find suitable device' (#1432, #1416, #762). The SPIR-V
backend only emits the capability for pixel shaders with
per-vertex/barycentric inputs, so make the extension and feature
optional: warn at selection and creation, enable only when supported.
Shaders that need custom barycentrics fail at pipeline creation
instead of blocking startup for every game.

* vulkan: fix macOS build and warn about unsupported GPUs

---------

Co-authored-by: Matthew Hurley <171001439+theantipopau@users.noreply.github.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-17 12:55:33 +02:00
nmzik 9f88043ebe shader: use compact integer RTZ half conversion 2026-10-17 12:23:12 +02:00
nmzik c2173801d4 shader: skip resources in proven empty unsigned loops 2026-10-17 10:38:14 +02:00
nmzik 56d03f16e9 network: UDPP2P 2026-10-17 10:24:45 +02:00
nmzik 3ecc6b7d1b network: implement POSIX socket shutdown on Linux 2026-10-17 10:10:16 +02:00
nmzik 5bf37f52ad renderer: clear compressed stencil with pending depth metadata 2026-10-17 09:50:11 +02:00
nmzik 57791645e4 renderer: track attachment load clears by image aspect 2026-10-17 09:34:51 +02:00
nmzik d1fe1324c8 shader: keep coherent 64bit atomic 2026-10-17 07:52:20 +02:00
nmzik b485dd24be system service: stub 2026-10-17 07:30:21 +02:00
nmzik ae25558321 shader: size NGG primitive outputs from amplification 2026-10-17 06:54:45 +02:00
nmzik ec08ea197b shader: new fill pattern 2026-10-17 06:24:29 +02:00
nmzik f6f1ac8b40 shader: batch dependent folds to avoid repeated SSA graph walks 2026-10-17 06:20:02 +02:00
Nawid3333andClaude Opus 5.5 b82512be06 shader: read float inline constants on 64-bit operands as f64 bits (#1576)
A float inline constant (240-248) read by a 64-bit operand is the f64 bit
pattern for integer B64 opcodes too: AMD's compilers emit
v_lshrrev_b64 v[4:5], v0, 1.0 for 0x3ff0000000000000 >> lane. ReadU32Pair
returned {f32 bits, 0}, so only F64-typed reads had the right value.

Based on TheCruZ's 284deab28 (closed #1219).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 05:21:06 +02:00
Nawid3333andClaude Opus 5.5 ba8b0c85b8 shader: keep quiet NaNs with payload bits out of the packed f16 min/max sNaN path (#1579)
V_PK_MIN_F16 and V_PK_MAX_F16 return the quieted NaN for a signaling NaN
operand and the other operand for a quiet one (per half, as V_MIN_F16 and
V_MAX_F16 do). The signaling test masked the half with 0x7c00 and checked
for a nonzero payload, so a quiet NaN with payload bits such as 0x7e01 was
taken as signaling and returned. Mask with 0x7e00 so the quiet bit must be
clear.

Two more vectors in PackedMinMaxF16NanAndSignedZeroEdges cover it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 05:12:37 +02:00
Nawid3333andClaude Opus 5.5 b6bd9ecae4 shader: read float inline constants on 16-bit operands as half bits (#1582)
The 16-bit encodings of the float inline constants are fixed: the exact
halves of 0.5, 1.0, 2.0 and 4.0 and their negations, and 0x3118 for
1/(2*pi). Read16LaneBits converted the 32-bit value at run time instead,
through PackHalf2x16, which the AMD driver rounds toward zero, so
1/(2*pi) became 0x3117. Use the encodings directly.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 05:05:40 +02:00
Felipe b95ea4bb5f Support mountable-content DLC (addcont with real content) (#1581)
Support mountable-content DLC (addcont) and fix PlayGo chunk count query
2026-10-17 04:44:28 +02:00
e93b4b0378 shader: support returning 32-bit LDS compare-exchange (DS_CMPST_RTN_B32) (#1578)
* shader: support DS_CMPST_RTN_B32

Assassin's Creed Valhalla (PPSA01490, #281) stops on DS opcode 0x30 in a
compute shader. DS compares DATA and stores DATA2, the opposite of
BUFFER_ATOMIC_CMPSWAP, so the 32-bit atomic path swaps the two for this
opcode, as the 64-bit form does.

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

* shader: reject unsupported GDS compare-store variant

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-17 04:37:16 +02:00
nmzik 0fd41ae206 Fix Linux CI xxHash include path (#1580)
Fix xxHash include path capitalization for test builds
2026-10-17 04:37:00 +02:00
nmzikandJuan Pablo Farias 9202bddad2 recognize verified pattern fills
Co-authored-by: Juan Pablo Farias <juanpablo.profissional.a.f@gmail.com>
2026-10-17 04:09:54 +02:00
nmzik 32587337b5 renderdoc: advance capture flips independently of logging 2026-10-17 04:09:54 +02:00
Çağatay Güçlü 02ead0e2e1 shader: 64-bit buffer atomic add (BUFFER_ATOMIC_ADD_X2) 2026-10-17 04:09:54 +02:00
NeySlim 4f1f3b1bb3 shader: make the f32 clamp modifier turn NaN into 0 2026-10-17 04:09:53 +02:00
Nawid3333andClaude Opus 5.5 42bb0da520 loader: keep the caller's flags in the TLS stubs (#1571)
A patched TLS load calls TlsRegStub and SafeCall, which moved RSP past the
red zone with sub/add rsp,0x80. ADD and SUB write the arithmetic flags, so
the sub ran before SafeCall's pushfq saved them and the add ran after its
popfq restored them: guest code that compares, loads fs:[0] and then
branches on the flags could branch the wrong way. lea moves RSP without
touching the flags. The entry lea plus a two-byte nop keeps every patched
offset in both stubs; the exit lea is one byte longer and moves only ret.

TlsStubsPreserveFlags calls the stubs with two flag patterns and checks
the status flags, the output register and RAX; it fails with the old
bytes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 00:05:08 +02:00
nmzik 5dec523143 Stub RazorCpu profiling markers in a dedicated library 2026-10-16 19:16:42 +02:00
nmzik d01633e18e Revert "Fix scalar buffer reads across sparse guest mappings"
This reverts commit d7974dc763.
2026-10-16 19:13:36 +02:00
nmzik e7ac242039 Load packaged system modules through the native module lifecycle 2026-10-16 19:13:36 +02:00
nmzik d7974dc763 Fix scalar buffer reads across sparse guest mappings 2026-10-16 18:20:31 +02:00
shadowbeat070 b899717900 renderer: log unsupported depth feedback once 2026-10-16 17:24:45 +02:00
90 changed files with 2078 additions and 437 deletions

No files matched your search

+1 -1
View File
@@ -350,7 +350,7 @@ set(inc_headers
${KYTY_THIRD_PARTY_DIR}/SPIRV-Tools/include
${KYTY_THIRD_PARTY_DIR}/SPIRV-Headers/include
${KYTY_THIRD_PARTY_DIR}/fmt/include
${KYTY_THIRD_PARTY_DIR}/xxhash
${KYTY_THIRD_PARTY_DIR}/xxHash
${KYTY_THIRD_PARTY_DIR}/cpuinfo/include
${KYTY_THIRD_PARTY_DIR}/renderdoc
${KYTY_THIRD_PARTY_DIR}/stb
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "تم كسب الكأس!"
"trophy.toast.earned": "تم كسب الكأس!",
"user.guest": "ضيف"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofej získána!"
"trophy.toast.earned": "Trofej získána!",
"user.guest": "Host"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofæ optjent!"
"trophy.toast.earned": "Trofæ optjent!",
"user.guest": "Gæst"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophäe erhalten!"
"trophy.toast.earned": "Trophäe erhalten!",
"user.guest": "Gast"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!"
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!",
"user.guest": "Επισκ."
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophy earned!"
"trophy.toast.earned": "Trophy earned!",
"user.guest": "Guest"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophy earned!"
"trophy.toast.earned": "Trophy earned!",
"user.guest": "Guest"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "¡Ganaste un trofeo!"
"trophy.toast.earned": "¡Ganaste un trofeo!",
"user.guest": "Invitado"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "¡Ganaste un trofeo!"
"trophy.toast.earned": "¡Ganaste un trofeo!",
"user.guest": "Invitado"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Pokaali ansaittu!"
"trophy.toast.earned": "Pokaali ansaittu!",
"user.guest": "Vieras"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophée obtenu !"
"trophy.toast.earned": "Trophée obtenu !",
"user.guest": "Invité"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophée obtenu !"
"trophy.toast.earned": "Trophée obtenu !",
"user.guest": "Invité"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trófea megszerezve!"
"trophy.toast.earned": "Trófea megszerezve!",
"user.guest": "Vendég"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofi diperoleh!"
"trophy.toast.earned": "Trofi diperoleh!",
"user.guest": "Tamu"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofeo ottenuto!"
"trophy.toast.earned": "Trofeo ottenuto!",
"user.guest": "Ospite"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "トロフィーを獲得しました!"
"trophy.toast.earned": "トロフィーを獲得しました!",
"user.guest": "ゲスト"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "트로피를 획득했습니다!"
"trophy.toast.earned": "트로피를 획득했습니다!",
"user.guest": "게스트"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofee behaald!"
"trophy.toast.earned": "Trofee behaald!",
"user.guest": "Gast"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofé opptjent!"
"trophy.toast.earned": "Trofé opptjent!",
"user.guest": "Gjest"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Zdobyto trofeum!"
"trophy.toast.earned": "Zdobyto trofeum!",
"user.guest": "Gość"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Troféu conquistado!"
"trophy.toast.earned": "Troféu conquistado!",
"user.guest": "Convidado"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Troféu conquistado!"
"trophy.toast.earned": "Troféu conquistado!",
"user.guest": "Convidado"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofeu câștigat!"
"trophy.toast.earned": "Trofeu câștigat!",
"user.guest": "Invitat"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Трофей получен!"
"trophy.toast.earned": "Трофей получен!",
"user.guest": "Гость"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofé intjänad!"
"trophy.toast.earned": "Trofé intjänad!",
"user.guest": "Gäst"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!"
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!",
"user.guest": "แขก"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Kupa kazanıldı!"
"trophy.toast.earned": "Kupa kazanıldı!",
"user.guest": "Misafir"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Đã nhận cúp!"
"trophy.toast.earned": "Đã nhận cúp!",
"user.guest": "Khách"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "已获得奖杯!"
"trophy.toast.earned": "已获得奖杯!",
"user.guest": "访客"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "已獲得獎盃!"
"trophy.toast.earned": "已獲得獎盃!",
"user.guest": "訪客"
}
+22
View File
@@ -24,6 +24,7 @@ void Load(const ConfigOptions& cfg) {
EXIT_IF(cfg.user_name.empty() || cfg.user_name.size() > MAX_USER_NAME_LENGTH);
EXIT_IF(!IsConfiguredUserIdValid(cfg.user_id));
EXIT_IF(cfg.controller_speaker_volume > 100 || cfg.controller_vibration_intensity > 100);
EXIT_IF(cfg.keyboard_player > static_cast<uint32_t>(MAX_PLAYERS));
*g_config = cfg;
}
@@ -60,6 +61,27 @@ uint32_t GetControllerVibrationIntensity() {
return g_config->controller_vibration_intensity;
}
uint32_t GetKeyboardPlayer() {
return g_config->keyboard_player;
}
int32_t GetPlayerUserId(int player) {
return g_config->user_id + player * PLAYER_USER_ID_STEP;
}
int GetPlayerFromUserId(int32_t user_id) {
for (int player = 0; player < MAX_PLAYERS; player++) {
if (user_id == GetPlayerUserId(player)) {
return player;
}
}
return -1;
}
bool IsGuestUserId(int32_t user_id) {
return GetPlayerFromUserId(user_id) > 0;
}
PresentMode GetPresentMode() {
return g_config->present_mode;
}
+17 -1
View File
@@ -6,6 +6,7 @@
#include <cstddef>
#include <array>
#include <filesystem>
#include <limits>
#include <optional>
#include <string>
#include <vector>
@@ -35,10 +36,16 @@ constexpr uint32_t MAX_CONSOLE_LANGUAGE = 29;
constexpr std::size_t MAX_USER_NAME_LENGTH = 16;
constexpr int32_t DEFAULT_USER_ID = 1000;
constexpr int MAX_PLAYERS = 4;
constexpr int32_t PLAYER_USER_ID_STEP = 0x100;
constexpr int32_t MAX_USER_ID =
std::numeric_limits<int32_t>::max() - (MAX_PLAYERS - 1) * PLAYER_USER_ID_STEP;
constexpr bool IsConfiguredUserIdValid(int32_t user_id) {
constexpr int32_t USER_ID_EVERYONE = 0xfe;
constexpr int32_t USER_ID_SYSTEM = 0xff;
return user_id >= 0 && user_id != USER_ID_EVERYONE && user_id != USER_ID_SYSTEM;
return user_id >= 0 && user_id <= MAX_USER_ID && user_id != USER_ID_EVERYONE &&
user_id != USER_ID_SYSTEM;
}
struct ConfigOptions {
@@ -50,6 +57,8 @@ struct ConfigOptions {
std::optional<ControllerColor> controller_color;
uint32_t controller_speaker_volume = 50;
uint32_t controller_vibration_intensity = 100;
// 0 shares player 1 with the first gamepad; 1..MAX_PLAYERS dedicates that player to the keyboard.
uint32_t keyboard_player = 0;
PresentMode present_mode = PresentMode::Mailbox;
int32_t gpu_index = -1;
bool fullscreen_enabled = false;
@@ -100,6 +109,13 @@ const std::string& GetAudioInputDevice();
const std::optional<ControllerColor>& GetControllerColor();
uint32_t GetControllerSpeakerVolume();
uint32_t GetControllerVibrationIntensity();
uint32_t GetKeyboardPlayer();
// Player 0 is the configured user; later players derive their IDs from it.
int32_t GetPlayerUserId(int player);
// Returns -1 when the user ID does not belong to any player.
int GetPlayerFromUserId(int32_t user_id);
// Players after the first are guests: they have no profile, trophies or NP account.
bool IsGuestUserId(int32_t user_id);
PresentMode GetPresentMode();
int32_t GetGpuIndex();
bool FullscreenEnabled();
@@ -311,7 +311,6 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
}
r.desc = MakeDepthTargetDesc(buffer, z);
r.depth_clear_enable = rc.depth_clear_enable;
r.depth_load_clear_enable = r.depth_clear_enable;
r.depth_clear_value = hw.GetDepthClearValue();
r.depth_test_enable = dc.z_enable;
r.depth_write_enable = r.depth_test_enable && dc.z_write_enable &&
@@ -421,9 +420,9 @@ vk::ImageAspectFlags RenderDepthInfo::AttachmentWriteAspects() const {
}
const auto available = ImageViewOps::DepthAspectMask(format);
vk::ImageAspectFlags writes {};
vk::ImageAspectFlags writes = load_clear_aspects & available;
if ((available & vk::ImageAspectFlagBits::eDepth) &&
(depth_load_clear_enable || depth_write_enable)) {
depth_write_enable) {
writes |= vk::ImageAspectFlagBits::eDepth;
}
if (!(available & vk::ImageAspectFlagBits::eStencil)) {
@@ -461,8 +460,7 @@ vk::ImageAspectFlags RenderDepthInfo::AttachmentWriteAspects() const {
(can_pass && depth_pass && state.passOp != vk::StencilOp::eKeep) ||
(can_pass && depth_fail && state.depthFailOp != vk::StencilOp::eKeep);
};
if (stencil_clear_enable ||
(stencil_test_enable && (face_writes(stencil_front) || face_writes(stencil_back)))) {
if (stencil_test_enable && (face_writes(stencil_front) || face_writes(stencil_back))) {
writes |= vk::ImageAspectFlagBits::eStencil;
}
return writes;
@@ -22,7 +22,7 @@ struct RenderDepthInfo {
// Discovery keeps guest image information but can remap the view into a larger cache image.
TextureCache::ImageDesc desc;
bool depth_clear_enable = false;
bool depth_load_clear_enable = false;
vk::ImageAspectFlags load_clear_aspects;
float depth_clear_value = 0.0f;
bool depth_test_enable = false;
// Effective draw writes; discovery applies test, target-write and clear controls.
@@ -94,9 +94,10 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
constexpr std::array formats {
Prospero::BufferFormat::k32UInt, Prospero::BufferFormat::k32_32UInt,
Prospero::BufferFormat::k32_32_32UInt, Prospero::BufferFormat::k32_32_32_32UInt};
// Offset-or-swizzle bounds do not describe the element range proved by the shader.
if (descriptor.Stride() != element_size || descriptor.Format() != formats[fill.words - 1] ||
descriptor.SwizzleEnabled() || descriptor.IndexStride() != 0 || descriptor.AddTid() ||
descriptor.Base48() == 0) {
descriptor.OutOfBounds() == 3 || descriptor.Base48() == 0) {
return false;
}
if (input.threads_num[0] == 0 || input.threads_num[0] != fill.group_stride[0] ||
@@ -108,7 +109,11 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
? group_x
: static_cast<uint64_t>(group_x) * input.threads_num[0];
const auto size = descriptor.GetSize();
if (invocations != descriptor.NumRecords() || size == 0 || size > UINT32_MAX ||
const auto elements = descriptor.NumRecords();
const bool covered = fill.bounded_elements != 0
? fill.bounded_elements == elements && invocations >= elements
: invocations == elements;
if (!covered || invocations > uint64_t {UINT32_MAX} + 1 || size == 0 || size > UINT32_MAX ||
(input.dispatch_thread_dimensions &&
(group_x % input.threads_num[0] != 0 || input.dispatch_threads_num[0] != group_x ||
input.dispatch_threads_num[1] != 1 || input.dispatch_threads_num[2] != 1))) {
@@ -252,10 +257,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
return;
}
if (sh_ctx.GetCs().cs_regs.data_addr == 0) {
return;
}
constexpr uint32_t DISPATCH_INITIATOR_USE_THREAD_DIMENSIONS = 1u << 5u;
constexpr uint32_t DISPATCH_INITIATOR_BASE_BITS = 0x41u;
constexpr uint32_t DISPATCH_INITIATOR_MODIFIER_BITS = 0xa038u;
+15 -9
View File
@@ -507,14 +507,15 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
}
const auto image_view = cache.FindDepthTarget(depth.image_id, depth.desc);
const auto& metadata = depth.desc.info.metadata;
if (metadata.kind == ImageMetadataKind::Htile && depth.depth_clear_enable &&
!cache.ClearMeta(metadata.range.address)) {
EXIT("failed to acquire HTile metadata for a depth clear\n");
}
const bool meta_clear =
metadata.kind == ImageMetadataKind::Htile &&
cache.IsMetaCleared(metadata.range.address, depth.desc.view_info.base_layer);
depth.depth_load_clear_enable = depth.depth_clear_enable || meta_clear;
depth.load_clear_aspects = {};
if (depth.depth_clear_enable || meta_clear)
depth.load_clear_aspects |= vk::ImageAspectFlagBits::eDepth;
// Apply pending clears to compressed stencil as well as depth.
if (depth.stencil_clear_enable || (meta_clear && metadata.stencil_compressed))
depth.load_clear_aspects |= vk::ImageAspectFlagBits::eStencil;
if (meta_clear &&
!cache.TouchMeta(metadata.range.address, depth.desc.view_info.base_layer, false)) {
EXIT("failed to consume HTile clear state\n");
@@ -536,7 +537,10 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
}
}
if (feedback_aspects && !m_context.GetGraphics().attachment_feedback_loop_enabled) {
EXIT("depth attachment feedback loop is not supported by the host\n");
static std::atomic_bool logged = false;
if (!logged.exchange(true, std::memory_order_relaxed)) {
std::printf("depth attachment feedback loop is not supported by the host\n");
}
}
auto layout = depth_attachment_layout(depth);
if (sampled_aspects & ~DepthReadableAspects(layout)) {
@@ -564,9 +568,11 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
attachment.clear_value[0] = std::bit_cast<uint32_t>(depth.depth_clear_value);
attachment.clear_value[1] = depth.stencil_clear_value;
attachment.has_depth = static_cast<bool>(aspects & vk::ImageAspectFlagBits::eDepth);
attachment.depth_clear = depth.depth_load_clear_enable;
attachment.depth_clear =
static_cast<bool>(depth.load_clear_aspects & vk::ImageAspectFlagBits::eDepth);
attachment.has_stencil = static_cast<bool>(aspects & vk::ImageAspectFlagBits::eStencil);
attachment.stencil_clear = depth.stencil_clear_enable;
attachment.stencil_clear =
static_cast<bool>(depth.load_clear_aspects & vk::ImageAspectFlagBits::eStencil);
}
if (color_count == 0 && !depth.image_id) {
const auto& limits = buffer.GetGraphics().GetPhysicalDeviceProperties().limits;
@@ -1057,7 +1063,7 @@ bool RenderExecutor::CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo&
state.ps_input_info.ps_sample_mask_export_enable);
vk::ImageAspectFlags feedback;
const auto rendering = AcquireRenderTargets(buffer, &color, 1, depth, feedback);
if (depth.depth_load_clear_enable) {
if (depth.load_clear_aspects) {
m_context.GetCommandScheduler().BeginRendering(rendering);
}
auto& cache = m_context.GetTextureCache();
+2 -1
View File
@@ -162,7 +162,8 @@ void RenderDocOnGuestFlip(RenderContext& renderer) {
return;
}
if (state == RenderDocState::Capturing) {
LOGF("RenderDoc: captured guest flip %u/2\n", ++g_captured_flips);
++g_captured_flips;
LOGF("RenderDoc: captured guest flip %u/2\n", g_captured_flips);
if (g_captured_flips < 2) {
return;
}
@@ -254,7 +254,8 @@ public:
}
const bool reserved = binding.key == SDLK_ESCAPE || binding.key == SDLK_F1 ||
binding.key == SDLK_F7 || binding.key == SDLK_F11;
binding.key == SDLK_F7 || binding.key == SDLK_F8 ||
binding.key == SDLK_F11;
if (binding.control == INVALID_CONTROL || reserved ||
(binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) {
EXIT("Invalid input mapping: %s\n", value.c_str());
@@ -266,7 +266,6 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
#endif
#if !defined(__APPLE__)
check_feature(device_features2.features.depthClamp, "depthClamp");
check_feature(fragment_barycentric.fragmentShaderBarycentric, "fragmentShaderBarycentric");
#endif
check_feature(features12.samplerMirrorClampToEdge, "samplerMirrorClampToEdge",
@@ -498,7 +497,21 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
image_atomic_int64.pNext = supported_features2.pNext;
supported_features2.pNext = &image_atomic_int64;
}
const bool barycentric_extension =
HasExtension(device_extensions, VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME);
vk::PhysicalDeviceFragmentShaderBarycentricFeaturesKHR supported_barycentric {};
if (barycentric_extension) {
supported_barycentric.pNext = supported_features2.pNext;
supported_features2.pNext = &supported_barycentric;
}
physical_device.getFeatures2(&supported_features2);
const bool barycentric_supported =
barycentric_extension && supported_barycentric.fragmentShaderBarycentric == VK_TRUE;
if (!barycentric_supported) {
Log::WriteToConsoleAndLog(
"WARNING: Your GPU is not fully supported (fragmentShaderBarycentric is unavailable). "
"Some games may render incorrectly or crash.\n");
}
graphics.shader_image_int64_atomics_enabled = image_atomic_int64.shaderImageInt64Atomics;
auto features12 = WindowContext::RequiredVulkan12Features();
@@ -610,9 +623,13 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
robustness2.pNext = &features12;
#else
vk::PhysicalDeviceFragmentShaderBarycentricFeaturesKHR fragment_barycentric {};
fragment_barycentric.pNext = &features12;
fragment_barycentric.fragmentShaderBarycentric = VK_TRUE;
robustness2.pNext = &fragment_barycentric;
if (barycentric_supported) {
fragment_barycentric.pNext = &features12;
fragment_barycentric.fragmentShaderBarycentric = VK_TRUE;
robustness2.pNext = &fragment_barycentric;
} else {
robustness2.pNext = &features12;
}
#endif
robustness2.robustBufferAccess2 = supported_robustness2.robustBufferAccess2;
robustness2.robustImageAccess2 = supported_robustness2.robustImageAccess2;
@@ -966,7 +983,6 @@ void WindowContext::CreateVulkan() {
#else
device_extensions.push_back(VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME);
device_extensions.push_back(VK_EXT_COLOR_WRITE_ENABLE_EXTENSION_NAME);
device_extensions.push_back(VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME);
#endif
#ifdef KYTY_ENABLE_DEBUG_PRINTF
@@ -1024,7 +1040,8 @@ void WindowContext::CreateVulkan() {
VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME,
VK_EXT_MESH_SHADER_EXTENSION_NAME,
VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME,
VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME}) {
VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME,
VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME}) {
if (HasExtension(available_extensions, extension)) {
device_extensions.push_back(extension);
}
@@ -466,6 +466,13 @@ void WindowContext::ProcessEvent(double time_s) {
}
return;
}
if ((event->type == SDL_EVENT_KEY_DOWN || event->type == SDL_EVENT_KEY_UP) &&
event->key.key == SDLK_F8) {
if (event->type == SDL_EVENT_KEY_DOWN && event->key.repeat == 0) {
Controller::CycleKeyboardPlayer();
}
return;
}
if (ProcessSystemOverlayInput(*event)) {
return;
}
@@ -67,6 +67,44 @@ uint32_t EmitExt(EmitterState& state, uint32_t type, uint32_t opcode,
return result;
}
uint32_t EmitF32ToF16RtzBits(EmitterState& state, uint32_t value) {
// Native half conversion causes regressions; use compact integer RTZ conversion.
const auto bits = EmitBitcastF32ToU32(state, value);
const auto abs = EmitAndConstant(state, bits, 0x7fffffffu);
const auto sign = EmitAndConstant(
state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), bits, ConstantU32(state, 16)),
0x8000u);
const auto exp = Binary(state, spv::OpShiftRightLogical, TypeU32(state), abs,
ConstantU32(state, 23));
const auto mant = EmitAndConstant(state, abs, 0x007fffffu);
// Align the hidden bit to the half subnormal mantissa. Even unused shifts must be < 32.
const auto shift = EmitUMin32(
state, Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 126), exp),
ConstantU32(state, 31));
const auto sub = Binary(state, spv::OpShiftRightLogical, TypeU32(state),
EmitOrU32(state, mant, ConstantU32(state, 0x00800000u)), shift);
// Drop 13 mantissa bits and rebias the exponent; finite overflow saturates to max half.
const auto truncated = Binary(state, spv::OpShiftRightLogical, TypeU32(state), abs,
ConstantU32(state, 13));
const auto normal = EmitUMin32(
state, Binary(state, spv::OpISub, TypeU32(state), truncated, ConstantU32(state, 0x1c000u)),
ConstantU32(state, 0x7bffu));
const auto finite = Select(state, TypeU32(state),
EmitCompareU32Constant(state, spv::OpULessThan, abs, 0x38800000u),
sub, normal);
const auto nan = EmitOrU32(state, EmitAndConstant(state, truncated, 0x03ffu),
ConstantU32(state, 0x7e00u));
const auto special = Select(state, TypeU32(state),
EmitCompareU32Constant(state, spv::OpIEqual, mant, 0),
ConstantU32(state, 0x7c00u), nan);
const auto magnitude = Select(
state, TypeU32(state),
EmitCompareU32Constant(state, spv::OpUGreaterThanEqual, abs, 0x7f800000u), special, finite);
return EmitOrU32(state, sign, magnitude);
}
uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
const auto trunc = EmitTruncF32Value(state, src);
const auto converted_raw = state.builder.AllocateId();
@@ -210,28 +248,18 @@ uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value
}
uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1) {
const auto pair = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 2), pair, arg0, arg1);
const auto packed = EmitPackHalf2x16(state, pair);
const auto back = EmitExt(state, TypeF32Vector(state, 2), GLSLstd450UnpackHalf2x16, {packed});
auto result = packed;
for (uint32_t i = 0; i < 2; i++) {
const auto rounded = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), rounded, back, i);
const auto above =
Binary(state, spv::OpFOrdGreaterThan, TypeBool(state),
EmitExt(state, TypeF32(state), GLSLstd450FAbs, {rounded}),
EmitExt(state, TypeF32(state), GLSLstd450FAbs, {i == 0 ? arg0 : arg1}));
result = Binary(state, spv::OpISub, TypeU32(state), result,
Select(state, TypeU32(state), above,
ConstantU32(state, i == 0 ? 1u : 0x10000u), ConstantU32(state, 0)));
}
return result;
const auto low = EmitF32ToF16RtzBits(state, arg0);
const auto high = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
EmitF32ToF16RtzBits(state, arg1), ConstantU32(state, 16));
return EmitOrU32(state, low, high);
}
uint32_t EmitFPSaturate32(EmitterState& state, uint32_t arg0) {
return EmitExt(state, TypeF32(state), GLSLstd450FClamp,
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
// The clamp modifier runs in DX10 clamp mode: NaN becomes 0. FClamp leaves NaN undefined.
const auto clamped = EmitExt(state, TypeF32(state), GLSLstd450FClamp,
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
return Select(state, TypeF32(state), EmitClassifyF32(state, arg0).nan, ConstantF32(state, 0),
clamped);
}
uint32_t EmitSMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1) {
@@ -296,6 +296,7 @@ inline constexpr auto EmitBufferAtomicSMax32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicUMax32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicAnd32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicAnd64 = EmitBufferAtomic64;
inline constexpr auto EmitBufferAtomicIAdd64 = EmitBufferAtomic64;
inline constexpr auto EmitBufferAtomicOr32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicOr64 = EmitBufferAtomic64;
inline constexpr auto EmitBufferAtomicXor32 = EmitAtomic32;
@@ -325,6 +326,7 @@ inline constexpr auto EmitSharedAtomicSMin32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicUMin32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicSMax32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicUMax64 = EmitSharedAtomic64;
inline constexpr auto EmitSharedAtomicCmpSwap32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicCmpSwap64 = EmitSharedAtomic64;
inline constexpr auto EmitSharedAtomicUMax32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicAnd32 = EmitAtomic32;
@@ -420,6 +420,7 @@ void EnsureLdsStorage(EmitterState& state);
struct MemoryResourceAccess {
IR::ResourceKind kind = IR::ResourceKind::None;
uint32_t object_pointer = 0;
uint32_t descriptor_index = 0;
uint32_t length = 0;
uint32_t byte_offset = 0;
uint32_t element_bits = 32;
@@ -1075,6 +1075,63 @@ uint32_t LoadIndirectFormattedX(ValueEmitContext& ctx, const IR::Inst& inst) {
});
}
uint32_t LoadRawBuffer(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
const MemoryResourceAccess& resource, uint32_t components) {
auto& state = ctx.state;
const auto scalar = [&] {
std::array<uint32_t, 4> values {};
for (uint32_t component = 0; component < components; component++) {
values[component] =
LoadWordPrepared(ctx, inst, RebaseRawComponent(mem, component), resource);
}
return ConstructU32Composite(state, components, values);
};
if (!mem.IsCoherentBufferPairLoad(inst.GetOpcode())) return scalar();
const auto first = ByteAddress(ctx, inst, mem, resource.byte_offset);
const auto second = ByteAddress(ctx, inst, RebaseRawComponent(mem, 1), resource.byte_offset);
const auto wide =
PrepareStorageBufferResourceAccess(state, mem, state.storage_buffers[3],
resource.non_uniform ? resource.descriptor_index : 0u);
const auto index =
Binary(state, spv::OpShiftRightLogical, TypeU32(state), first, ConstantU32(state, 3));
auto atomic =
Binary(state, spv::OpIEqual, TypeBool(state),
Binary(state, spv::OpBitwiseAnd, TypeU32(state), first, ConstantU32(state, 7)),
ConstantU32(state, 0));
atomic = AndCondition(state, atomic,
Binary(state, spv::OpIEqual, TypeBool(state), second,
EmitAddU32(state, first, ConstantU32(state, 4))));
// The 64-bit alias requires the entire pair; partial and swizzled reads retain DWORD bounds.
atomic = AndCondition(state, atomic, EmitMemoryElementInBounds(state, wide, index));
const auto atomic_label = state.builder.AllocateId(), scalar_label = state.builder.AllocateId();
const auto merge = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone);
state.builder.AddFunction(spv::OpBranchConditional, atomic, atomic_label, scalar_label);
EmitLabel(state, atomic_label);
const auto pointer = EmitStorageBufferElementPointer(
state, wide, index, TypeStorageBufferElementPointer(state, 64));
const auto snapshot = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAtomicLoad, TypeU64(state), snapshot, pointer,
ConstantU32(state, spv::ScopeDevice),
ConstantU32(state, spv::MemorySemanticsAcquireMask |
spv::MemorySemanticsUniformMemoryMask));
const std::array<uint32_t, 4> words {
EmitCompositeExtractU64(state, snapshot, IR::Value(0u)),
EmitCompositeExtractU64(state, snapshot, IR::Value(1u))};
const auto pair = ConstructU32Composite(state, 2, words);
state.builder.AddFunction(spv::OpBranch, merge);
EmitLabel(state, scalar_label);
const auto fallback = scalar();
const auto scalar_exit = state.current_label;
state.builder.AddFunction(spv::OpBranch, merge);
EmitLabel(state, merge);
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpPhi, TypeU32Composite(state, 2), result, pair, atomic_label,
fallback, scalar_exit);
return result;
}
uint32_t LoadBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t components) {
auto& state = ctx.state;
const auto mem = ctx.Memory(inst);
@@ -1100,12 +1157,7 @@ uint32_t LoadBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t compon
return LoadFormattedPrepared(ctx, inst, selected, resource, info, components);
}
if (resource.object_pointer == 0u) return ConstantU32CompositeZero(state, components);
std::array<uint32_t, 4> values {};
for (uint32_t component = 0; component < components; component++) {
values[component] =
LoadWordPrepared(ctx, inst, RebaseRawComponent(selected, component), resource);
}
return ConstructU32Composite(state, components, values);
return LoadRawBuffer(ctx, inst, selected, resource, components);
});
});
}
@@ -207,10 +207,11 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
const bool non_uniform = array_index != 0u;
const auto static_index = state.program.info.buffers.at(mem.resource).descriptor_index;
if (!non_uniform) array_index = ConstantU32(state, static_index);
MemoryResourceAccess access {
.kind = mem.kind,
.non_uniform = non_uniform,
.memory_access = mem.coherent ? spv::MemoryAccessVolatileMask : spv::MemoryAccessMaskNone};
MemoryResourceAccess access {.kind = mem.kind,
.descriptor_index = array_index,
.non_uniform = non_uniform,
.memory_access = mem.coherent ? spv::MemoryAccessVolatileMask
: spv::MemoryAccessMaskNone};
access.object_pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain, buffer.pointer_type, access.object_pointer,
buffer.variable, array_index);
@@ -504,6 +505,7 @@ uint32_t EncodePackedColorComponent(EmitterState& state, const Format::BufferFor
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
switch (opcode) {
case IR::ValueOpcode::SharedAtomicCmpSwap32:
case IR::ValueOpcode::SharedAtomicCmpSwap64:
case IR::ValueOpcode::ImageAtomicCompareSwap32:
case IR::ValueOpcode::BufferAtomicCmpSwap32: return spv::OpAtomicCompareExchange;
@@ -516,6 +518,7 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
case IR::ValueOpcode::ImageAtomicIAdd64:
case IR::ValueOpcode::BufferAtomicIAdd32:
case IR::ValueOpcode::SharedAtomicIAdd64:
case IR::ValueOpcode::BufferAtomicIAdd64:
case IR::ValueOpcode::SharedAtomicIAdd32: return spv::OpAtomicIAdd;
case IR::ValueOpcode::BufferAtomicISub32:
case IR::ValueOpcode::SharedAtomicISub32: return spv::OpAtomicISub;
@@ -560,8 +563,12 @@ uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32
const bool wide = inst.GetType() == IR::Type::U64;
const auto type = wide ? TypeU64(ctx.state) : TypeU32(ctx.state);
if (opcode == spv::OpAtomicCompareExchange) {
const auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
const auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
if (inst.GetOpcode() == IR::ValueOpcode::SharedAtomicCmpSwap32) {
// RDNA2 DS compares DATA and stores DATA2, opposite to BUFFER atomics.
std::swap(desired, comparator);
}
ctx.state.builder.AddFunction(
spv::OpAtomicCompareExchange, TypeU32(ctx.state), old, pointer,
ConstantU32(ctx.state, scope), ConstantU32(ctx.state, spv::MemorySemanticsMaskNone),
@@ -67,6 +67,7 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
{0x3fu, Opcode::BUFFER_ATOMIC_FMIN, 1, 32},
{0x40u, Opcode::BUFFER_ATOMIC_FMAX, 1, 32},
{0x50u, Opcode::BUFFER_ATOMIC_SWAP_X2, 2, 32},
{0x52u, Opcode::BUFFER_ATOMIC_ADD_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},
@@ -114,6 +115,7 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
{0x27u, Opcode::DS_MIN_RTN_U32, 1, 32}, {0x28u, Opcode::DS_MAX_RTN_U32, 1, 32},
{0x29u, Opcode::DS_AND_RTN_B32, 1, 32}, {0x2au, Opcode::DS_OR_RTN_B32, 1, 32},
{0x2bu, Opcode::DS_XOR_RTN_B32, 1, 32}, {0x2du, Opcode::DS_WRXCHG_RTN_B32, 1, 32},
{0x30u, Opcode::DS_CMPST_RTN_B32, 1, 32},
{0x35u, Opcode::DS_SWIZZLE_B32, 1, 32}, {0x36u, Opcode::DS_READ_B32, 1, 32},
{0x37u, Opcode::DS_READ2_B32, 2, 32}, {0x38u, Opcode::DS_READ2ST64_B32, 2, 32},
{0x39u, Opcode::DS_READ_I8, 1, 8, true}, {0x3au, Opcode::DS_READ_U8, 1, 8},
@@ -200,6 +202,7 @@ bool IsDsAtomicOpcode(Opcode opcode) {
case Opcode::DS_MAX_U32:
case Opcode::DS_MAX_RTN_U32:
case Opcode::DS_MAX_RTN_U64:
case Opcode::DS_CMPST_RTN_B32:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_AND_B32:
case Opcode::DS_AND_RTN_B32:
@@ -220,6 +223,7 @@ uint32_t DsSourceCount(Opcode opcode) {
case Opcode::DS_WRITE2ST64_B32:
case Opcode::DS_WRITE2_B64:
case Opcode::DS_WRITE2ST64_B64:
case Opcode::DS_CMPST_RTN_B32:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_MSKOR_B32: return 3u;
case Opcode::DS_MIN_F32:
@@ -454,6 +458,9 @@ void DecodeDs(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
if (inst.opcode == Opcode::DS_SWIZZLE_B32 && inst.offset >= 0xe000u) {
SetUnsupported(inst, Family::DS, opcode, "DS swizzle FFT mode is not implemented");
}
if (inst.gds && inst.opcode == Opcode::DS_CMPST_RTN_B32) {
SetUnsupported(inst, Family::DS, opcode, "DS compare-store return B32 GDS variant is not implemented");
}
if (inst.gds &&
(inst.opcode == Opcode::DS_SWIZZLE_B32 || inst.opcode == Opcode::DS_PERMUTE_B32 ||
inst.opcode == Opcode::DS_BPERMUTE_B32 ||
@@ -670,6 +670,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::BUFFER_ATOMIC_CMPSWAP:
case Opcode::BUFFER_ATOMIC_SWAP_X2:
case Opcode::BUFFER_ATOMIC_ADD:
case Opcode::BUFFER_ATOMIC_ADD_X2:
case Opcode::BUFFER_ATOMIC_SUB:
case Opcode::BUFFER_ATOMIC_SMIN:
case Opcode::BUFFER_ATOMIC_UMIN:
@@ -717,6 +718,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::DS_MAX_U32:
case Opcode::DS_MAX_RTN_U32:
case Opcode::DS_MAX_RTN_U64:
case Opcode::DS_CMPST_RTN_B32:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_AND_B32:
case Opcode::DS_AND_RTN_B32:
@@ -586,6 +586,7 @@ enum class Opcode {
BUFFER_ATOMIC_CMPSWAP,
BUFFER_ATOMIC_SWAP_X2,
BUFFER_ATOMIC_ADD,
BUFFER_ATOMIC_ADD_X2,
BUFFER_ATOMIC_SUB,
BUFFER_ATOMIC_SMIN,
BUFFER_ATOMIC_UMIN,
@@ -631,6 +632,7 @@ enum class Opcode {
DS_MAX_U32,
DS_MAX_RTN_U32,
DS_MAX_RTN_U64,
DS_CMPST_RTN_B32,
DS_CMPST_RTN_B64,
DS_AND_B32,
DS_AND_RTN_B32,
@@ -27,7 +27,7 @@ void Translator::PackedFloat16(const Decoder::Instruction& inst, IR::ValueOpcode
if (quiet_snan) {
const auto quiet_snan_lane = [&](const Decoder::Operand& operand, bool high) {
const auto bits = ReadU16LaneAsU32(operand, high, false);
const auto exponent = ir.BitwiseAnd(bits, IR::U32(IR::Value(0x7c00u)));
const auto exponent = ir.BitwiseAnd(bits, IR::U32(IR::Value(0x7e00u)));
const auto payload = ir.BitwiseAnd(bits, IR::U32(IR::Value(0x01ffu)));
const auto snan = ir.LogicalAnd(ir.IEqual(exponent, IR::U32(IR::Value(0x7c00u))),
ir.INotEqual(payload, IR::U32(IR::Value(0u))));
@@ -1046,6 +1046,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicFMin32);
case Decoder::Opcode::BUFFER_ATOMIC_FMAX:
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicFMax32);
case Decoder::Opcode::BUFFER_ATOMIC_ADD_X2:
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicIAdd64);
case Decoder::Opcode::DS_ADD_U32:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicIAdd32, false);
@@ -1083,6 +1085,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax32, true);
case Decoder::Opcode::DS_MAX_RTN_U64:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax64, true);
case Decoder::Opcode::DS_CMPST_RTN_B32:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicCmpSwap32, true);
case Decoder::Opcode::DS_CMPST_RTN_B64:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicCmpSwap64, true);
case Decoder::Opcode::DS_AND_B32:
@@ -233,13 +233,6 @@ IR::Value Translator::ReadOperand(const Decoder::Operand& operand, IR::Type type
if (type == IR::Type::F64) {
if (operand.kind == Decoder::OperandKind::LiteralConstant) {
pair = {IR::U32(IR::Value(0u)), IR::U32(IR::Value(operand.value))};
} else if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
const auto bits = operand.value == 0x3e22f983u
? 0x3fc45f306dc9c882ull
: std::bit_cast<uint64_t>(static_cast<double>(
std::bit_cast<float>(operand.value)));
pair = {IR::U32(IR::Value(static_cast<uint32_t>(bits))),
IR::U32(IR::Value(static_cast<uint32_t>(bits >> 32u)))};
}
if (operand.absolute) {
pair[1] = ir.BitwiseAnd(pair[1], IR::U32(IR::Value(0x7fffffffu)));
@@ -488,6 +481,14 @@ std::array<IR::U32, 2> Translator::ReadU32Pair(const Decoder::Operand& operand)
if (operand.kind == Decoder::OperandKind::VccLo) {
return {ir.GetVccLo(), ir.GetVccHi()};
}
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
const auto bits =
operand.value == 0x3e22f983u
? 0x3fc45f306dc9c882ull
: std::bit_cast<uint64_t>(static_cast<double>(std::bit_cast<float>(operand.value)));
return {IR::U32(IR::Value(static_cast<uint32_t>(bits))),
IR::U32(IR::Value(static_cast<uint32_t>(bits >> 32u)))};
}
const auto low = ApplyBitSourceModifiers(operand, ReadRawU32(operand));
IR::U32 high(IR::Value(0u));
if (operand.kind == Decoder::OperandKind::Sgpr || operand.kind == Decoder::OperandKind::Vgpr) {
@@ -546,9 +547,11 @@ IR::U32 Translator::Read16LaneBits(const Decoder::Operand& operand, bool high_la
auto bits = ReadRawU32(operand);
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
// Inline floats encode the operand's width before SDWA or OP_SEL selects bits.
const auto half = IR::F16(ir.Emit(IR::ValueOpcode::ConvertF16F32, {ir.BitCastF32(bits)}));
const auto half_bits = IR::U16(ir.Emit(IR::ValueOpcode::BitCastU16F16, {half}));
bits = IR::U32(ir.Emit(IR::ValueOpcode::ConvertU32U16, {half_bits}));
const auto value = operand.value;
bits = IR::U32(IR::Value(value == 0x3e22f983u
? 0x3118u
: ((value >> 16u) & 0x8000u) |
((((value >> 23u) & 0xffu) - 112u) << 10u)));
}
bits = ApplyBitSourceModifiers(raw_operand, bits);
uint32_t offset = (high_lane ? operand.op_sel_hi : operand.op_sel) ? 16u : 0u;
@@ -25,6 +25,7 @@ struct UniformFill {
std::array<uint32_t, 3> group_stride {};
uint32_t words = 0;
uint32_t value = 0;
uint32_t bounded_elements = 0;
bool operator==(const UniformFill&) const = default;
};
@@ -82,6 +82,11 @@ struct MemoryInfo {
bool coherent = false;
bool planning_only = false;
[[nodiscard]] bool IsCoherentBufferPairLoad(ValueOpcode opcode) const {
return kind == ResourceKind::Buffer && opcode == ValueOpcode::LoadBufferU32x2 && coherent &&
!typed && !formatted && data_bits == 32u;
}
[[nodiscard]] bool SupportsIndirectBufferLoad(ValueOpcode opcode) const {
if (typed) return false;
if (data_bits == 8u) return !formatted && opcode == ValueOpcode::LoadBufferU8;
@@ -622,6 +627,8 @@ struct CompiledShaderInfo {
struct UniformFillPlan {
UniformFill fill;
std::array<Value, 4> values;
Value pattern_size;
Value element_count;
};
// Resource analysis retained by the shader cache. It owns immutable descriptor/SRT,
@@ -118,6 +118,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
case ValueOpcode::BufferAtomicCmpSwap32:
case ValueOpcode::BufferAtomicSwap64:
case ValueOpcode::BufferAtomicIAdd32:
case ValueOpcode::BufferAtomicIAdd64:
case ValueOpcode::BufferAtomicISub32:
case ValueOpcode::BufferAtomicSMin32:
case ValueOpcode::BufferAtomicUMin32:
@@ -137,6 +138,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
uint32_t BufferComponentCount(ValueOpcode opcode) {
switch (opcode) {
case ValueOpcode::BufferAtomicSwap64:
case ValueOpcode::BufferAtomicIAdd64:
case ValueOpcode::BufferAtomicAnd64:
case ValueOpcode::BufferAtomicOr64:
case ValueOpcode::LoadBufferU32x2:
@@ -176,6 +178,7 @@ SharedAccess SharedAccessOf(ValueOpcode opcode) {
case ValueOpcode::SharedAtomicSMax32:
case ValueOpcode::SharedAtomicUMax32:
case ValueOpcode::SharedAtomicUMax64:
case ValueOpcode::SharedAtomicCmpSwap32:
case ValueOpcode::SharedAtomicCmpSwap64:
case ValueOpcode::SharedAtomicAnd32:
case ValueOpcode::SharedAtomicOr32:
@@ -268,6 +268,7 @@ VALUE_OPCODE(BufferAtomicSwap32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicCmpSwap32, U32, BufferResource, U32, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicSwap64, U64, BufferResource, U32, U32, U32, U64, U1)
VALUE_OPCODE(BufferAtomicIAdd32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicIAdd64, U64, BufferResource, U32, U32, U32, U64, U1)
VALUE_OPCODE(BufferAtomicISub32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicSMin32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicUMin32, U32, BufferResource, U32, U32, U32, U32, U1)
@@ -306,6 +307,7 @@ VALUE_OPCODE(SharedAtomicUMin32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicSMax32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicUMax32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicUMax64, U64, U32, U64, U1)
VALUE_OPCODE(SharedAtomicCmpSwap32, U32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicCmpSwap64, U64, U32, U64, U64, U1)
VALUE_OPCODE(SharedAtomicAnd32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicOr32, U32, U32, U32, U1)
@@ -51,11 +51,13 @@ public:
}
void QueueUsers(const std::vector<Use>& users) {
++generation;
affected.clear();
for (const auto& use: users) affected.push_back(use.user);
}
void FlushUsers() {
++generation;
// Pattern folds inspect nested operands even when intermediate operations
// stay dynamic. Visit consumers once per change, including cyclic Phi edges.
// stay dynamic. Visit their union once per drain, including cyclic Phi edges.
for (size_t i = 0; i < affected.size(); ++i) {
auto* inst = affected[i];
const auto found = locations.find(inst);
@@ -64,6 +66,7 @@ public:
Queue(inst);
for (const auto& use: inst->Uses()) affected.push_back(use.user);
}
affected.clear();
}
Block::iterator Prepend(Block& block, Block::iterator before, ValueOpcode opcode,
@@ -977,27 +980,30 @@ void ConstantPropagationPass(const BlockList& blocks, uint32_t wave_size) {
std::vector<Use> users;
std::vector<Value> operands;
const auto drain = [&] {
for (size_t i = 0; i < worklist.pending.size(); ++i) {
auto* inst = worklist.pending[i];
auto& location = worklist.locations.at(inst);
location.queued = false;
const auto opcode = inst->GetOpcode();
if (opcode == ValueOpcode::Identity || opcode == ValueOpcode::Void) continue;
const auto where = location.instruction;
users.assign(inst->Uses().begin(), inst->Uses().end());
operands.clear();
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) operands.push_back(inst->Arg(arg));
mask_projection.Fold(*inst, wave_size);
FoldInstruction(*inst->Parent(), where, worklist);
bool changed = opcode != inst->GetOpcode() || operands.size() != inst->NumArgs();
for (size_t arg = 0; !changed && arg < operands.size(); ++arg)
changed = operands[arg] != inst->Arg(arg);
if (changed) {
worklist.QueueUsers(users);
worklist.Queue(inst);
do {
for (size_t i = 0; i < worklist.pending.size(); ++i) {
auto* inst = worklist.pending[i];
auto& location = worklist.locations.at(inst);
location.queued = false;
const auto opcode = inst->GetOpcode();
if (opcode == ValueOpcode::Identity || opcode == ValueOpcode::Void) continue;
const auto where = location.instruction;
users.assign(inst->Uses().begin(), inst->Uses().end());
operands.clear();
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) operands.push_back(inst->Arg(arg));
mask_projection.Fold(*inst, wave_size);
FoldInstruction(*inst->Parent(), where, worklist);
bool changed = opcode != inst->GetOpcode() || operands.size() != inst->NumArgs();
for (size_t arg = 0; !changed && arg < operands.size(); ++arg)
changed = operands[arg] != inst->Arg(arg);
if (changed) {
worklist.QueueUsers(users);
worklist.Queue(inst);
}
}
}
worklist.pending.clear();
worklist.pending.clear();
worklist.FlushUsers();
} while (!worklist.pending.empty());
};
drain();
// Normalize retained PHI/select values only after every supported field read has
@@ -1409,7 +1409,8 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
}
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr &&
op != ValueOpcode::LogicalNot && op != ValueOpcode::IEqual32 &&
op != ValueOpcode::INotEqual32 && op != ValueOpcode::ULessThanEqual32 &&
op != ValueOpcode::INotEqual32 && op != ValueOpcode::ULessThan32 &&
op != ValueOpcode::ULessThanEqual32 &&
op != ValueOpcode::SelectU1 && op != ValueOpcode::SelectU32) return finish(opaque());
auto& node = plan.value_storage.emplace_back(op);
for (size_t index = 0; index < inst->NumArgs(); ++index)
@@ -1424,10 +1425,11 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
for (auto it = first; it != plan.value_storage.end(); ++it) it->Invalidate();
plan.value_storage.erase(first, plan.value_storage.end());
}
plan.uniform_fill = AnalyzeUniformFill(program);
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
}
plan.uniform_fill = program.uniform_fill.fill.kind != UniformFillKind::None
? program.uniform_fill : AnalyzeUniformFill(program);
for (auto& value: plan.uniform_fill.values) value = Clone(value);
plan.uniform_fill.pattern_size = Clone(plan.uniform_fill.pattern_size);
plan.uniform_fill.element_count = Clone(plan.uniform_fill.element_count);
plan.capture_specialization_reads |=
capture_indirect_reads || capture_control_reads || !plan.source_reads.empty();
if (plan.capture_specialization_reads) {
@@ -1513,15 +1515,21 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
}
snapshot.uniform_fill = {};
const auto& fill = program.uniform_fill;
const auto words = fill.fill.words;
auto words = fill.fill.words;
std::array<uint32_t, 4> stored {};
bool uniform_fill = words != 0;
if (uniform_fill && !fill.pattern_size.IsEmpty())
uniform_fill = clean.Evaluate(fill.pattern_size, words) && words >= 1 && words <= 4;
uint32_t elements = 0;
if (uniform_fill && !fill.element_count.IsEmpty())
uniform_fill = clean.Evaluate(fill.element_count, elements) && elements != 0;
for (uint32_t i = 0; i < words && uniform_fill; ++i) {
uniform_fill = clean.Evaluate(fill.values[i], stored[i]) && stored[i] == stored[0];
}
if (uniform_fill) {
snapshot.uniform_fill = fill.fill;
snapshot.uniform_fill.value = stored[0];
snapshot.uniform_fill.bounded_elements = elements;
}
const auto evaluate = [&](uint32_t source, DescriptorValue& value, bool written = false) {
if (source >= program.descriptor_sources.size()) {
@@ -15,6 +15,8 @@
#include <tuple>
#include <unordered_map>
#include <utility>
#define XXH_INLINE_ALL
#include <xxhash.h>
namespace Libs::Graphics::ShaderRecompiler::IR {
namespace {
@@ -4040,11 +4042,71 @@ private:
bool m_shader_writes = false;
};
UniformFillPlan RecognizePatternFill(Program& program, const Decoder::Program& decoded,
ShaderStageInputInfo input) {
struct Pattern {
size_t code_words;
uint64_t hash;
uint32_t wave_size_mask;
bool bounded;
};
constexpr std::array patterns {
// Bounded 64-lane variant.
Pattern {.code_words = 69, .hash = 0x0704b9818e3cba06ull, .wave_size_mask = 64, .bounded = true},
// Dispatch thread dimensions, 64-lane variants.
Pattern {.code_words = 42, .hash = 0x92ce57f21310ef02ull, .wave_size_mask = 64, .bounded = false},
Pattern {.code_words = 40, .hash = 0x374161027f58bee7ull, .wave_size_mask = 64, .bounded = false},
Pattern {.code_words = 40, .hash = 0x7d7172c56c2e296dull, .wave_size_mask = 64, .bounded = false},
// Dispatch thread dimensions, valid in both wave sizes.
Pattern {.code_words = 38, .hash = 0xacbda1a1ccdd4198ull, .wave_size_mask = 32 | 64, .bounded = false},
};
if (program.stage != ShaderType::Compute || input.compute == nullptr ||
decoded.instructions.empty() || program.user_data_base != 0 || program.user_data_count < 10 ||
program.info.buffers.size() != 1 || !program.info.images.empty()) return {};
const auto& cs = *input.compute;
const bool matching_inputs = cs.workgroup_register == 10 && cs.thread_ids_num == 1 &&
cs.group_id[0] && !cs.group_id[1] && !cs.group_id[2] && cs.threads_num[0] == 64 &&
cs.threads_num[1] == 1 && cs.threads_num[2] == 1 && cs.float_mode == 0xc0;
if (!matching_inputs) return {};
const auto& last = decoded.instructions.back();
const size_t words = last.pc / sizeof(uint32_t) + last.word_count;
if (words > decoded.code.size() ||
std::ranges::find(patterns, words, &Pattern::code_words) == patterns.end()) return {};
// Hash executable instructions through S_ENDPGM, excluding binary metadata.
// Verified variants implement Destination[id] = value[id % pattern_size].
const auto hash = XXH3_64bits(decoded.code.data(), words * sizeof(uint32_t));
const auto pattern = std::ranges::find_if(patterns, [&](const Pattern& candidate) {
return candidate.code_words == words && candidate.hash == hash;
});
if (pattern == patterns.end() || (program.wave_size & pattern->wave_size_mask) == 0 ||
cs.dispatch_thread_dimensions != !pattern->bounded) return {};
std::array<Value, 10> userdata;
for (auto* block: program.blocks) {
for (auto& inst: *block) {
if (inst.GetOpcode() != ValueOpcode::GetUserData) continue;
const auto reg = RegIndex(inst.Arg(0).ScalarRegister());
if (reg < userdata.size()) userdata[reg] = Value(&inst);
}
}
for (uint32_t reg = 4; reg < 10; ++reg)
if ((reg != 8 || pattern->bounded) && userdata[reg].IsEmpty()) return {};
UniformFillPlan result;
result.fill.kind = UniformFillKind::Buffer;
result.fill.words = 1;
result.fill.group_stride[0] = 64;
std::copy_n(userdata.begin() + 4, 4, result.values.begin());
result.pattern_size = userdata[9];
// Only the bounded variant reads an explicit count; the other uses thread dimensions.
if (pattern->bounded) result.element_count = userdata[8];
return result;
}
} // namespace
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg,
ShaderStageInputInfo input) {
Tracker(program, decoded, native_cfg, input).Run();
program.uniform_fill = RecognizePatternFill(program, decoded, input);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -340,6 +340,7 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
}
if (kind == ResourceKind::Buffer) {
info.coherent_buffers |= memory.coherent;
info.buffer_int64_atomics |= memory.IsCoherentBufferPairLoad(op);
if (memory.resource >= info.buffers.size()) {
return Fail("buffer operation has invalid resource metadata");
}
@@ -1054,7 +1054,13 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
}
return false;
case ValueOpcode::ULessThan32:
if (binary()) {
// No U32 is below zero, including an unknown loop-carried value.
if (!Arg(inst, 1, b)) return false;
if (static_cast<uint32_t>(b) == 0u) {
result = 0u;
return true;
}
if (Arg(inst, 0, a)) {
result = static_cast<uint32_t>(a) < static_cast<uint32_t>(b);
return true;
}
+5 -5
View File
@@ -583,16 +583,16 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
mesh.input_primitive, sh.m_vgtGsOutPrimType, sh.m_vgtGsMaxVertOut,
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),
native_ngg ? mesh.max_primitives :
native_ngg ? mesh.InputPrimitiveCount(mesh.max_vertices) :
mesh.max_vertices / sh.m_vgtGsMaxVertOut});
EXIT_IF(mesh.primitives_per_group == 0u);
mesh.vertices_per_group = mesh.InputVertexCount(mesh.primitives_per_group);
const auto threads = native_ngg && !mesh.fast_launch ? mesh.vertices_per_group : mesh.max_vertices;
mesh.max_primitives = mesh.primitives_per_group * (sh.m_geNggSubgrpCntl & 0x1ffu);
const auto threads = std::max(native_ngg && !mesh.fast_launch ? mesh.vertices_per_group
: mesh.max_vertices,
mesh.max_primitives);
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;
+8 -2
View File
@@ -338,14 +338,20 @@ static bool HasWindowsForbiddenFilenameCharacter(const std::string& relative_pat
std::filesystem::path MountPoints::ResolvePath(const std::string& mounted_name) {
Common::LockGuard lock(m_mutex);
// Guests join mount points and names with extra slashes (e.g. "/addcont0//file").
std::string normalized = Common::FixFilenameSlash(mounted_name);
normalized.erase(std::unique(normalized.begin(), normalized.end(),
[](char a, char b) { return a == '/' && b == '/'; }),
normalized.end());
// Match the entire guest path so a mount root works with or without a trailing slash.
const auto mounted_path = Common::FixDirectorySlash(mounted_name);
const auto mounted_path = Common::FixDirectorySlash(normalized);
const auto it = std::find_if(
m_mount_pairs.begin(), m_mount_pairs.end(),
[&mounted_path](const MountPair& p) { return mounted_path.starts_with(p.point); });
if (it != m_mount_pairs.end()) {
const auto& p = *it;
auto rel_path = Common::RemoveFirst(Common::FixFilenameSlash(mounted_name), p.point.size());
auto rel_path = Common::RemoveFirst(normalized, p.point.size());
while (rel_path.starts_with('/')) {
rel_path = Common::RemoveFirst(rel_path, 1);
}
+24
View File
@@ -0,0 +1,24 @@
#ifndef EMULATOR_SRC_KERNEL_MODULE_H_
#define EMULATOR_SRC_KERNEL_MODULE_H_
#include "common/abi.h"
#include <cstddef>
#include <cstdint>
namespace Libs::LibKernel {
using KernelModule = int32_t;
struct KernelLoadModuleOpt;
struct KernelUnloadModuleOpt;
KernelModule KYTY_SYSV_ABI KernelLoadStartModule(const char* module_file_name, size_t args,
const void* argp, uint32_t flags,
const KernelLoadModuleOpt* opt, int* res);
int KYTY_SYSV_ABI KernelStopUnloadModule(KernelModule handle, size_t args, const void* argp,
uint32_t flags, const KernelUnloadModuleOpt* opt,
int* res);
} // namespace Libs::LibKernel
#endif // EMULATOR_SRC_KERNEL_MODULE_H_
@@ -66,7 +66,6 @@
<item row="1" column="1">
<widget class="QSpinBox" name="spinBox_user_id">
<property name="toolTip"><string>Local user ID exposed to games</string></property>
<property name="maximum"><number>2147483647</number></property>
</widget>
</item>
<item row="2" column="0">
@@ -347,6 +346,16 @@
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_keyboard_player">
<property name="text"><string>Keyboard player:</string></property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="comboBox_keyboard_player">
<property name="toolTip"><string>Gamepads fill the remaining players in connection order. Every player uses the same input mapping.</string></property>
</widget>
</item>
</layout>
</widget>
</item>
+5
View File
@@ -48,11 +48,14 @@ struct ControllerSettings {
QString color;
int speaker_volume = 50;
int vibration_intensity = 100;
// 0 shares player 1 with the first gamepad; 1..MAX_PLAYERS dedicates that player to the keyboard.
int keyboard_player = 0;
void WriteSettings(QSettings* s) const {
s->setValue("controller_color", color);
s->setValue("controller_speaker_volume", speaker_volume);
s->setValue("controller_vibration_intensity", vibration_intensity);
s->setValue("controller_keyboard_player", keyboard_player);
}
void ReadSettings(QSettings* s) {
@@ -65,6 +68,8 @@ struct ControllerSettings {
};
speaker_volume = read_percent("controller_speaker_volume", 50);
vibration_intensity = read_percent("controller_vibration_intensity", 100);
keyboard_player = qBound(0, s->value("controller_keyboard_player", 0).toInt(),
Config::MAX_PLAYERS);
}
};
@@ -209,10 +209,19 @@ static void ListInit(QComboBox* combo, T value) {
void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->lineEdit_user_name->setMaxLength(static_cast<int>(Config::MAX_USER_NAME_LENGTH));
m_ui->lineEdit_user_name->setText(info.user_name);
m_ui->spinBox_user_id->setMaximum(Config::MAX_USER_ID);
m_ui->spinBox_user_id->setValue(info.user_id);
UpdateControllerColorButton(m_ui->button_controller_color, info.controller.color);
m_ui->slider_controller_vibration->setValue(info.controller.vibration_intensity);
m_ui->slider_controller_volume->setValue(info.controller.speaker_volume);
auto* keyboard_player = m_ui->comboBox_keyboard_player;
keyboard_player->clear();
keyboard_player->addItem(tr("Shared with player 1 (default)"), 0);
for (int player = 1; player <= Config::MAX_PLAYERS; player++) {
keyboard_player->addItem(tr("Player %1").arg(player), player);
}
keyboard_player->setCurrentIndex(
qMax(0, keyboard_player->findData(info.controller.keyboard_player)));
auto* microphone = m_ui->comboBox_audio_input_device;
microphone->clear();
microphone->addItem(tr("None"), QString {});
@@ -458,6 +467,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
info.controller.color = ui.button_controller_color->property("controllerColor").toString();
info.controller.vibration_intensity = ui.slider_controller_vibration->value();
info.controller.speaker_volume = ui.slider_controller_volume->value();
info.controller.keyboard_player = ui.comboBox_keyboard_player->currentData().toInt();
}
info.screen_resolution =
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
+1
View File
@@ -246,6 +246,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
}
args << "--controller-volume" << QString::number(info.controller.speaker_volume);
args << "--controller-vibration" << QString::number(info.controller.vibration_intensity);
args << "--keyboard-player" << QString::number(info.controller.keyboard_player);
args << "--present-mode" << EnumToText(info.present_mode);
if (info.gpu_index >= 0) {
args << "--gpu" << QString::number(info.gpu_index);
+245 -124
View File
@@ -18,6 +18,7 @@
#include <atomic>
#include <cmath>
#include <cstring>
#include <memory>
#include <vector>
namespace Libs::Controller {
@@ -107,18 +108,19 @@ struct ControllerState {
class GameController {
public:
GameController() = default;
explicit GameController(int player): m_player(player) {}
KYTY_CLASS_NO_COPY(GameController);
[[nodiscard]] int Handle() const { return PAD_HANDLE + m_player; }
int InitPort();
int OpenPort(int user_id, int type, int index);
int GetPortHandle(int user_id, int type, int index);
int ClosePort(int handle);
int CheckPortHandle(int handle);
void Connect(int id);
void Disconnect(int id);
void SetDevices(int active_id);
void Button(int id, uint32_t button, bool down);
void Axis(int id, Axis axis, int value);
void RightStick(int id, int x, int y);
@@ -128,7 +130,6 @@ public:
void ResetOrientation();
void ToggleGyroEmulation(int id);
void ResetInputState();
void ReleaseHostPads();
void GetConnectionInfo(bool* flag, int* count);
void SetVibration(uint8_t large_motor, uint8_t small_motor);
int GetActiveControllerId();
@@ -145,8 +146,10 @@ private:
enum class PortState { Uninitialized, Closed, Open };
int CheckPortHandleLocked(int handle) const;
[[nodiscard]] bool PadOpenArgsAreValid(int user_id, int type, int index) const;
const int m_player;
void CheckActive();
void AddState();
// Emulated tilt: the right stick drives pitch and roll instead of the stick axes.
void UpdateEmulatedTilt();
@@ -157,7 +160,6 @@ private:
Common::Mutex m_mutex;
PortState m_port_state = PortState::Uninitialized;
std::vector<int> m_connected_ids;
int m_active_id = -1;
bool m_connected = false;
int m_connected_count = 0;
@@ -184,7 +186,39 @@ private:
std::array<std::atomic<uint32_t>, 3> m_setting_steps {};
};
static GameController* g_controller = nullptr;
static std::array<std::unique_ptr<GameController>, Config::MAX_PLAYERS> g_players;
// Maps host gamepads to players; the keyboard follows the configured keyboard player.
static Common::Mutex g_router_mutex;
static std::vector<std::pair<int, int>> g_pad_players;
static uint32_t g_keyboard_player = 0;
static void RefreshPlayers();
static void StopHostPad(SDL_Gamepad* pad);
static int KeyboardPlayer() {
return g_keyboard_player == 0 ? 0 : static_cast<int>(g_keyboard_player) - 1;
}
// Caller holds g_router_mutex.
static GameController* PlayerForDevice(int id) {
if (id == HOST_INPUT_CONTROLLER_ID) {
return g_players[KeyboardPlayer()].get();
}
for (const auto& [pad, player]: g_pad_players) {
if (pad == id) {
return player >= 0 ? g_players[player].get() : nullptr;
}
}
return nullptr;
}
static GameController* PlayerForHandle(int handle) {
return handle >= PAD_HANDLE && handle < PAD_HANDLE + Config::MAX_PLAYERS
? g_players[handle - PAD_HANDLE].get()
: nullptr;
}
static void pad_fill_data(PadData* data, const ControllerState& state, bool connected,
int connected_count) {
@@ -228,13 +262,15 @@ constexpr std::array SPEAKER_VOLUME = {1.0f, 0.0f, 0.02f, 0.12f, 0.42f};
constexpr std::array INTENSITY = {1.0f, 0.0f, 0.33f, 0.66f};
void CycleSetting(Setting setting) {
if (g_controller != nullptr) {
g_controller->CycleSetting(setting);
for (const auto& player: g_players) {
if (player != nullptr) {
player->CycleSetting(setting);
}
}
}
float GetSettingScale(Setting setting) {
return g_controller != nullptr ? g_controller->GetSettingScale(setting) : 1.0f;
return g_players[0] != nullptr ? g_players[0]->GetSettingScale(setting) : 1.0f;
}
void GameController::CycleSetting(Setting setting) {
@@ -404,35 +440,53 @@ static int32_t TriggerEffectState(const PadTriggerEffectCommand& command, int va
}
void Initialize() {
EXIT_IF(g_controller != nullptr);
EXIT_IF(g_players[0] != nullptr);
g_controller = new GameController;
g_controller->Connect(HOST_INPUT_CONTROLLER_ID);
for (int i = 0; i < Config::MAX_PLAYERS; i++) {
g_players[i] = std::make_unique<GameController>(i);
}
g_keyboard_player = Config::GetKeyboardPlayer();
RefreshPlayers();
}
void Shutdown() {
EmergencyShutdown();
delete g_controller;
g_controller = nullptr;
for (auto& player: g_players) {
player.reset();
}
}
void EmergencyShutdown() {
if (g_controller != nullptr) {
g_controller->ReleaseHostPads();
Common::LockGuard lock(g_router_mutex);
DualSenseHaptics::Shutdown();
std::vector<SDL_Gamepad*> pads;
for (const auto& [id, player]: g_pad_players) {
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id))) {
StopHostPad(pad);
pads.push_back(pad);
}
}
if (!pads.empty()) {
SDL_Delay(RELEASE_FLUSH_MS);
for (auto* pad: pads) {
SDL_CloseGamepad(pad);
}
}
g_pad_players.clear();
for (const auto& player: g_players) {
if (player != nullptr) {
player->SetDevices(-1);
}
}
}
void GameController::Connect(int id) {
void GameController::SetDevices(int active_id) {
Common::LockGuard lock(m_mutex);
if (std::find(m_connected_ids.begin(), m_connected_ids.end(), id) != m_connected_ids.end()) {
if (m_active_id == active_id) {
return;
}
m_connected_ids.push_back(id);
if (id != HOST_INPUT_CONTROLLER_ID) {
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id));
if (active_id != HOST_INPUT_CONTROLLER_ID && active_id != -1) {
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(active_id));
pad != nullptr) {
if (const auto& color = Config::GetControllerColor()) {
(void)SDL_SetGamepadLED(pad, (*color)[0], (*color)[1], (*color)[2]);
@@ -446,42 +500,11 @@ void GameController::Connect(int id) {
}
}
CheckActive();
}
void GameController::Disconnect(int id) {
Common::LockGuard lock(m_mutex);
const auto it = std::find(m_connected_ids.begin(), m_connected_ids.end(), id);
EXIT_IF(it == m_connected_ids.end());
m_connected_ids.erase(it);
CheckActive();
}
void GameController::CheckActive() {
int new_active_id = -1;
bool new_connected = false;
if (!m_connected_ids.empty()) {
new_active_id = m_connected_ids[0];
for (const auto id: m_connected_ids) {
if (id != HOST_INPUT_CONTROLLER_ID) {
new_active_id = id;
break;
}
}
new_connected = true;
}
if (m_connected == new_connected && m_active_id == new_active_id) {
return;
}
const bool new_connected = active_id != -1;
if (!m_connected && new_connected) {
m_connected_count++;
}
m_active_id = new_active_id;
m_active_id = active_id;
m_connected = new_connected;
m_state = {};
m_gyro_time = 0;
@@ -513,7 +536,7 @@ void GameController::AddState() {
void GameController::Button(int id, uint32_t button, bool down) {
Common::LockGuard lock(m_mutex);
// The keyboard shares the player-1 pad with the active gamepad.
// Keyboard events are routed to the configured player's port.
if (m_active_id == id || id == HOST_INPUT_CONTROLLER_ID) {
m_state.time = LibKernel::KernelGetProcessTime();
@@ -798,39 +821,16 @@ void GameController::ResetInputState() {
AddState();
}
void GameController::ReleaseHostPads() {
Common::LockGuard lock(m_mutex);
DualSenseHaptics::Shutdown();
std::vector<SDL_Gamepad*> pads;
for (const auto id: m_connected_ids) {
if (id == HOST_INPUT_CONTROLLER_ID) {
continue;
}
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id));
pad != nullptr) {
if (SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5) {
DualSenseEffects effect {};
effect.enable_bits = 0x0c;
effect.right_trigger[0] = 0x05;
effect.left_trigger[0] = 0x05;
(void)SDL_SendGamepadEffect(pad, &effect, sizeof(effect));
}
(void)SDL_RumbleGamepad(pad, 0, 0, 0);
(void)SDL_SetGamepadLED(pad, 0, 0, 0);
pads.push_back(pad);
}
static void StopHostPad(SDL_Gamepad* pad) {
if (SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5) {
DualSenseEffects effect {};
effect.enable_bits = 0x0c;
effect.right_trigger[0] = 0x05;
effect.left_trigger[0] = 0x05;
(void)SDL_SendGamepadEffect(pad, &effect, sizeof(effect));
}
if (!pads.empty()) {
SDL_Delay(RELEASE_FLUSH_MS);
for (auto* pad: pads) {
SDL_CloseGamepad(pad);
}
}
m_connected_ids.clear();
CheckActive();
(void)SDL_RumbleGamepad(pad, 0, 0, 0);
(void)SDL_SetGamepadLED(pad, 0, 0, 0);
}
void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) {
@@ -847,7 +847,9 @@ void GameController::ApplyVibration() {
const auto scale = duration != 0 ? GetSettingScale(Setting::VibrationIntensity) : 0.0f;
const auto large_motor = Scale(m_vibration[0], scale);
const auto small_motor = Scale(m_vibration[1], scale);
if (DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor, duration)) {
// The DualSense haptics backend drives a single pad, so only player 1 uses it.
if (m_player == 0 &&
DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor, duration)) {
return;
}
@@ -999,49 +1001,149 @@ int GameController::ReadStates(ControllerState* states, int states_num, bool* fl
return ret_num;
}
// Keep every opened pad, including those waiting for a free player slot.
// Caller holds g_router_mutex.
static void RefreshPlayers() {
std::array<bool, Config::MAX_PLAYERS> taken {};
const int keyboard = KeyboardPlayer();
if (g_keyboard_player != 0) {
taken[keyboard] = true;
}
for (auto& [id, player]: g_pad_players) {
if (g_keyboard_player != 0 && player == keyboard) {
player = -1;
}
if (player >= 0) {
taken[player] = true;
}
}
for (auto& [id, player]: g_pad_players) {
if (player >= 0) {
continue;
}
const auto free = std::find(taken.begin(), taken.end(), false);
if (free != taken.end()) {
player = static_cast<int>(free - taken.begin());
*free = true;
LOGF("Controller %d assigned to player %d\n", id, player + 1);
}
}
std::array<int, Config::MAX_PLAYERS> active;
active.fill(-1);
active[keyboard] = HOST_INPUT_CONTROLLER_ID;
for (const auto& [id, player]: g_pad_players) {
if (player >= 0) {
active[player] = id;
}
}
// Stop old owners before configuring new owners of the same physical pad.
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
const int previous = g_players[player]->GetActiveControllerId();
if (previous != active[player]) {
if (player == 0) {
(void)DualSenseHaptics::SetVibration(previous, 0, 0, 0);
}
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(previous))) {
StopHostPad(pad);
}
}
}
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
g_players[player]->SetDevices(active[player]);
}
}
void Connect(int id) {
g_controller->Connect(id);
Common::LockGuard lock(g_router_mutex);
if (std::any_of(g_pad_players.begin(), g_pad_players.end(),
[id](const auto& pad) { return pad.first == id; })) {
return;
}
g_pad_players.emplace_back(id, -1);
RefreshPlayers();
}
void Disconnect(int id) {
g_controller->Disconnect(id);
Common::LockGuard lock(g_router_mutex);
std::erase_if(g_pad_players, [id](const auto& pad) { return pad.first == id; });
RefreshPlayers();
}
void CycleKeyboardPlayer() {
Common::LockGuard lock(g_router_mutex);
g_keyboard_player = (g_keyboard_player + 1) % (Config::MAX_PLAYERS + 1);
RefreshPlayers();
if (g_keyboard_player == 0) {
LOGF("Keyboard shared with player 1\n");
} else {
LOGF("Keyboard assigned to player %u\n", g_keyboard_player);
}
}
void SetButton(int id, uint32_t button, bool down) {
g_controller->Button(id, button, down);
Common::LockGuard lock(g_router_mutex);
if (auto* player = PlayerForDevice(id)) {
player->Button(id, button, down);
}
}
void SetAxis(int id, Axis axis, int value) {
g_controller->Axis(id, axis, value);
Common::LockGuard lock(g_router_mutex);
if (auto* player = PlayerForDevice(id)) {
player->Axis(id, axis, value);
}
}
void SetRightStick(int id, int x, int y) {
g_controller->RightStick(id, x, y);
Common::LockGuard lock(g_router_mutex);
if (auto* player = PlayerForDevice(id)) {
player->RightStick(id, x, y);
}
}
void SetTouchPad(int id, int finger, bool down, float x, float y) {
g_controller->TouchPad(id, finger, down, x, y);
Common::LockGuard lock(g_router_mutex);
if (auto* player = PlayerForDevice(id)) {
player->TouchPad(id, finger, down, x, y);
}
}
void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us) {
g_controller->Motion(id, sensor, data, time_us);
Common::LockGuard lock(g_router_mutex);
if (auto* player = PlayerForDevice(id)) {
player->Motion(id, sensor, data, time_us);
}
}
void ToggleGyroEmulation(int id) {
g_controller->ToggleGyroEmulation(id);
Common::LockGuard lock(g_router_mutex);
if (auto* player = PlayerForDevice(id)) {
player->ToggleGyroEmulation(id);
}
}
void ResetInputState() {
g_controller->ResetInputState();
for (const auto& player: g_players) {
if (player != nullptr) {
player->ResetInputState();
}
}
}
int GetActiveControllerId() {
return g_controller != nullptr ? g_controller->GetActiveControllerId() : -1;
return g_players[0] != nullptr ? g_players[0]->GetActiveControllerId() : -1;
}
static bool PadOpenArgsAreValid(int user_id, int type, int index) {
// The host keyboard and gamepad feed one standard port for the configured user.
return user_id == Config::GetUserId() && type == 0 && index == 0;
bool IsPlayerConnected(int player) {
if (player < 0 || player >= Config::MAX_PLAYERS || g_players[player] == nullptr) {
return false;
}
return g_players[player]->GetActiveControllerId() != -1;
}
bool GameController::PadOpenArgsAreValid(int user_id, int type, int index) const {
// The host keyboard and gamepads of a player feed one standard port for that player's user.
return user_id == Config::GetPlayerUserId(m_player) && type == 0 && index == 0;
}
int GameController::InitPort() {
@@ -1064,7 +1166,7 @@ int GameController::OpenPort(int user_id, int type, int index) {
return PAD_ERROR_ALREADY_OPENED;
}
m_port_state = PortState::Open;
return PAD_HANDLE;
return Handle();
}
int GameController::GetPortHandle(int user_id, int type, int index) {
@@ -1072,15 +1174,16 @@ int GameController::GetPortHandle(int user_id, int type, int index) {
if (m_port_state == PortState::Uninitialized) {
return PAD_ERROR_NOT_INITIALIZED;
}
return m_port_state == PortState::Open && PadOpenArgsAreValid(user_id, type, index) ? PAD_HANDLE
: PAD_ERROR_NO_HANDLE;
return m_port_state == PortState::Open && PadOpenArgsAreValid(user_id, type, index)
? Handle()
: PAD_ERROR_NO_HANDLE;
}
int GameController::CheckPortHandleLocked(int handle) const {
if (m_port_state == PortState::Uninitialized) {
return PAD_ERROR_NOT_INITIALIZED;
}
return m_port_state == PortState::Open && handle == PAD_HANDLE ? OK : PAD_ERROR_INVALID_HANDLE;
return m_port_state == PortState::Open && handle == Handle() ? OK : PAD_ERROR_INVALID_HANDLE;
}
int GameController::CheckPortHandle(int handle) {
@@ -1098,12 +1201,19 @@ int GameController::ClosePort(int handle) {
}
int PadCheckHandle(int handle) {
return g_controller->CheckPortHandle(handle);
if (g_players[0] == nullptr) {
return PAD_ERROR_NOT_INITIALIZED;
}
auto* player = PlayerForHandle(handle);
return player != nullptr ? player->CheckPortHandle(handle) : PAD_ERROR_INVALID_HANDLE;
}
int KYTY_SYSV_ABI PadInit() {
PRINT_NAME();
return g_controller->InitPort();
for (const auto& player: g_players) {
(void)player->InitPort();
}
return OK;
}
int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param) {
@@ -1115,7 +1225,8 @@ int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param) {
"\t param = 0x%016" PRIx64 "\n",
user_id, type, index, reinterpret_cast<uint64_t>(param));
return g_controller->OpenPort(user_id, type, index);
const int player = Config::GetPlayerFromUserId(user_id);
return player >= 0 ? g_players[player]->OpenPort(user_id, type, index) : PAD_ERROR_INVALID_ARG;
}
int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index) {
@@ -1126,13 +1237,19 @@ int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index) {
"\t index = %d\n",
user_id, type, index);
return g_controller->GetPortHandle(user_id, type, index);
const int player = Config::GetPlayerFromUserId(user_id);
return player >= 0 ? g_players[player]->GetPortHandle(user_id, type, index)
: PAD_ERROR_NO_HANDLE;
}
int KYTY_SYSV_ABI PadClose(int handle) {
PRINT_NAME();
LOGF("\t handle = %d\n", handle);
return g_controller->ClosePort(handle);
auto* player = PlayerForHandle(handle);
if (player == nullptr) {
return PAD_ERROR_INVALID_HANDLE;
}
return player->ClosePort(handle);
}
int KYTY_SYSV_ABI PadSetMotionSensorState(int handle, bool enable) {
@@ -1143,7 +1260,7 @@ int KYTY_SYSV_ABI PadSetMotionSensorState(int handle, bool enable) {
}
LOGF("\t enable = %s\n", (enable ? "true" : "false"));
g_controller->SetMotionSensorState(enable);
PlayerForHandle(handle)->SetMotionSensorState(enable);
return OK;
}
@@ -1167,7 +1284,7 @@ int KYTY_SYSV_ABI PadResetOrientation(int handle) {
return error;
}
g_controller->ResetOrientation();
PlayerForHandle(handle)->ResetOrientation();
return OK;
}
@@ -1183,7 +1300,7 @@ int KYTY_SYSV_ABI PadGetControllerInformation(int handle, PadControllerInformati
int connected_count = 0;
bool connected = false;
g_controller->GetConnectionInfo(&connected, &connected_count);
PlayerForHandle(handle)->GetConnectionInfo(&connected, &connected_count);
std::memset(info, 0, sizeof(*info));
@@ -1228,7 +1345,7 @@ int KYTY_SYSV_ABI PadReadState(int handle, PadData* data) {
bool connected = false;
ControllerState state;
g_controller->ReadState(&state, &connected, &connected_count);
PlayerForHandle(handle)->ReadState(&state, &connected, &connected_count);
pad_fill_data(data, state, connected, connected_count);
@@ -1252,11 +1369,12 @@ int KYTY_SYSV_ABI PadRead(int handle, PadData* data, int num) {
bool connected = false;
ControllerState states[64] = {};
int ret_num = g_controller->ReadStates(states, num, &connected, &connected_count);
auto* player = PlayerForHandle(handle);
int ret_num = player->ReadStates(states, num, &connected, &connected_count);
if (!connected || ret_num == 0) {
if (connected) {
g_controller->ReadState(&states[0], &connected, &connected_count);
player->ReadState(&states[0], &connected, &connected_count);
}
ret_num = 1;
}
@@ -1270,6 +1388,7 @@ int KYTY_SYSV_ABI PadRead(int handle, PadData* data, int num) {
int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param) {
PRINT_NAME();
Common::LockGuard lock(g_router_mutex);
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
@@ -1282,7 +1401,7 @@ int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param) {
"\t small_motor = %d\n",
static_cast<int>(param->large_motor), static_cast<int>(param->small_motor));
g_controller->SetVibration(param->large_motor, param->small_motor);
PlayerForHandle(handle)->SetVibration(param->large_motor, param->small_motor);
return OK;
}
@@ -1299,6 +1418,7 @@ int KYTY_SYSV_ABI PadResetLightBar(int handle) {
int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param) {
PRINT_NAME();
Common::LockGuard lock(g_router_mutex);
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
@@ -1307,13 +1427,14 @@ int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param) {
return PAD_ERROR_INVALID_ARG;
}
g_controller->SetLightBar(param->r, param->g, param->b);
PlayerForHandle(handle)->SetLightBar(param->r, param->g, param->b);
return OK;
}
int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* param) {
PRINT_NAME();
Common::LockGuard lock(g_router_mutex);
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
@@ -1322,7 +1443,7 @@ int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* p
return PAD_ERROR_INVALID_ARG;
}
return g_controller->SetTriggerEffect(*param) ? OK : PAD_ERROR_INVALID_ARG;
return PlayerForHandle(handle)->SetTriggerEffect(*param) ? OK : PAD_ERROR_INVALID_ARG;
}
int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle, PadTriggerEffectStateInformation* info) {
@@ -1337,7 +1458,7 @@ int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle, PadTriggerEffectStateInfo
return PAD_ERROR_INVALID_ARG;
}
g_controller->GetTriggerEffectState(info);
PlayerForHandle(handle)->GetTriggerEffectState(info);
return OK;
}
+4
View File
@@ -68,6 +68,8 @@ inline int controller_get_axis(int min, int max, int value) {
void Connect(int id);
void Disconnect(int id);
// Moves the keyboard to the next player (shared with player 1, then 1-4) while the game runs.
void CycleKeyboardPlayer();
void SetButton(int id, uint32_t button, bool down);
void SetAxis(int id, Axis axis, int value);
void SetRightStick(int id, int x, int y);
@@ -76,6 +78,8 @@ void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us);
void ToggleGyroEmulation(int id);
void ResetInputState();
int GetActiveControllerId();
// True when the player has a keyboard or gamepad assigned.
bool IsPlayerConnected(int player);
enum class Setting { SpeakerVolume, VibrationIntensity, TriggerEffectIntensity };
+22
View File
@@ -25,6 +25,9 @@
#include <string>
#include <thread>
#include <unordered_map>
#include <atomic>
#include <cinttypes>
#include <iterator>
#include <variant>
#include <vector>
@@ -176,6 +179,14 @@ static int GetHostPathStat(const std::filesystem::path& host_path, LibKernel::Fi
return OK;
}
// Mounts can appear after a failed lookup (e.g. additional content), so forget negative results.
void InvalidateFailedResolves() {
std::scoped_lock lock(g_mutex);
for (auto it = g_resolved_paths.begin(); it != g_resolved_paths.end();) {
it = it->second.result != OK ? g_resolved_paths.erase(it) : std::next(it);
}
}
static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size) {
if (guest_path == nullptr || guest_path[0] == '\0') {
return LibKernel::KERNEL_ERROR_EINVAL;
@@ -225,6 +236,10 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
log_missing = true;
}
}
if (!log_missing && info.result == OK && path.starts_with("/addcont")) {
LOGF("\tAPR resolve: %s -> %s (%" PRIu64 " bytes)\n", guest_path,
Common::PathToString(info.host_path).c_str(), info.file_size);
}
if (log_missing) {
LOGF("\tAPR resolve missing path: %s -> %s\n", guest_path, Common::PathToString(info.host_path).c_str());
}
@@ -1181,6 +1196,13 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) {
uint64_t bytes_read = 0;
auto result = ReadHostFileToGuest(host_path, command.file_offset, command.destination,
command.size, &bytes_read);
static std::atomic<uint32_t> s_logged_dlc_reads {0};
if (host_path.filename() == "re_dlc_000.pak" && s_logged_dlc_reads.fetch_add(1) < 32) {
LOGF("\tAPR read dlc: id=0x%08" PRIx32 ", offset=0x%016" PRIx64 ", size=0x%016" PRIx64
", read=0x%016" PRIx64 ", result=0x%08" PRIx32 "\n",
command.file_id, command.file_offset, command.size, bytes_read,
static_cast<uint32_t>(result));
}
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), Common::PathToString(host_path).c_str());
+20 -5
View File
@@ -22,13 +22,16 @@
namespace Libs {
namespace AprShared {
void InvalidateFailedResolves();
}
LIB_VERSION("AppContent", 1, "AppContentUtil", 1, 1);
namespace AppContent {
constexpr uint32_t APP_CONTENT_APPPARAM_SKU_FLAG_FULL = 3;
constexpr int APP_CONTENT_ERROR_PARAMETER = -2133262334; /* 0x80D90002 */
constexpr int APP_CONTENT_ERROR_BUSY = -2133262333; /* 0x80D90003 */
constexpr int APP_CONTENT_ERROR_NOT_MOUNTED = -2133262332; /* 0x80D90004 */
constexpr int APP_CONTENT_ERROR_NOT_FOUND = -2133262331; /* 0x80D90005 */
constexpr int APP_CONTENT_ERROR_MOUNT_FULL = -2133262330; /* 0x80D90006 */
@@ -207,7 +210,10 @@ int KYTY_SYSV_ABI AppContentAddcontMount(uint32_t servic
for (const auto& m: g_addcont_mounts) {
if (m.label == entry->entitlement_label) {
return APP_CONTENT_ERROR_BUSY;
const auto guest_point = m.point.substr(0, m.point.size() - 1);
std::strncpy(mount_point->data, guest_point.c_str(), sizeof(mount_point->data) - 1);
LOGF("\t already mounted %s\n", guest_point.c_str());
return OK;
}
}
@@ -232,7 +238,15 @@ int KYTY_SYSV_ABI AppContentAddcontMount(uint32_t servic
LOGF("\t mounted %s -> %s\n", mount.point.c_str(),
Common::PathToString(entry->mount_dir).c_str());
std::strncpy(mount_point->data, mount.point.c_str(), sizeof(mount_point->data) - 1);
AprShared::InvalidateFailedResolves();
// Titles scan /download0/pdlc for downloaded content; it must exist even when empty.
if (const auto pdlc = LibKernel::FileSystem::GetRealFilename("/download0/pdlc"); !pdlc.empty()) {
std::filesystem::create_directories(pdlc, ec);
}
const auto guest_point = mount.point.substr(0, mount.point.size() - 1);
std::strncpy(mount_point->data, guest_point.c_str(), sizeof(mount_point->data) - 1);
g_addcont_mounts.push_back(std::move(mount));
return OK;
@@ -251,8 +265,8 @@ int KYTY_SYSV_ABI AppContentAddcontUnmount(const AppContentMountPoint* mount_poi
return APP_CONTENT_ERROR_NOT_MOUNTED;
}
const std::string point(mount_point->data,
strnlen(mount_point->data, sizeof(mount_point->data)));
const std::string point = Common::FixDirectorySlash(
std::string_view(mount_point->data, strnlen(mount_point->data, sizeof(mount_point->data))));
std::lock_guard lock(g_addcont_mutex);
@@ -263,6 +277,7 @@ int KYTY_SYSV_ABI AppContentAddcontUnmount(const AppContentMountPoint* mount_poi
}
LibKernel::FileSystem::Umount(point);
g_addcont_mounts.erase(it);
AprShared::InvalidateFailedResolves();
return OK;
}
+13 -7
View File
@@ -11,6 +11,7 @@
#include "kernel/eventQueue.h"
#include "kernel/fileSystem.h"
#include "kernel/memory.h"
#include "kernel/module.h"
#include "kernel/pthread.h"
#include "kernel/semaphore.h"
#include "kernel/syncOnAddress.h"
@@ -65,7 +66,6 @@ LIB_DEFINE(InitLibKernel_1_Apr);
namespace LibKernel {
using KernelModule = int32_t;
using get_thread_atexit_count_func_t = KYTY_SYSV_ABI int (*)(KernelModule);
using thread_atexit_report_func_t = KYTY_SYSV_ABI void (*)(KernelModule);
@@ -1201,9 +1201,9 @@ static int KYTY_SYSV_ABI KernelRaiseException(Pthread thread, int signum) {
return OK;
}
static KYTY_SYSV_ABI KernelModule KernelLoadStartModule(const char* module_file_name, size_t args,
const void* argp, uint32_t flags,
const KernelLoadModuleOpt* opt, int* res) {
KernelModule KYTY_SYSV_ABI KernelLoadStartModule(const char* module_file_name, size_t args,
const void* argp, uint32_t flags,
const KernelLoadModuleOpt* opt, int* res) {
PRINT_NAME();
// EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
@@ -1255,9 +1255,9 @@ static KYTY_SYSV_ABI KernelModule KernelLoadStartModule(const char* module_file_
return static_cast<KernelModule>(handle);
}
static int KYTY_SYSV_ABI KernelStopUnloadModule(KernelModule handle, size_t args, const void* argp,
uint32_t flags, const KernelUnloadModuleOpt* opt,
int* res) {
int KYTY_SYSV_ABI KernelStopUnloadModule(KernelModule handle, size_t args, const void* argp,
uint32_t flags, const KernelUnloadModuleOpt* opt,
int* res) {
PRINT_NAME();
// EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
@@ -2006,6 +2006,11 @@ int KYTY_SYSV_ABI listen(int s, int backlog) {
return Network::Net::Listen(s, backlog);
}
int KYTY_SYSV_ABI shutdown(int s, int how) {
PRINT_NAME();
return Network::Net::Shutdown(s, how);
}
int KYTY_SYSV_ABI accept(int s, void* addr, uint32_t* addrlen) {
PRINT_NAME();
return Network::Net::Accept(s, addr, addrlen);
@@ -2242,6 +2247,7 @@ LIB_DEFINE(InitLibKernel_1_Posix) {
LIB_FUNC("KuOmgKoqCdY", Posix::bind);
LIB_FUNC("XVL8So3QJUk", Posix::connect);
LIB_FUNC("pxnCmagrtao", Posix::listen);
LIB_FUNC("TUuiYS2kE8s", Posix::shutdown);
LIB_FUNC("3e+4Iv7IJ8U", Posix::accept);
LIB_FUNC("RenI1lL1WFk", Posix::getsockname);
LIB_FUNC("6O8EwYOgH9Y", Posix::getsockopt);
+8 -2
View File
@@ -221,11 +221,17 @@ int KYTY_SYSV_ABI PlayGoGetChunkId(int handle, uint16_t* out_chunk_id_list,
if (out_entries == nullptr) {
return PLAYGO_ERROR_BAD_POINTER;
}
if (number_of_entries != 0 && out_chunk_id_list == nullptr) {
return PLAYGO_ERROR_BAD_POINTER;
if (out_chunk_id_list != nullptr && number_of_entries == 0) {
return PLAYGO_ERROR_BAD_SIZE;
}
ensure_chunks_loaded();
// A null output buffer asks for the total chunk count.
if (out_chunk_id_list == nullptr) {
*out_entries = g_chunks_num;
return OK;
}
const uint32_t entries = std::min(number_of_entries, g_chunks_num);
for (uint32_t i = 0; i < entries; i++) {
out_chunk_id_list[i] = static_cast<uint16_t>(i);
@@ -1,9 +1,9 @@
#include "common/abi.h"
#include "common/common.h"
#include "common/stringUtils.h"
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
#include <cstdint>
namespace Libs {
namespace LibRazorCpu {
@@ -16,8 +16,18 @@ static KYTY_SYSV_ABI uint32_t RazorCpuIsCapturing() {
return 0;
}
static int KYTY_SYSV_ABI RazorCpuPopMarker() {
return 0;
}
static int KYTY_SYSV_ABI RazorCpuPushMarker(const char*, uint32_t, uint32_t) {
return 0;
}
LIB_DEFINE(InitRazorCpu_1) {
LIB_FUNC("EboejOQvLL4", LibRazorCpu::RazorCpuIsCapturing);
LIB_FUNC("YpkGsMXP3ew", LibRazorCpu::RazorCpuPopMarker);
LIB_FUNC("zw+celG7zSI", LibRazorCpu::RazorCpuPushMarker);
}
} // namespace LibRazorCpu
+13 -4
View File
@@ -1,6 +1,7 @@
#include "common/abi.h"
#include "common/assert.h"
#include "common/common.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
@@ -300,15 +301,23 @@ static std::string memory_dir_name(uint32_t slot_id) {
return "sce_sdmemory" + (slot_id == 0 ? std::string() : std::to_string(slot_id));
}
static std::filesystem::path save_user_directory(std::string_view title_id, int32_t user_id) {
auto directory = std::filesystem::path(SAVE_DATA_DIR);
if (Config::IsGuestUserId(user_id)) {
directory /= std::filesystem::path("[users]") / std::to_string(user_id);
}
return directory / title_id;
}
static std::filesystem::path save_directory(std::string_view title_id, std::string_view dir_name,
int32_t user_id) {
const auto directory = std::filesystem::path(SAVE_DATA_DIR) / title_id / dir_name;
for (uint32_t slot = 0; slot < 4; slot++) {
if (dir_name == memory_dir_name(slot)) {
return directory / std::to_string(user_id);
return std::filesystem::path(SAVE_DATA_DIR) / title_id / dir_name /
std::to_string(user_id);
}
}
return directory;
return save_user_directory(title_id, user_id) / dir_name;
}
static std::filesystem::path memory_directory(int32_t user_id, uint32_t slot_id) {
@@ -590,7 +599,7 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
Common::LockGuard lock(g_mount_mutex);
const std::string title_id = cond->title_id != nullptr ? cond->title_id->data : get_title_id();
const auto root = std::filesystem::path(SAVE_DATA_DIR) / title_id;
const auto root = save_user_directory(title_id, cond->user_id);
std::vector<std::string> dir_list;
if (Common::File::IsDirectoryExisting(root)) {
+47 -16
View File
@@ -3,6 +3,7 @@
#include "common/common.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "kernel/module.h"
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
@@ -20,14 +21,40 @@ int KYTY_SYSV_ABI KernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleI
namespace Sysmodule {
constexpr int SYSMODULE_ERROR_UNLOADED = static_cast<int>(0x805a1001u);
constexpr int SYSMODULE_ERROR_UNLOADED = static_cast<int>(0x805a1001u);
constexpr int SYSMODULE_ERROR_FATAL = static_cast<int>(0x805a10ffu);
static std::mutex g_module_mutex;
static std::unordered_map<uint16_t, uint32_t> g_module_refs;
static constexpr struct NativeModule {
uint16_t id;
const char* path;
} g_native_modules[] = {{0x0115, "/app0/sce_module/libSceNpCppWebApi.prx"}};
static void LoadModule(uint16_t id) {
struct Module {
uint32_t references = 0;
LibKernel::KernelModule handle = -1;
};
static std::mutex g_module_mutex;
static std::unordered_map<uint16_t, Module> g_modules;
static int LoadModule(uint16_t id) {
std::scoped_lock lock(g_module_mutex);
++g_module_refs[id];
if (const auto module = g_modules.find(id); module != g_modules.end()) {
++module->second.references;
return 0;
}
LibKernel::KernelModule handle = -1;
for (const auto& module: g_native_modules) {
if (module.id != id) continue;
handle = LibKernel::KernelLoadStartModule(module.path, 0, nullptr, 0, nullptr, nullptr);
if (handle < 0) {
return handle;
}
break;
}
g_modules.emplace(id, Module {1, handle});
return 0;
}
static KYTY_SYSV_ABI int SysmoduleGetModuleInfoForUnwind(uint64_t addr, int flags,
@@ -40,8 +67,7 @@ static KYTY_SYSV_ABI int SysmoduleLoadModule(uint16_t id) {
LOGF("\t id = %d\n", static_cast<int>(id));
LoadModule(id);
return 0;
return LoadModule(id);
}
static KYTY_SYSV_ABI int SysmoduleUnloadModule(uint16_t id) {
@@ -50,12 +76,19 @@ 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()) {
const auto module = g_modules.find(id);
if (module == g_modules.end()) {
return SYSMODULE_ERROR_UNLOADED;
}
if (--module->second == 0) {
g_module_refs.erase(module);
if (module->second.references == 1) {
if (module->second.handle >= 0 &&
LibKernel::KernelStopUnloadModule(module->second.handle, 0, nullptr, 0, nullptr,
nullptr) < 0) {
return SYSMODULE_ERROR_FATAL;
}
g_modules.erase(id);
} else {
--module->second.references;
}
return 0;
}
@@ -71,10 +104,8 @@ 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;
*ret = LoadModule(id);
return *ret;
}
static KYTY_SYSV_ABI int SysmoduleIsLoaded(uint16_t id) {
@@ -83,7 +114,7 @@ static KYTY_SYSV_ABI int SysmoduleIsLoaded(uint16_t id) {
LOGF("\t id = %d\n", static_cast<int>(id));
std::scoped_lock lock(g_module_mutex);
return g_module_refs.contains(id) ? 0 : SYSMODULE_ERROR_UNLOADED;
return g_modules.contains(id) ? 0 : SYSMODULE_ERROR_UNLOADED;
}
} // namespace Sysmodule
+5
View File
@@ -258,6 +258,10 @@ static int KYTY_SYSV_ABI SystemServicePowerTick() {
return OK;
}
static int KYTY_SYSV_ABI SystemServiceDisableMusicPlayer() {
return OK;
}
static int KYTY_SYSV_ABI SystemServiceReportAbnormalTermination(const void* info) {
PRINT_NAME();
@@ -577,6 +581,7 @@ LIB_DEFINE(InitSystemService_1) {
LIB_FUNC("8Lo6Zv94aho", SystemService::SystemServiceDisableNoticeScreenSkipFlagAutoSet);
LIB_FUNC("Q3utJvma4Mo", SystemService::SystemServiceSetNoticeScreenSkipFlag);
LIB_FUNC("XbbJC3E+L5M", SystemService::SystemServicePowerTick);
LIB_FUNC("x1UB9bwDSOw", SystemService::SystemServiceDisableMusicPlayer);
LIB_FUNC("3s8cHiCBKBE", SystemService::SystemServiceReportAbnormalTermination);
SystemGesture::InitSystemGesture_1(s);
}
+12 -3
View File
@@ -105,6 +105,8 @@ struct TrophyState {
const Trophies::Package* package = nullptr;
Trophies::UnlockData unlocks;
std::filesystem::path path;
// Guests (players 2+) have no profile: nothing is loaded, saved or announced.
bool guest = false;
};
struct TrophyContext {
@@ -153,9 +155,12 @@ static TrophyState* LoadTrophyState(TrophyKey key) {
package.event_rules.size(), uds_filename.c_str());
}
}
auto path = Trophies::UnlocksPath({}, title_id, key.first, key.second);
auto unlocks = Trophies::LoadUnlockData(path);
return &g_states.emplace(key, TrophyState {&package, std::move(unlocks), std::move(path)})
const bool guest = Config::IsGuestUserId(key.first);
auto path = guest ? std::filesystem::path {}
: Trophies::UnlocksPath({}, title_id, key.first, key.second);
auto unlocks = guest ? Trophies::UnlockData {} : Trophies::LoadUnlockData(path);
return &g_states
.emplace(key, TrophyState {&package, std::move(unlocks), std::move(path), guest})
.first->second;
}
@@ -240,6 +245,10 @@ static void FillGroupInfo(const TrophyState& state, int group_id, const std::str
}
static void RecordTrophyUnlock(TrophyState& state, int trophy_id, bool from_stat = false) {
if (state.guest) {
LOGF("[Trophy] guest users cannot earn trophies (trophy %d)\n", trophy_id);
return;
}
const auto found = state.package->trophies.find(trophy_id);
if (found == state.package->trophies.end()) {
LOGF("[Trophy] ignored unknown trophy ID %d\n", trophy_id);
+54 -27
View File
@@ -1,12 +1,14 @@
#include "common/abi.h"
#include "common/assert.h"
#include "common/emulatorConfig.h"
#include "common/localization.h"
#include "libs/controller.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
#include <cstdio>
#include <cstring>
#include <string>
namespace Libs {
@@ -39,6 +41,15 @@ struct UserServiceGamePresets {
static_assert(sizeof(UserServiceGamePresets) == 40);
static bool IsPlayerLoggedIn(int player) {
return player == 0 || (player > 0 && Controller::IsPlayerConnected(player));
}
static int GetLoggedInPlayer(int user_id) {
const int player = Config::GetPlayerFromUserId(user_id);
return IsPlayerLoggedIn(player) ? player : -1;
}
static KYTY_SYSV_ABI int UserServiceInitialize(const void* /*params*/) {
PRINT_NAME();
@@ -66,13 +77,16 @@ static KYTY_SYSV_ABI int UserServiceGetEvent(SceUserServiceEvent* event) {
EXIT_NOT_IMPLEMENTED(event == nullptr);
static bool logged_in = false;
static bool logged_in[Config::MAX_PLAYERS] = {};
if (!logged_in) {
logged_in = true;
event->event_type = UserServiceEventTypeLogin;
event->user_id = Config::GetUserId();
return OK;
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
const bool connected = IsPlayerLoggedIn(player);
if (connected != logged_in[player]) {
logged_in[player] = connected;
event->event_type = connected ? UserServiceEventTypeLogin : UserServiceEventTypeLogout;
event->user_id = Config::GetPlayerUserId(player);
return OK;
}
}
return USER_SERVICE_ERROR_NO_EVENT;
@@ -83,24 +97,36 @@ static KYTY_SYSV_ABI int UserServiceGetLoginUserIdList(UserServiceLoginUserIdLis
EXIT_NOT_IMPLEMENTED(user_id_list == nullptr);
user_id_list->user_id[0] = Config::GetUserId();
user_id_list->user_id[1] = -1;
user_id_list->user_id[2] = -1;
user_id_list->user_id[3] = -1;
int count = 0;
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
if (IsPlayerLoggedIn(player)) {
user_id_list->user_id[count++] = Config::GetPlayerUserId(player);
}
}
while (count < Config::MAX_PLAYERS) {
user_id_list->user_id[count++] = -1;
}
return OK;
}
static KYTY_SYSV_ABI int UserServiceGetUserName(int user_id, char* name, size_t size) {
if (user_id != Config::GetUserId()) {
if (name == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
const int player = GetLoggedInPlayer(user_id);
if (player < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
EXIT_NOT_IMPLEMENTED(size < 5);
const auto& user_name = Config::GetUserName();
int s = snprintf(name, size, "%s", user_name.c_str());
EXIT_NOT_IMPLEMENTED(static_cast<size_t>(s) >= size);
const auto user_name =
player > 0 ? std::string(Common::Localization::Get(Config::GetConsoleLanguage(), "user.guest")) +
" " + std::to_string(player + 1)
: Config::GetUserName();
if (size <= user_name.size()) {
return USER_SERVICE_ERROR_BUFFER_TOO_SHORT;
}
std::memcpy(name, user_name.c_str(), user_name.size() + 1);
return OK;
}
@@ -111,11 +137,12 @@ static KYTY_SYSV_ABI int UserServiceGetUserNumber(int user_id, int32_t* number)
if (number == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != Config::GetUserId()) {
const int player = GetLoggedInPlayer(user_id);
if (player < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
*number = 1;
*number = player + 1;
return OK;
}
@@ -126,7 +153,7 @@ static KYTY_SYSV_ABI int UserServiceGetGamePresets(int user_id, UserServiceGameP
if (presets == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != Config::GetUserId()) {
if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -147,7 +174,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityVibration(int user_id, int32
if (vibration == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != Config::GetUserId()) {
if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -163,7 +190,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityTriggerEffect(int user_
if (trigger_effect == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != Config::GetUserId()) {
if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -178,7 +205,7 @@ static KYTY_SYSV_ABI int UserServiceGetAgeLevel(int user_id, uint32_t* age_level
if (age_level == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != Config::GetUserId()) {
if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -194,7 +221,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityChatTranscription(int u
if (chat_transcription == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != Config::GetUserId()) {
if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -210,7 +237,7 @@ UserServiceGetAccessibilityPressAndHoldDelay(int user_id, int32_t* press_and_hol
if (press_and_hold_delay == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != Config::GetUserId()) {
if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -226,7 +253,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityZoomEnabled(int user_id
if (zoom_enabled == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != Config::GetUserId()) {
if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -242,7 +269,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityZoomFollowFocus(int use
if (zoom_follow_focus == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != Config::GetUserId()) {
if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
+26 -13
View File
@@ -891,6 +891,7 @@ struct P2pEndpoint {
uint16_t virtual_port = 0;
int nonblocking = 0;
int broadcast = 0;
int use_crypto = 0;
int send_buffer = 9216;
int receive_buffer = 40 * 1024;
};
@@ -1095,6 +1096,7 @@ static int* P2pSocketOption(P2pEndpoint& endpoint, int option) {
case 0x1001: return &endpoint.send_buffer;
case 0x1002: return &endpoint.receive_buffer;
case 0x1200: return &endpoint.nonblocking;
case 0x20000: return &endpoint.use_crypto;
default: return nullptr;
}
}
@@ -2096,19 +2098,15 @@ int KYTY_SYSV_ABI Shutdown(int s, int how) {
return -1;
}
#if defined(_WIN32)
if (::shutdown(socket, how) == SOCKET_ERROR) {
if (::shutdown(socket, how) != 0) {
return SetHostSocketError();
}
#if defined(_WIN32)
if (how == SD_RECEIVE || how == SD_BOTH) {
transport->receive_shutdown.store(true, std::memory_order_relaxed);
}
return 0;
#else
*Posix::GetErrorAddr() = Posix::POSIX_ENOSYS;
return -1;
#endif
return 0;
}
int KYTY_SYSV_ABI Getsockname(int s, void* addr, uint32_t* addrlen) {
@@ -2463,9 +2461,9 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
return -1;
}
NativeSocket socket = INVALID_NATIVE_SOCKET;
std::shared_ptr<SocketTransport> transport;
if (!GetSocketBackend(s, &socket, nullptr, &transport)) {
NativeSocket socket = INVALID_NATIVE_SOCKET;
SocketSlot state;
if (!GetSocketBackend(s, &socket, &state)) {
return -1;
}
@@ -2473,6 +2471,21 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
if (host_flags < 0) {
return -1;
}
if (state.p2p) {
if (!state.p2p->bound || (!state.p2p->nonblocking && (flags & 0x80) == 0)) {
return SetGuestSocketError(Posix::POSIX_EOPNOTSUPP);
}
char pending;
if (::recv(socket, &pending, sizeof(pending), MSG_PEEK) < 0) {
SetHostSocketError();
// Winsock reports a peeked datagram's truncation as EMSGSIZE.
if (*Posix::GetErrorAddr() != Posix::POSIX_EMSGSIZE) {
return -1;
}
}
// Pending UDPP2P packets still require framing and optional decryption.
return SetGuestSocketError(Posix::POSIX_EOPNOTSUPP);
}
const auto host_len = static_cast<SocketIoLength>(
std::min<uint64_t>(len, std::numeric_limits<SocketIoLength>::max()));
@@ -2480,12 +2493,12 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
SocketLength host_addrlen = sizeof(host_addr);
int64_t result = 0;
#if defined(_WIN32)
if (transport->type == SOCK_STREAM) {
if (state.transport->type == SOCK_STREAM) {
if (addr != nullptr &&
::getpeername(socket, reinterpret_cast<sockaddr*>(&host_addr), &host_addrlen) != 0) {
return SetHostSocketError();
}
result = ReceiveStream(*transport, static_cast<char*>(buf), host_len, host_flags,
result = ReceiveStream(*state.transport, static_cast<char*>(buf), host_len, host_flags,
(flags & 0x80) != 0);
if (result < 0) {
return result;
@@ -2493,7 +2506,7 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
} else
#endif
{
transport.reset();
state.transport.reset();
if (addr == nullptr) {
result = ::recv(socket, static_cast<char*>(buf), host_len, host_flags);
} else {
+11 -9
View File
@@ -58,16 +58,17 @@ struct TlsRegStub {
}
static uint64_t GetSize() { return 32; }
// sub rsp,0x80
// lea rsp,[rsp-0x80]
// nop
// push rax
// call safe_call
// mov <reg>,rax
// pop rax
// add rsp,0x80
// lea rsp,[rsp+0x80]
// ret
uint8_t code[32] = {0x48, 0x81, 0xEC, 0x80, 0x00, 0x00, 0x00, 0x50, 0xE8, 0x00, 0x00,
0x00, 0x00, 0x48, 0x89, 0xC0, 0x58, 0x48, 0x81, 0xC4, 0x80, 0x00,
0x00, 0x00, 0xC3, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90};
uint8_t code[32] = {0x48, 0x8D, 0x64, 0x24, 0x80, 0x66, 0x90, 0x50, 0xE8, 0x00, 0x00,
0x00, 0x00, 0x48, 0x89, 0xC0, 0x58, 0x48, 0x8D, 0xA4, 0x24, 0x80,
0x00, 0x00, 0x00, 0xC3, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90};
};
struct SafeCall {
@@ -78,7 +79,8 @@ struct SafeCall {
static uint64_t GetSize() { return 0x1000; }
uint8_t code[0x90] = {
/*00*/ 0x48, 0x81, 0xec, 0x80, 0x00, 0x00, 0x00, // sub rsp,0x80
/*00*/ 0x48, 0x8d, 0x64, 0x24, 0x80, // lea rsp,[rsp-0x80]
/*05*/ 0x66, 0x90, // nop
/*07*/ 0x9c, // pushfq
/*08*/ 0x51, // push rcx
/*09*/ 0x52, // push rdx
@@ -108,9 +110,9 @@ struct SafeCall {
/*40*/ 0x5a, // pop rdx
/*41*/ 0x59, // pop rcx
/*42*/ 0x9d, // popfq
/*43*/ 0x48, 0x81, 0xc4, 0x80, 0x00, 0x00,
0x00, // add rsp,0x80
/*4a*/ 0xc3, // ret
/*43*/ 0x48, 0x8d, 0xa4, 0x24, 0x80, 0x00, 0x00,
0x00, // lea rsp,[rsp+0x80]
/*4b*/ 0xc3, // ret
};
};
+7
View File
@@ -57,6 +57,7 @@ static void PrintUsage() {
::printf(" --controller-color <#RRGGBB> Override the controller lightbar color.\n");
::printf(" --controller-volume <0-100> DualSense speaker volume. Default: 50.\n");
::printf(" --controller-vibration <0-100> DualSense vibration intensity. Default: 100.\n");
::printf(" --keyboard-player <0-4> Keyboard player; 0 shares player 1 with the gamepad. Default: 0.\n");
::printf(
" --present-mode <value> Fifo, Mailbox, or Immediate. Default: Mailbox.\n");
::printf(
@@ -378,6 +379,12 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
::printf("invalid controller vibration intensity: %s\n", value.c_str());
return false;
}
} else if (arg == "--keyboard-player") {
if (!ParseUint32(value, options.config.keyboard_player) ||
options.config.keyboard_player > static_cast<uint32_t>(Config::MAX_PLAYERS)) {
::printf("invalid keyboard player: %s\n", value.c_str());
return false;
}
} else if (arg == "--present-mode") {
if (!ParseEnum(value, options.config.present_mode)) {
::printf("invalid present mode: %s\n", value.c_str());
+139 -4
View File
@@ -4,6 +4,7 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <vector>
namespace {
@@ -23,6 +24,9 @@ std::vector<Rumble> rumble;
std::vector<Rumble> haptics;
std::vector<std::array<Uint8, 32>> effects;
bool haptics_handles_rumble = false;
std::array<Rumble, 6> pad_rumble {};
std::vector<int> closed_pads;
std::array<std::array<Uint8, 3>, 6> pad_colors {};
} // namespace
namespace Fake {
@@ -30,14 +34,15 @@ Uint64 GetTicks() {
return now;
}
SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
return id == 1 || id == 2 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
return id >= 1 && id <= 5 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
: nullptr;
}
SDL_GamepadType GetGamepadType(SDL_Gamepad*) {
return SDL_GAMEPAD_TYPE_PS5;
}
bool RumbleGamepad(SDL_Gamepad*, Uint16 large, Uint16 small, Uint32 duration) {
bool RumbleGamepad(SDL_Gamepad* pad, Uint16 large, Uint16 small, Uint32 duration) {
rumble.push_back({large, small, duration});
pad_rumble[reinterpret_cast<uintptr_t>(pad)] = rumble.back();
return true;
}
bool SendGamepadEffect(SDL_Gamepad*, const void* data, int size) {
@@ -47,7 +52,8 @@ bool SendGamepadEffect(SDL_Gamepad*, const void* data, int size) {
effects.push_back(effect);
return true;
}
bool SetGamepadLED(SDL_Gamepad*, Uint8, Uint8, Uint8) {
bool SetGamepadLED(SDL_Gamepad* pad, Uint8 r, Uint8 g, Uint8 b) {
pad_colors[reinterpret_cast<uintptr_t>(pad)] = {r, g, b};
return true;
}
bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) {
@@ -56,7 +62,9 @@ bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) {
bool SetGamepadSensorEnabled(SDL_Gamepad*, SDL_SensorType, bool) {
return true;
}
void CloseGamepad(SDL_Gamepad*) {}
void CloseGamepad(SDL_Gamepad* pad) {
closed_pads.push_back(static_cast<int>(reinterpret_cast<uintptr_t>(pad)));
}
void Delay(Uint32) {}
} // namespace Fake
@@ -368,6 +376,10 @@ void TestIndependentOutputsAndPadSwitch() {
"speaker volume resent controller effects");
Connect(2);
Disconnect(1);
Check(GetActiveControllerId() == HOST_INPUT_CONTROLLER_ID,
"disconnecting player 1 stole player 2's pad");
Disconnect(2);
Connect(2);
Check(GetActiveControllerId() == 2, "active controller did not change");
haptics.clear();
effects.clear();
@@ -394,6 +406,126 @@ void TestIndependentOutputsAndPadSwitch() {
}
}
void TestPlayerUserIds() {
Check(Config::IsConfiguredUserIdValid(Config::MAX_USER_ID) &&
!Config::IsConfiguredUserIdValid(Config::MAX_USER_ID + 1) &&
!Config::IsConfiguredUserIdValid(254) && !Config::IsConfiguredUserIdValid(255),
"configured user IDs did not reserve a valid range for guests");
Config::ConfigOptions options;
options.user_id = Config::MAX_USER_ID;
Config::Load(options);
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
Check(Config::GetPlayerFromUserId(Config::GetPlayerUserId(player)) == player,
"player user ID did not round-trip at the upper limit");
}
Check(Config::GetPlayerUserId(3) == std::numeric_limits<int32_t>::max() &&
Config::GetPlayerFromUserId(-1) == -1,
"guest user ID overflowed or accepted an unknown user");
Config::Load(Config::ConfigOptions {});
}
void TestMultiplayerRouting() {
Controller controller;
for (int player = 1; player < Config::MAX_PLAYERS; player++) {
Check(PadOpen(Config::GetPlayerUserId(player), 0, 0, nullptr) == player + 1,
"a guest could not open its own pad port");
}
for (int id = 2; id <= 5; id++) {
Connect(id);
}
Connect(2); // Duplicate device reports must not consume another port.
const auto read = [](int handle) {
PadData data {};
Check(PadReadState(handle, &data) == OK, "player pad read failed");
return data;
};
SetButton(1, PAD_BUTTON_CROSS, true);
SetButton(2, PAD_BUTTON_CIRCLE, true);
SetButton(5, PAD_BUTTON_TRIANGLE, true);
Check(read(1).buttons == PAD_BUTTON_CROSS && read(2).buttons == PAD_BUTTON_CIRCLE &&
read(3).buttons == 0 && read(4).buttons == 0,
"physical pads did not route independently or overflow reached a player");
SetRumble(100, 50);
CycleKeyboardPlayer(); // Dedicated player 1; physical pad 1 waits.
Check(GetActiveControllerId() == HOST_INPUT_CONTROLLER_ID &&
read(2).buttons == PAD_BUTTON_CIRCLE && pad_rumble[1].large == 0 &&
pad_rumble[1].small == 0,
"keyboard reassignment changed an unaffected player or left pad output "
"running");
for (int mode = 2; mode <= Config::MAX_PLAYERS; mode++) {
CycleKeyboardPlayer();
}
CycleKeyboardPlayer(); // Shared player 1; all original pads must return.
ResetInputState();
for (int id = 1; id <= 5; id++) {
SetButton(id, 1u << id, true);
}
for (int handle = 1; handle <= Config::MAX_PLAYERS; handle++) {
Check(read(handle).buttons == (1u << handle),
"cycling keyboard players lost or duplicated a connected pad");
}
// Moving the keyboard clears its held input from the old player.
SetButton(HOST_INPUT_CONTROLLER_ID, PAD_BUTTON_TRIANGLE, true);
CycleKeyboardPlayer();
CycleKeyboardPlayer();
SetButton(HOST_INPUT_CONTROLLER_ID, PAD_BUTTON_TRIANGLE, true);
CycleKeyboardPlayer(); // Keyboard now moves from player 2 to player 3.
Check((read(2).buttons & PAD_BUTTON_TRIANGLE) == 0,
"moving the keyboard retained a held button on its old player");
CycleKeyboardPlayer();
CycleKeyboardPlayer();
Disconnect(2);
SetButton(5, PAD_BUTTON_SQUARE, true);
Check(read(2).buttons == PAD_BUTTON_SQUARE && GetActiveControllerId() == 1,
"waiting pad did not fill a free slot or an unaffected pad moved");
Disconnect(99);
Check(PadCheckHandle(std::numeric_limits<int>::min()) == PAD_ERROR_INVALID_HANDLE &&
PadClose(std::numeric_limits<int>::max()) == PAD_ERROR_INVALID_HANDLE,
"an extreme handle aliased a player port");
closed_pads.clear();
EmergencyShutdown();
std::sort(closed_pads.begin(), closed_pads.end());
Check(closed_pads == std::vector<int> {1, 3, 4, 5},
"shutdown did not close each registered host pad exactly once");
}
void TestDedicatedKeyboardOutputs() {
Config::ConfigOptions options;
options.keyboard_player = 1;
options.controller_color = Config::ControllerColor {30, 40, 50};
Config::Load(options);
Initialize();
PadInit();
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
Check(PadOpen(Config::GetPlayerUserId(player), 0, 0, nullptr) == player + 1,
"dedicated keyboard port did not open");
}
Connect(1);
Connect(2);
const PadVibrationParam request {100, 50};
Check(PadSetVibration(3, &request) == OK, "player three vibration failed");
SetButton(2, PAD_BUTTON_CROSS, true);
CycleKeyboardPlayer(); // Pad 1 moves from player 2 to player 1.
PadData data {};
Check(GetActiveControllerId() == 1 && pad_colors[1] == *options.controller_color,
"moving a pad to an earlier slot cleared its configured lightbar");
Check(PadReadState(3, &data) == OK && data.buttons == PAD_BUTTON_CROSS &&
pad_rumble[2].large == 100 * 257 && pad_rumble[2].small == 50 * 257,
"keyboard reassignment reset an unaffected player's input or output");
haptics_handles_rumble = true;
SetRumble(200, 100);
for (int mode = 0; mode < 4; mode++) {
CycleKeyboardPlayer();
}
Check(GetActiveControllerId() == HOST_INPUT_CONTROLLER_ID && haptics.back().large == 0 &&
haptics.back().small == 0 && haptics.back().duration == 0,
"moving player one's pad retained its DualSense vibration request");
Shutdown();
Config::Load(Config::ConfigOptions {});
}
void TestTriggerEffectState() {
Controller controller;
PadTriggerEffectStateInformation info {{-1, -1}};
@@ -553,6 +685,7 @@ void TestTriggerEffectState() {
int main() {
Config::Initialize();
TestPlayerUserIds();
TestPadPortOwnership();
TestSettingCycles();
TestGlobalControllerLevels();
@@ -561,6 +694,8 @@ int main() {
TestMaskedTriggersAndValidation();
TestIndependentOutputsAndPadSwitch();
TestTriggerEffectState();
TestMultiplayerRouting();
TestDedicatedKeyboardOutputs();
Config::Shutdown();
return 0;
}
+87 -3
View File
@@ -235,6 +235,9 @@ void TestSysmoduleReferences() {
const auto unload = resolve("eR2bZFAAU0Q");
const auto is_loaded = resolve("fMP5NHUOaMk");
constexpr int unloaded = static_cast<int>(0x805a1001u);
Check(load(0x115) == Libs::LibKernel::KERNEL_ERROR_ENOENT && is_loaded(0x115) == unloaded &&
unload(0x115) == unloaded,
"missing packaged NpCppWebApi fails without recording a loaded module");
Check(load(0x113) == OK && is_loaded(0xb4) == unloaded,
"loading entitlement access does not mark AppContent loaded");
Check(unload(0x113) == OK, "release independent module reference");
@@ -255,6 +258,9 @@ void TestSysmoduleReferences() {
reinterpret_cast<int (KYTY_SYSV_ABI *)(uint16_t, int, int, int, int*)>(
internal_symbol->vaddr);
int result = -1;
Check(internal_load(0x115, 0, 0, 0, &result) == Libs::LibKernel::KERNEL_ERROR_ENOENT &&
result == Libs::LibKernel::KERNEL_ERROR_ENOENT && is_loaded(0x115) == unloaded,
"internal module loading propagates missing native module failure");
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");
@@ -407,7 +413,9 @@ void TestAddContent(const std::filesystem::path& root) {
Check(mount(0, nullptr, &point) == static_cast<int>(0x80d90002u) &&
list(0, nullptr, 0, nullptr) == static_cast<int>(0x817d0002u),
"null add-on parameters return parameter errors");
Check(mount(0, &info[3].label, &point) == OK, "mount owned add-on");
Check(mount(0, &info[3].label, &point) == OK &&
!std::string_view(point.data).ends_with('/'),
"mount owned add-on without trailing slash");
const std::string mounted_file = std::string(point.data) + "/payload.txt";
const int fd = FileSystem::KernelOpen(mounted_file.c_str(), 0, 0);
std::array<char, 11> payload {};
@@ -416,8 +424,9 @@ void TestAddContent(const std::filesystem::path& root) {
FileSystem::KernelClose(fd) == OK,
"guest reads files from the selected title's add-on");
MountPoint repeated {};
Check(mount(0, &info[3].label, &repeated) == static_cast<int>(0x80d90003u),
"duplicate mount returns BUSY");
Check(mount(0, &info[3].label, &repeated) == OK &&
std::strcmp(repeated.data, point.data) == 0,
"duplicate mount returns the same mount point");
Check(unmount(&point) == OK && FileSystem::GetRealFilename(mounted_file).empty() &&
unmount(&point) == static_cast<int>(0x80d90004u),
"unmount removes mapping and repeated unmount reports NOT_MOUNTED");
@@ -1896,6 +1905,11 @@ void CheckSocketWakeup() {
namespace Net = Libs::Network::Net;
Loader::SymbolDatabase symbols;
Libs::LibNet::InitNet_1_Net(&symbols);
Libs::InitLibKernel_1(&symbols);
const auto *shutdown_symbol = symbols.Find(
{"TUuiYS2kE8s", "Posix", 1, "libkernel", 1, 1, Loader::SymbolType::Func});
Check(shutdown_symbol != nullptr, "POSIX shutdown export resolves");
const auto shutdown = reinterpret_cast<int (KYTY_SYSV_ABI *)(int, int)>(shutdown_symbol->vaddr);
const auto *connect_symbol = symbols.Find(
{"OXXX4mUk3uk", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
const auto *getsockopt_symbol = symbols.Find(
@@ -2136,6 +2150,76 @@ void CheckSocketWakeup() {
CheckSocketReceiveBuffer(reader, writer);
CheckSocketReceiveConcurrency();
#endif
const auto [shutdown_reader, shutdown_writer] = CreateTcpPair();
const auto ready_to_read = [](int fd) {
std::array<uint64_t, 16> readable {};
readable[fd / 64] = uint64_t {1} << (fd % 64);
const std::array<int64_t, 2> timeout {2, 0};
return Net::Select(fd + 1, readable.data(), nullptr, nullptr, timeout.data()) == 1;
};
Check(shutdown(-1, 1) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF &&
shutdown(shutdown_writer, 3) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
"shutdown rejects invalid descriptors and directions");
Check(Net::Send(shutdown_writer, "x", 1, 0) == 1 && shutdown(shutdown_writer, 1) == 0 &&
ready_to_read(shutdown_reader) &&
Net::Recv(shutdown_reader, received.data(), 1, 0) == 1 && received[0] == 'x' &&
ready_to_read(shutdown_reader) &&
Net::Recv(shutdown_reader, received.data(), 1, 0) == 0 &&
Net::Send(shutdown_reader, "y", 1, 0) == 1 &&
ready_to_read(shutdown_writer) &&
Net::Recv(shutdown_writer, received.data(), 1, 0) == 1 && received[0] == 'y',
"write shutdown drains queued bytes, signals EOF, and preserves reverse traffic");
Check(Net::SocketClose(shutdown_reader) == 0 && Net::SocketClose(shutdown_writer) == 0,
"close half-shutdown sockets");
const int encrypted = Net::Socket(2, 6, 0);
int crypto = -1;
uint32_t crypto_size = sizeof(crypto);
Check(encrypted >= 0 &&
Net::Getsockopt(encrypted, 0xffff, 0x20000, &crypto, &crypto_size) == 0 &&
crypto == 0,
"UDPP2P encryption defaults to disabled");
for (const int setting : {7, 0}) {
Check(Net::Setsockopt(encrypted, 0xffff, 0x20000, &setting, sizeof(setting)) == 0 &&
Net::Getsockopt(encrypted, 0xffff, 0x20000, &crypto, &crypto_size) == 0 &&
crypto == (setting != 0),
"UDPP2P encryption configuration retains normalized enable and disable values");
Check(Net::Send(encrypted, "x", 1, 0) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EOPNOTSUPP,
"UDPP2P configuration never exposes unframed plaintext transport");
}
std::array<uint8_t, 16> p2p_address {16, 2, 0x0e, 0x4a, 127, 0, 0, 1, 0x1e, 0x61};
Check(Net::Bind(encrypted, p2p_address.data(), p2p_address.size()) == 0,
"bind UDPP2P physical and virtual ports");
char untouched = '!';
auto source = p2p_address;
uint32_t source_size = source.size();
Check(Net::Recvfrom(encrypted, &untouched, 1, 0x80, source.data(), &source_size) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK,
"per-call nonblocking UDPP2P receive reports an actually empty queue");
Check(Net::Setsockopt(encrypted, 0xffff, 0x1200, &enabled, sizeof(enabled)) == 0 &&
Net::Setsockopt(encrypted, 0xffff, 0x20000, &enabled, sizeof(enabled)) == 0 &&
Net::Recvfrom(encrypted, &untouched, 1, 0, source.data(), &source_size) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK &&
untouched == '!' && source == p2p_address && source_size == source.size(),
"empty encrypted nonblocking receive preserves all caller outputs");
const int raw_sender = Net::Socket(2, 2, 0);
Check(raw_sender >= 0 &&
Net::Sendto(raw_sender, "pending", 7, 0, p2p_address.data(), p2p_address.size()) == 7,
"queue a datagram requiring unsupported UDPP2P processing");
const auto pending_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
while (Net::Recv(encrypted, &untouched, 1, 0) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK &&
std::chrono::steady_clock::now() < pending_deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
Check(*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EOPNOTSUPP &&
Net::Recv(encrypted, &untouched, 1, 0) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EOPNOTSUPP && untouched == '!',
"queued packets remain queued and unsupported instead of masquerading as empty");
Check(Net::SocketClose(encrypted) == 0 && Net::SocketClose(raw_sender) == 0,
"close UDPP2P readiness fixtures");
Check(Net::SocketClose(writer) == 0, "close wake writer");
*net_errno = Libs::Posix::POSIX_EINVAL;
Check(net_recv(reader, received.data(), received.size(), 0) == 0 &&
+64 -26
View File
@@ -874,7 +874,7 @@ void TestFiniteImageRefreshReusesScalarReads() {
"finite image refresh grew reusable resource storage after warmup");
}
void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask) {
void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask, bool counted_loop = false) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
Program program;
@@ -898,54 +898,87 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask) {
block.AddBranch(&target);
if (other) block.AddBranch(other);
};
const auto lane = emit(entry, ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
const auto active = emit(entry, ValueOpcode::INotEqual32, {lane, Value(0u)});
const auto initial_mask = independent_mask
? emit(entry, ValueOpcode::IEqual32, {lane, Value(0u)}) : active;
const auto initial = emit(entry, ValueOpcode::SelectU32,
{initial_mask, Value(UINT32_MAX), Value(0u)});
Value lane, active, initial;
if (!counted_loop) {
lane = emit(entry, ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
active = emit(entry, ValueOpcode::INotEqual32, {lane, Value(0u)});
const auto initial_mask = independent_mask
? emit(entry, ValueOpcode::IEqual32, {lane, Value(0u)}) : active;
initial = emit(entry, ValueOpcode::SelectU32,
{initial_mask, Value(UINT32_MAX), Value(0u)});
}
program.memory_info.push_back({.kind = ResourceKind::ScalarAddress, .planning_only = true});
const auto address = emit(entry, ValueOpcode::GetAddressResource, {Value(0x1000u), Value(0u)});
const auto count = emit(entry, ValueOpcode::LoadAddressU32,
{address, Value(0u), Value(0u), Value(true)});
count.Instruction()->SetFlags(MemoryFlags{.index = 0});
const auto rejected = emit(entry, ValueOpcode::ULessThanEqual32, {count, lane});
const auto accepted = emit(entry, ValueOpcode::LogicalNot, {rejected});
branch(entry, loop, emit(entry, ValueOpcode::LogicalAnd, {active, accepted}), &merge);
if (counted_loop) {
branch(entry, loop);
} else {
const auto rejected = emit(entry, ValueOpcode::ULessThanEqual32, {count, lane});
const auto accepted = emit(entry, ValueOpcode::LogicalNot, {rejected});
branch(entry, loop, emit(entry, ValueOpcode::LogicalAnd, {active, accepted}), &merge);
}
// A nonempty traversal contains a varying loop-carried value. Its cycle must
// remain unknown; only the proven empty traversal can select the bypass arm.
auto &carried = loop.AppendNewInst(ValueOpcode::Phi);
carried.SetFlags(Type::U32);
const auto next = emit(body, ValueOpcode::IAdd32, {Value(&carried), Value(1u)});
carried.AddPhiOperand(&entry, lane);
carried.AddPhiOperand(&entry, counted_loop ? Value(0u) : lane);
carried.AddPhiOperand(&body, next);
branch(loop, body, emit(loop, ValueOpcode::INotEqual32, {Value(&carried), Value(UINT32_MAX)}), &merge);
const auto condition = counted_loop
? emit(loop, ValueOpcode::ULessThan32, {Value(&carried), count})
: emit(loop, ValueOpcode::INotEqual32, {Value(&carried), Value(UINT32_MAX)});
branch(loop, counted_loop ? use : body, condition, &merge);
loop.terminator.loop_header = true;
loop.terminator.continue_block = &body;
loop.terminator.merge_block = &merge;
branch(body, loop);
auto &selected = merge.AppendNewInst(ValueOpcode::Phi);
selected.SetFlags(Type::U32);
selected.AddPhiOperand(&entry, initial);
selected.AddPhiOperand(&loop, Value(&carried));
const auto valid = emit(merge, ValueOpcode::INotEqual32, {Value(&selected), Value(UINT32_MAX)});
branch(merge, use, emit(merge, ValueOpcode::LogicalAnd, {active, valid}), &exit);
branch(use, exit);
if (counted_loop) {
branch(merge, exit);
branch(use, body);
} else {
auto &selected = merge.AppendNewInst(ValueOpcode::Phi);
selected.SetFlags(Type::U32);
selected.AddPhiOperand(&entry, initial);
selected.AddPhiOperand(&loop, Value(&carried));
const auto valid = emit(merge, ValueOpcode::INotEqual32, {Value(&selected), Value(UINT32_MAX)});
branch(merge, use, emit(merge, ValueOpcode::LogicalAnd, {active, valid}), &exit);
branch(use, exit);
}
DescriptorSource source;
source.dwords = {Value(0x8000u), Value(1u << 16u), Value(16u), Value(3u << 28u)};
const auto descriptor_address = emit(use, ValueOpcode::GetAddressResource,
{Value(0x2000u), Value(0u)});
for (uint32_t i = 0; i < 4; ++i) {
const auto index = static_cast<uint32_t>(program.memory_info.size());
program.memory_info.push_back({.kind = ResourceKind::ScalarAddress, .planning_only = true});
source.dwords[i] = emit(use, ValueOpcode::LoadAddressU32,
{descriptor_address, Value(i * 4u), Value(0u), Value(true)});
source.dwords[i].Instruction()->SetFlags(MemoryFlags{.index = index});
}
source.dword_count = 4;
program.descriptor_sources.push_back(source);
program.info.buffers.push_back({.source = 0, .read = true});
const auto buffer_index = static_cast<uint32_t>(program.memory_info.size());
program.memory_info.push_back({.kind = ResourceKind::Buffer, .resource = 0});
const auto buffer = emit(use, ValueOpcode::GetBufferResource,
{source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]});
emit(use, ValueOpcode::LoadBufferU32, {buffer, Value(0u), Value(0u), Value(0u), Value(true)})
.Instruction()->SetFlags(MemoryFlags{.index = 1});
.Instruction()->SetFlags(MemoryFlags{.index = buffer_index});
auto plan = ExtractResourcePlan(program);
struct Reads { uint32_t count = 0; bool clean = true; } reads;
struct Reads { uint32_t count = 0; bool clean = true; uint32_t descriptors = 0; } reads;
const SrtRuntime runtime{
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
constexpr std::array<uint32_t, 4> descriptor{0x8000u, 1u << 16u, 16u,
(3u << 28u) | 0x4dfacu};
if (address < 0x2000u || address >= 0x2010u || address % 4u || words.size() != 1)
return false;
++static_cast<Reads *>(data)->descriptors;
words[0] = descriptor[(address - 0x2000u) / 4u];
return true;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
const auto &reads = *static_cast<Reads *>(data);
@@ -957,9 +990,11 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask) {
ResourceSpecialization specialization;
for (const auto state : {Reads{0u, true}, Reads{4u, true}, Reads{0u, false}, Reads{0u, true}}) {
reads = state;
uint32_t condition = 99;
Check(!SrtWalker(plan, runtime).Evaluate(plan.control_flow[3].condition, condition),
"direct merge evaluation used missing or previous runtime reachability");
if (!counted_loop) {
uint32_t condition = 99;
Check(!SrtWalker(plan, runtime).Evaluate(plan.control_flow[3].condition, condition),
"direct merge evaluation used missing or previous runtime reachability");
}
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"partial traversal proof rejected a valid resource snapshot");
const bool inactive = !independent_mask && reads.clean && reads.count == 0u;
@@ -967,6 +1002,8 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask) {
"empty traversal kept its descriptor or an uncertain traversal lost it");
Check(specialization.buffers[0].zero_stride_oob == inactive,
"inactive traversal did not expose dead vector reads to specialization");
Check(reads.descriptors == (inactive ? 0u : 4u),
"inactive traversal read its descriptor or an uncertain traversal skipped it");
if (inactive) {
Check(!snapshot.specialization_reads.empty(), "empty traversal lost its clean scalar read");
for (const auto &read : snapshot.specialization_reads)
@@ -1331,6 +1368,7 @@ int main() {
TestFiniteImageRefreshReusesScalarReads();
for (const bool independent_mask : {false, true})
TestEmptyTraversalSkipsUnmappedDescriptor(independent_mask);
TestEmptyTraversalSkipsUnmappedDescriptor(false, true);
TestCyclicControlPredicatesRemainUnknown();
TestEmptyScalarBufferSpecialization();
TestMixedSamplerVariantsShareRuntimeDescriptor();
+56 -1
View File
@@ -378,7 +378,7 @@ void TestClassicSavePaths() {
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
}
Reset("CLASSIC");
for (const char* text: {"", ".", "..", "../save1", "save/1", "save\\1"}) {
for (const char* text: {"", ".", "..", "../save1", "save/1", "save\\1", "[users]"}) {
auto name = DirName(text);
struct SaveDataMount3 mount {};
mount.user_id = 1;
@@ -396,6 +396,57 @@ void TestClassicSavePaths() {
CHECK(Search(1) == names);
}
void TestGuestSaveIsolation() {
Reset("MULTIUSER");
const auto name = DirName("slot");
const std::array<int32_t, 3> users {Config::GetUserId(), Config::GetPlayerUserId(1),
Config::GetPlayerUserId(2)};
std::array<fs::path, users.size()> paths;
for (size_t i = 0; i < users.size(); ++i) {
CHECK(Search(users[i]).empty());
struct SaveDataMount3 mount {};
mount.user_id = users[i];
mount.dir_name = &name;
mount.mount_mode = 4;
mount.blocks = 48;
SaveDataMountResult result {};
CHECK(SaveDataMount3(&mount, &result) == OK);
paths[i] = g_mounted_directory;
std::ofstream(paths[i] / "progress") << users[i];
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
CHECK(Search(users[i]) == std::vector<std::string> {"slot"});
}
CHECK(paths[0] == fs::path("_SaveData") / "MULTIUSER" / "slot");
CHECK(paths[0] != paths[1] && paths[1] != paths[2] && paths[0] != paths[2]);
SceSaveDataTitleId title {};
std::strcpy(title.data, "MULTIUSER");
Reset("OTHER");
for (size_t i = 0; i < users.size(); ++i) {
CHECK(Search(users[i]).empty());
CHECK(Search(users[i], &title) == std::vector<std::string> {"slot"});
struct SaveDataTransferringMount transfer {};
transfer.user_id = users[i];
transfer.title_id = &title;
transfer.dir_name = &name;
SaveDataMountResult result {};
CHECK(SaveDataTransferringMount(&transfer, &result) == OK);
CHECK(g_mounted_directory == paths[i]);
std::ifstream progress(paths[i] / "progress");
int saved_user = -1;
progress >> saved_user;
CHECK(saved_user == users[i]);
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
}
struct SaveDataDelete remove {};
remove.user_id = users[1];
remove.title_id = &title;
remove.dir_name = &name;
CHECK(SaveDataDelete(&remove) == OK);
CHECK(Search(users[1], &title).empty());
CHECK(Search(users[0], &title) == std::vector<std::string> {"slot"});
CHECK(Search(users[2], &title) == std::vector<std::string> {"slot"});
}
void TestSaveAllocations() {
Reset("CAPACITY");
std::array<SceSaveDataDirName, 2> names {DirName("Options"), DirName("Player")};
@@ -596,6 +647,7 @@ void RunChild(const fs::path& executable, const char* mode) {
} // namespace
int main(int argc, char** argv) {
Config::Initialize();
if (argc == 2) {
if (std::string_view(argv[1]) == "write") {
WriteRestartFixture();
@@ -603,6 +655,7 @@ int main(int argc, char** argv) {
CHECK(std::string_view(argv[1]) == "read");
ReadRestartFixture();
}
Config::Shutdown();
return 0;
}
const auto executable = fs::absolute(argv[0]);
@@ -617,10 +670,12 @@ int main(int argc, char** argv) {
TestIsolationAndSync();
TestFailedWritePreservesSave();
TestClassicSavePaths();
TestGuestSaveIsolation();
TestSaveAllocations();
TestClassicSaveParams();
CHECK(SaveDataTerminate() == OK);
fs::current_path(previous);
fs::remove_all(temp);
Config::Shutdown();
return 0;
}
+463 -44
View File
@@ -14672,7 +14672,7 @@ public:
phased_rendering.num_color_attachments == 0 &&
phased_rendering.depth_stencil_attachment.has_depth &&
phased_rendering.depth_stencil_attachment.depth_clear &&
phased_depth.depth_load_clear_enable &&
static_cast<bool>(phased_depth.load_clear_aspects & vk::ImageAspectFlagBits::eDepth) &&
texture_cache.IsMeta(phased_depth.desc.info.metadata.range.address) &&
!texture_cache.IsMetaCleared(
phased_depth.desc.info.metadata.range.address,
@@ -14709,6 +14709,8 @@ public:
RenderDepthInfo depth_only{};
RenderExecutorTestAccess::ResolveRenderDepthTarget(
executor, scheduler.Current(), depth_only);
(void)RenderExecutorTestAccess::AcquireRenderTargets(
executor, scheduler.Current(), &no_color, 0, depth_only);
Require(
name, "depth-only target with stale stencil state",
depth_only.image_id && depth_only.desc.view_info.format == vk::Format::eD32Sfloat &&
@@ -14853,7 +14855,7 @@ public:
bounds_depth.image_id == depth_only.image_id &&
bounds_depth.depth_bounds_test_enable &&
!bounds_depth.depth_write_enable &&
bounds_depth.depth_load_clear_enable == (pass == 0) &&
static_cast<bool>(bounds_depth.load_clear_aspects & vk::ImageAspectFlagBits::eDepth) == (pass == 0) &&
!texture_cache.IsMetaCleared(depth_only_htile_address, 0) &&
bounds_feedback == (pass == 0
? vk::ImageAspectFlagBits::eDepth
@@ -14932,7 +14934,7 @@ public:
executor, scheduler.Current(), &no_color, 0, acquired_high_slice);
Require(name, "high HTile slice clear consumption",
acquired_high_slice.image_id == high_slice.image_id &&
acquired_high_slice.depth_load_clear_enable == (pass == 0) &&
static_cast<bool>(acquired_high_slice.load_clear_aspects & vk::ImageAspectFlagBits::eDepth) == (pass == 0) &&
rendering.depth_stencil_attachment.depth_clear == (pass == 0) &&
!texture_cache.IsMetaCleared(high_slice_target.htile_data_base_addr, 86) &&
texture_cache.IsMetaCleared(high_slice_target.htile_data_base_addr, 85),
@@ -17912,7 +17914,7 @@ public:
registers.SetTargetOutputMode(1, 4);
stencil_target.z_info.htile_acceleration = true;
stencil_target.htile_data_base_addr = depth_address + 0x30000;
registers.SetDepthRenderTarget(stencil_target);
registers.SetStencilClearValue(5);
registers.SetDepthClearValue(1);
stencil_control = {};
stencil_control.stencil_enable = true;
@@ -17922,35 +17924,60 @@ public:
registers.SetStencilMask({0x80, 0, 0x80, 1, 0x80, 0, 0x80, 1});
user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriList);
shaders.SetEsShaderBase(vertex_address);
for (uint32_t discard = 0; discard < null_pixels.size(); discard++) {
const auto &code = null_pixels[discard];
const auto address = reinterpret_cast<uint64_t>(code.data());
ShaderMapUserData(address,
{.type = Prospero::ShaderBinaryType::kPs,
.code_size_bytes = static_cast<uint32_t>(code.size() * sizeof(u32))});
shaders.SetPsShaderBase(address);
RenderExecutorTestAccess::ResolveRenderDepthTarget(executor, scheduler.Current(), depth);
(void)cache.FindDepthTarget(depth.image_id, depth.desc);
vk::ClearValue zero{};
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), depth.image_id,
{vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil,
0, 1, 0, 1}, zero);
Require(name, "pending stencil-pass HTile clear",
cache.ClearMeta(stencil_target.htile_data_base_addr),
"the stencil pass did not retain its HTile metadata");
RenderExecutorTestAccess::DrawAuto(
executor, scheduler.Current(), {.vertex_count = 3, .instance_count = 1});
const auto depth_pixels = ReadCachedTexel(name, context, depth.image_id,
{}, {extent, extent, 1});
Require(name, "NULL-export full depth clear",
std::ranges::all_of(depth_pixels, [](u32 v) { return v == 0x3f800000u; }),
"an unexported color target clipped the pending depth clear");
const auto stencil = read_stencil();
const auto expected = discard == 0 ? 0x80808080u : 0u;
Require(name, "NULL-export stencil coverage and discard",
std::ranges::all_of(stencil, [=](u32 v) { return v == expected; }),
"a stale color target clipped stencil coverage or the NULL-export shader was skipped");
// Compressed metadata clears stencil; an uncompressed plane and an explicit
// depth-only clear preserve its previous contents. Match Suikoden's stale mask 2.
for (uint32_t clear_mode = 0; clear_mode < 3; clear_mode++) {
stencil_target.stencil_info.htile_stencil_disabled = clear_mode == 0;
registers.SetDepthRenderTarget(stencil_target);
registers.SetRenderControl({.depth_clear_enable = clear_mode == 2});
for (uint32_t discard = 0; discard < null_pixels.size(); discard++) {
const auto &code = null_pixels[discard];
const auto address = reinterpret_cast<uint64_t>(code.data());
ShaderMapUserData(address,
{.type = Prospero::ShaderBinaryType::kPs,
.code_size_bytes = static_cast<uint32_t>(code.size() * sizeof(u32))});
shaders.SetPsShaderBase(address);
RenderExecutorTestAccess::ResolveRenderDepthTarget(executor, scheduler.Current(), depth);
(void)cache.FindDepthTarget(depth.image_id, depth.desc);
vk::ClearValue zero{};
zero.depthStencil.stencil = 2;
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), depth.image_id,
{vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil,
0, 1, 0, 1}, zero);
Require(name, "pending stencil-pass HTile clear",
clear_mode == 2
? cache.TouchMeta(stencil_target.htile_data_base_addr, 0, false)
: cache.ClearMeta(stencil_target.htile_data_base_addr),
"the stencil pass did not retain its HTile metadata");
RenderExecutorTestAccess::DrawAuto(
executor, scheduler.Current(), {.vertex_count = 3, .instance_count = 1});
const auto depth_pixels = ReadCachedTexel(name, context, depth.image_id,
{}, {extent, extent, 1});
Require(name, "NULL-export full depth clear",
std::ranges::all_of(depth_pixels, [](u32 v) { return v == 0x3f800000u; }),
"an unexported color target clipped the pending depth clear");
const auto stencil = read_stencil();
const auto cleared = clear_mode == 1 ? 0x05050505u : 0x02020202u;
const auto expected = cleared | (discard == 0 ? 0x80808080u : 0u);
Require(name, "NULL-export stencil coverage and discard",
std::ranges::all_of(stencil, [=](u32 v) { return v == expected; }),
"a stale color target clipped stencil coverage or the NULL-export shader was skipped");
Require(name, "HTile clear consumed once",
!cache.IsMetaCleared(stencil_target.htile_data_base_addr, 0),
"the draw retained a pending metadata clear");
if (clear_mode == 1 && discard == 1) {
zero.depthStencil.stencil = 9;
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), depth.image_id,
{vk::ImageAspectFlagBits::eStencil, 0, 1, 0, 1}, zero);
RenderExecutorTestAccess::DrawAuto(
executor, scheduler.Current(), {.vertex_count = 3, .instance_count = 1});
Require(name, "consumed HTile clear preserves later stencil",
std::ranges::all_of(read_stencil(), [](u32 v) { return v == 0x09090909u; }),
"a consumed metadata clear ran again on the next draw");
}
}
}
registers.SetRenderControl({});
// A depth-only draw retains the previous PS address and write masks, but
// SPI_SHADER_COL_FORMAT disables its exports. Compiling this stale PS
@@ -20991,6 +21018,7 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode,
case Opcode::BUFFER_ATOMIC_SMAX:
case Opcode::BUFFER_ATOMIC_UMAX:
case Opcode::BUFFER_ATOMIC_AND:
case Opcode::BUFFER_ATOMIC_ADD_X2:
case Opcode::BUFFER_ATOMIC_AND_X2:
case Opcode::BUFFER_ATOMIC_OR:
case Opcode::BUFFER_ATOMIC_XOR:
@@ -21014,6 +21042,7 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode,
case Opcode::DS_ADD_U32:
case Opcode::DS_ADD_U64:
case Opcode::DS_MAX_RTN_U64:
case Opcode::DS_CMPST_RTN_B32:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_ADD_RTN_U32:
case Opcode::DS_SUB_U32:
@@ -24667,6 +24696,56 @@ TestCase Vop3MulLoU16CapturedAndSelectors() {
return test;
}
TestCase Vop3F32ClampFlushesNaN() {
using O = ShaderOpcode;
struct ClampCase {
u32 opcode, input, expected;
u32 abs = 0;
};
constexpr u32 inf = 0x7f800000u, one = 0x3f800000u;
// GoW Ragnarok PS 3d52c7172f95fd37 0x2564: v_mul_f32 v18 clamp, inf, |v2|; and
// fma(x, inf, inf) clamp. The clamp modifier (DX10 clamp mode) turns their NaN into 0.
const std::array<ClampCase, 10> cases{{
{0x108, 0x00000000u, 0, 2}, // inf * |0| = NaN.
{0x108, 0x80000000u, 0, 2}, // inf * |-0| = NaN.
{0x108, 0x3f000000u, one, 2},
{0x108, 0xc0400000u, one, 2},
{0x14b, 0xbf800000u, 0}, // -1 * inf + inf = NaN.
{0x14b, 0x40000000u, one},
{0x103, 0x7fc00000u, 0}, // Quiet NaN input.
{0x103, 0xffc00001u, 0}, // Negative NaN input.
{0x103, 0x3e800000u, 0x3e800000u},
{0x103, 0x3fc00000u, one},
}};
TestCase test;
test.name = "Vop3F32ClampFlushesNaN";
for (const auto &entry : cases) test.initial.push_back(entry.input);
test.expected = test.initial;
auto &code = test.code;
AppendVMovLiteral(&code, 20, inf);
AppendVMovU32(&code, 21, 0);
for (u32 i = 0; i < cases.size(); ++i) {
const auto &entry = cases[i];
AppendVMovU32(&code, 30, i * 4u);
AppendBufferLoadDword(&code, 2, 30); // Runtime inputs prevent folding.
AppendVMovLiteral(&code, 18, 0xa5a5beefu);
if (entry.opcode == 0x108) {
AppendVop3(&code, 0x108, 18, Vgpr(20), Vgpr(2), 0, entry.abs, 0, true);
} else if (entry.opcode == 0x14b) {
AppendVop3(&code, 0x14b, 18, Vgpr(2), Vgpr(20), Vgpr(20), 0, 0, true);
} else {
AppendVop3(&code, 0x103, 18, Vgpr(2), Vgpr(21), 0, 0, 0, true);
}
AppendStoreVgpr(&code, 18, static_cast<u32>(cases.size()) + i);
test.expected.push_back(entry.expected);
}
AppendEnd(&code);
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_MUL_F32, O::V_FMA_F32,
O::V_ADD_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.decoded_counts = {{"V_MUL_F32", 4}, {"V_FMA_F32", 2}, {"V_ADD_F32", 4}};
return test;
}
TestCase Vop3MadI16CapturedSelectorsAndSaturation() {
using O = ShaderOpcode;
struct MadCase {
@@ -25140,6 +25219,34 @@ TestCase VectorCarryAndBitCountOps() {
O::S_ENDPGM}};
}
TestCase ScalarVector64BitFloatInlineConstants() {
using O = ShaderOpcode;
std::vector<u32> code;
code.push_back(EncodeSop1(0x04, 40, 242));
code.push_back(EncodeSop2(0x11, 42, 240, InlineU32(0)));
code.push_back(EncodeSop1(0x04, 44, 248));
AppendVop3(&code, 0x300, 2, InlineU32(32), 244);
code.push_back(EncodeSMovB32(46, InlineU32(0)));
AppendSMovLiteral(&code, 47, 0x3ff00000u);
code.push_back(EncodeSopc(0x12, 46, 242));
code.push_back(EncodeSMovB32(48, 253));
AppendStoreSgprPair(&code, 40, 0);
AppendStoreSgprPair(&code, 42, 2);
AppendStoreVgpr(&code, 2, 4);
AppendStoreVgpr(&code, 3, 5);
AppendStoreSgprPair(&code, 44, 6);
AppendStoreSgpr(&code, 48, 8);
AppendEnd(&code);
TestCase test;
test.name = "ScalarVector64BitFloatInlineConstants";
test.code = std::move(code);
test.expected = {0u, 0x3ff00000u, 0u, 0x3fe00000u, 0x40000000u, 0u, 0x6dc9c882u, 0x3fc45f30u, 1u};
test.opcodes = {O::S_MOV_B64, O::S_OR_B64, O::V_LSHRREV_B64, O::S_MOV_B32, O::S_CMP_EQ_U64,
O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
return test;
}
TestCase VectorMbcntUsesThreadMask() {
using O = ShaderOpcode;
@@ -25770,6 +25877,49 @@ TestCase CvtPkrtzF16F32SubnormalRoundsTowardZero() {
O::S_ENDPGM}};
}
TestCase CvtPkrtzF16F32RuntimeEdges() {
using O = ShaderOpcode;
// Independent expected half encodings, including truncation boundaries and NaN payloads.
constexpr std::array<std::array<u32, 2>, 31> inputs{{
{0x00000000u, 0x0000u}, {0x80000000u, 0x8000u},
{0x00000001u, 0x0000u}, {0x007fffffu, 0x0000u},
{0x00800000u, 0x0000u}, {0x32ffffffu, 0x0000u},
{0x33000000u, 0x0000u}, {0x337fffffu, 0x0000u},
{0x33800000u, 0x0001u}, {0x33c00000u, 0x0001u},
{0x34000000u, 0x0002u}, {0x387fc000u, 0x03ffu},
{0x387fe000u, 0x03ffu}, {0x387fffffu, 0x03ffu},
{0x38800000u, 0x0400u}, {0x38802000u, 0x0401u},
{0x3e22f983u, 0x3117u}, {0x3f800000u, 0x3c00u},
{0x3f801000u, 0x3c00u}, {0x3f801001u, 0x3c00u},
{0x3f802000u, 0x3c01u}, {0x477fe000u, 0x7bffu},
{0x477ff000u, 0x7bffu}, {0x477fffffu, 0x7bffu},
{0x47800000u, 0x7bffu}, {0x7f7fffffu, 0x7bffu},
{0x7f800000u, 0x7c00u}, {0x7f800001u, 0x7e00u},
{0x7fa02000u, 0x7f01u}, {0x7fc12000u, 0x7e09u},
{0x7fffffffu, 0x7fffu},
}};
TestCase test;
test.name = "CvtPkrtzF16F32RuntimeEdges";
for (const auto &input : inputs)
test.initial.insert(test.initial.end(), {input[0], input[0] ^ 0x80000000u});
test.expected = test.initial;
for (u32 i = 0; i < inputs.size(); ++i) {
for (u32 reg = 0; reg < 2; ++reg) {
AppendVMovU32(&test.code, 30, (i * 2u + reg) * 4u);
AppendBufferLoadDword(&test.code, reg, 30);
}
test.code.push_back(EncodeVop2(0x2f, 2, Vgpr(0), 1));
AppendStoreVgpr(&test.code, 2, static_cast<u32>(test.expected.size()));
const auto half = inputs[i][1];
test.expected.push_back(half | ((half ^ 0x8000u) << 16));
}
AppendEnd(&test.code);
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_CVT_PKRTZ_F16_F32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.forbidden_spirv = {"PackHalf2x16", "UnpackHalf2x16"};
return test;
}
TestCase CvtPkrtzF16F32SdwaAndOutputModifiers() {
using O = ShaderOpcode;
@@ -25828,13 +25978,17 @@ TestCase PackedMinMaxF16NanAndSignedZeroEdges() {
AppendVMovLiteral(&code, 3, 0x80007e00u);
AppendVMovLiteral(&code, 4, 0x40007d01u);
AppendVMovLiteral(&code, 5, 0x7d014000u);
AppendVMovLiteral(&code, 6, 0x40007e01u);
AppendVMovLiteral(&code, 7, 0x7e013c00u);
AppendVop3p(&code, 0x11, 10, Vgpr(0), Vgpr(1), 0, 0x3);
AppendVop3p(&code, 0x12, 11, Vgpr(2), Vgpr(3), 0, 0x3);
AppendVop3p(&code, 0x11, 12, Vgpr(4), Vgpr(5), 0, 0x3);
AppendVop3p(&code, 0x12, 13, Vgpr(4), Vgpr(5), 0, 0x3);
AppendVop3p(&code, 0x11, 14, Vgpr(6), Vgpr(7), 0, 0x3);
AppendVop3p(&code, 0x12, 15, Vgpr(6), Vgpr(7), 0, 0x3);
for (u32 i = 0; i < 4; i++) {
for (u32 i = 0; i < 6; i++) {
AppendStoreVgpr(&code, 10 + i, i);
}
AppendEnd(&code);
@@ -25842,7 +25996,7 @@ TestCase PackedMinMaxF16NanAndSignedZeroEdges() {
return {"PackedMinMaxF16NanAndSignedZeroEdges",
code,
{},
{0x80004000u, 0x00004000u, 0x7f017f01u, 0x7f017f01u},
{0x80004000u, 0x00004000u, 0x7f017f01u, 0x7f017f01u, 0x40003c00u, 0x40003c00u},
{O::V_MOV_B32, O::V_PK_MIN_F16, O::V_PK_MAX_F16,
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
}
@@ -31002,6 +31156,69 @@ TestCase BufferLoadDwordx2SnapshotsOverlappingAddress() {
return test;
}
TestCase CoherentBufferLoadDwordx2Bounds(u32 wave, u32 prefix) {
using O = ShaderOpcode;
TestCase test;
test.name = wave == 64 ? (prefix ? "CoherentBufferPairBoundsOffsetWave64"
: "CoherentBufferPairBoundsWave64")
: (prefix ? "CoherentBufferPairBoundsOffsetWave32"
: "CoherentBufferPairBoundsWave32");
test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u};
test.initial.resize(12);
test.expected = test.initial;
const u32 addresses[] = {0u, 4u, 8u, 0xfffffffcu};
const u32 base = prefix / 4;
const u32 values[][2] = {{test.initial[base], test.initial[base + 1]},
{test.initial[base + 1], test.initial[base + 2]},
{test.initial[base + 2], 0u},
{0u, test.initial[base]}};
for (u32 i = 0; i < std::size(addresses); ++i) {
AppendVMovLiteral(&test.code, 20, addresses[i]);
test.code.push_back(EncodeMubuf0(0x0d, 0, false, true, true) | (1u << 15u));
test.code.push_back(EncodeMubuf1(2, 0, 20));
for (u32 component = 0; component < 2; ++component) {
AppendStoreVgpr(&test.code, 2 + component, 4 + i * 2 + component);
test.expected[4 + i * 2 + component] = values[i][component];
}
}
AppendEnd(&test.code);
test.user_data = MakeStructuredStorageBufferData(0, 12);
test.has_user_data = true;
test.storage_buffer_offsets = {prefix};
test.storage_buffer_ranges = {{0u, 12u + prefix}};
test.required_spirv = {"OpAtomicLoad %ulong", "Int64Atomics",
"ArrayStride 8"};
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORDX2, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.compute_info.wave_size = wave;
test.has_compute_info = true;
return test;
}
TestCase CoherentBufferLoadDwordx2Swizzled() {
using O = ShaderOpcode;
TestCase test;
test.name = "CoherentBufferLoadDwordx2Swizzled";
AppendVMovU32(&test.code, 20, 0);
test.code.push_back(EncodeMubuf0(0x0d, 0, true, false, true) | (1u << 15u));
test.code.push_back(EncodeMubuf1(2, 0, 20));
AppendStoreVgpr(&test.code, 2, 9);
AppendStoreVgpr(&test.code, 3, 10);
AppendEnd(&test.code);
test.initial = {0x11111111u, 0x22222222u, 0, 0, 0, 0,
0, 0, 0x33333333u, 0, 0};
test.expected = test.initial;
test.expected[9] = test.initial[0];
test.expected[10] =
test.initial[8]; // Four-byte elements span eight-index groups.
test.user_data = MakeStructuredStorageBufferData(8, 8);
test.user_data[1] |= 1u << 31u;
test.has_user_data = true;
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORDX2, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
return test;
}
TestCase BufferLoadDwordx3SnapshotsOverlappingAddress() {
using O = ShaderOpcode;
@@ -34647,6 +34864,60 @@ TestCase DsAtomicReturn64() {
return test;
}
TestCase DsCompareStoreReturn32() {
using O = ShaderOpcode;
TestCase test;
test.name = "DsCompareStoreReturn32";
auto &code = test.code;
const auto result = [&](u32 reg, u32 value) {
AppendStoreVgpr(&code, reg, static_cast<u32>(test.expected.size()));
test.expected.push_back(value);
};
struct Case { u32 before, compare, replacement; };
constexpr std::array cases{Case{0x12345678u, 0x12345678u, 0x9abcdef0u},
Case{0x12345678u, 0x9abcdef0u, 0x12345678u},
Case{0x80000000u, 0u, 0x7fffffffu},
Case{UINT32_MAX, UINT32_MAX, 0u}};
AppendVMovU32(&code, 7, 0);
for (const auto &item : cases) {
AppendVMovLiteral(&code, 2, item.before);
code.push_back(EncodeDs0(0x0d, 8));
code.push_back(EncodeDs1(0, 2, 7));
AppendVMovLiteral(&code, 5, item.compare);
AppendVMovLiteral(&code, 19, item.replacement);
AppendVMovU32(&code, 4, 8);
// CS04b812daaacd532f pc 0x3e8: DATA compares, DATA2 replaces, VDst aliases ADDR.
code.insert(code.end(), {0xd8c00000u, 0x04130504u});
result(4, item.before);
code.push_back(EncodeDs0(0x36, 8));
code.push_back(EncodeDs1(8, 0, 7));
result(8, item.before == item.compare ? item.replacement : item.before);
}
AppendVMovU32(&code, 5, 0);
AppendVMovU32(&code, 4, 12); // Past the three LDS DWORDs.
code.insert(code.end(), {0xd8c00000u, 0x04130504u});
result(4, 0);
AppendVMovLiteral(&code, 2, 0x12345678u);
code.push_back(EncodeDs0(0x0d, 8));
code.push_back(EncodeDs1(0, 2, 7));
AppendVMovLiteral(&code, 5, 0x12345678u);
AppendVMovU32(&code, 4, 8);
code.push_back(EncodeSMovB32(126, InlineU32(0)));
code.insert(code.end(), {0xd8c00000u, 0x04130504u});
code.push_back(EncodeSMovB32(126, InlineU32(1)));
result(4, 8); // An inactive lane keeps its destination and leaves memory alone.
code.push_back(EncodeDs0(0x36, 8));
code.push_back(EncodeDs1(8, 0, 7));
result(8, 0x12345678u);
AppendEnd(&code);
test.compute_info.lds_size_dwords = 3;
test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::DS_WRITE_B32, O::DS_CMPST_RTN_B32,
O::DS_READ_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpAtomicCompareExchange %uint"};
test.forbidden_spirv = {"OpLoopMerge"};
return test;
}
TestCase DsAtomic64Contention(bool add, u32 wave_size) {
using O = ShaderOpcode;
TestCase test;
@@ -35497,6 +35768,65 @@ TestCase Wave64AppendConsumeHighHalf() {
return test;
}
TestCase CoherentBufferPairContention(u32 wave_size) {
using O = ShaderOpcode;
TestCase test;
test.name = wave_size == 64 ? "CoherentBufferPairContentionWave64"
: "CoherentBufferPairContentionWave32";
auto &code = test.code;
AppendVMovU32(&code, 20, 0);
AppendVMovU32(&code, 6, 0);
AppendSMovLiteral(&code, 20, 128);
const auto loop = code.size();
code.push_back(EncodeVop2(0x25, 2, InlineU32(1), 0));
code.push_back(
EncodeVop1(0x37, 3, Vgpr(2))); // v_not_b32: correlated high word.
AppendBufferStoreOpcode(&code, 0x50, 2, 20);
code.push_back(EncodeMubuf0(0x0d, 0, false, true, true) | (1u << 15u));
code.push_back(EncodeMubuf1(4, 12, 20));
code.push_back(EncodeVop2(0x1d, 4, Vgpr(4), 5));
code.push_back(EncodeVop1(0x37, 4, Vgpr(4)));
code.push_back(EncodeVop2(0x1c, 6, Vgpr(4), 6));
code.push_back(EncodeSop2(0x01, 20, 20, InlineU32(1)));
code.push_back(EncodeSopc(0x07, 20, InlineU32(0)));
code.push_back(EncodeSopp(0x05, static_cast<u32>(loop - code.size() - 1)));
AppendStoreVgprAtLaneDwordOffset(&code, 6, 0, 2);
code.push_back(EncodeSopp(0x0a, 0));
// Restore a deterministic mailbox after all readers finish.
code.push_back(EncodeSop1(0x04, 126, InlineU32(1)));
AppendVMovU32(&code, 2, 0);
AppendVMovLiteral(&code, 3, 0xffffffffu);
AppendBufferStoreOpcode(&code, 0x50, 2, 20);
AppendEnd(&code);
test.initial.assign(wave_size * 2 + 2, 0);
test.initial[1] = 0xffffffffu;
test.expected = test.initial;
test.required_spirv = {"OpAtomicExchange %ulong", "OpAtomicLoad %ulong",
"Int64Atomics", "OpLoopMerge"};
test.opcodes = {O::V_MOV_B32,
O::S_MOV_B32,
O::S_MOV_B64,
O::V_ADD_NC_U32,
O::V_NOT_B32,
O::V_XOR_B32,
O::V_OR_B32,
O::BUFFER_ATOMIC_SWAP_X2,
O::BUFFER_LOAD_DWORDX2,
O::S_SUB_U32,
O::S_CMP_LG_U32,
O::S_CBRANCH_SCC1,
O::V_LSHLREV_B32,
O::BUFFER_STORE_DWORD,
O::S_BARRIER,
O::S_ENDPGM};
test.compute_info.threads_num[0] = wave_size * 2;
test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1;
test.compute_info.thread_ids_num = 1;
test.compute_info.wave_size = wave_size;
test.has_compute_info = true;
return test;
}
TestCase BufferWorkgroupPublication(u32 wave_size, bool dlc_only = false,
bool store_completion = false) {
using O = ShaderOpcode;
@@ -35662,6 +35992,40 @@ TestCase BufferAtomicAndX2GlcAndExec() {
return test;
}
TestCase BufferAtomicAddX2GlcAndExec() {
using O = ShaderOpcode;
TestCase test;
test.name = "BufferAtomicAddX2GlcAndExec";
auto &code = test.code;
code.push_back(EncodeSop1(0x04, 12, 126));
AppendVMovU32(&code, 20, 0);
const u32 values[][2] = {{0x0ff00ff0u, 0xf00ff00fu},
{0xff00ff00u, 0x00ff00ffu},
{0x76543210u, 0xfedcba98u}};
for (u32 i = 0; i < std::size(values); ++i) {
AppendVMovLiteral(&code, 2, values[i][0]);
AppendVMovLiteral(&code, 3, values[i][1]);
if (i == 2) {
code.push_back(EncodeSop1(0x04, 126, InlineU32(0)));
}
AppendBufferStoreOpcode(&code, 0x52, 2, 20, i != 1);
code.push_back(EncodeSop1(0x04, 126, 12));
AppendStoreVgpr(&code, 2, 2 + i * 2);
AppendStoreVgpr(&code, 3, 3 + i * 2);
}
AppendEnd(&code);
test.initial = {0x12345678u, 0x87654321u, 0, 0, 0, 0, 0, 0};
// The first add wraps at 64 bits; the second carries from the low dword.
// GLC returns the first old value, preserves the second operand, and EXEC = 0
// suppresses the third add and its writeback despite its nonzero operand.
test.expected = {0x21256568u, 0x78743430u, 0x12345678u, 0x87654321u,
0xff00ff00u, 0x00ff00ffu, 0x76543210u, 0xfedcba98u};
test.opcodes = {O::V_MOV_B32, O::S_MOV_B64, O::BUFFER_ATOMIC_ADD_X2,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpAtomicIAdd %ulong", "Int64Atomics"};
return test;
}
TestCase BufferAtomicCmpSwapExactRaw() {
using O = ShaderOpcode;
@@ -41862,6 +42226,7 @@ std::vector<TestCase> MakeCases() {
AddCase(Vop3CvtPkI16I32Captured);
AddCase(Vop3MulLoU16CapturedAndSelectors);
AddCase(Vop3MadI16CapturedSelectorsAndSaturation);
AddCase(Vop3F32ClampFlushesNaN);
AddCase(Vop3Min3U16CapturedAndSelectors);
AddCase(Vop3Med3I16Captured);
AddCase(Vop2SdwaMinU32PreservesWordDestination);
@@ -41872,6 +42237,7 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorBfeI32SignExtendsField);
AddCase(VectorAlignByteUsesTwoBitByteOffset);
AddCase(VectorCarryAndBitCountOps);
AddCase(ScalarVector64BitFloatInlineConstants);
AddCase(VectorMbcntUsesThreadMask);
AddCase(VectorAddcWritesPerLaneCarryOut);
AddCase(VectorAddcUsesPerLaneCarryIn);
@@ -41890,6 +42256,7 @@ std::vector<TestCase> MakeCases() {
AddCase(Vop3pInt16Ops);
AddCase(CvtPkU8F32PacksSelectedByte);
AddCase(CvtPkrtzF16F32SubnormalRoundsTowardZero);
AddCase(CvtPkrtzF16F32RuntimeEdges);
AddCase(CvtPkrtzF16F32SdwaAndOutputModifiers);
AddCase(PackedMinMaxF16NanAndSignedZeroEdges);
AddCase(VectorMinMaxF16Ops);
@@ -42047,6 +42414,12 @@ std::vector<TestCase> MakeCases() {
cases.push_back(BufferSubwordStoresAtHostOffset(2));
cases.push_back(BufferSubwordStoresAtHostOffset(1));
AddCase(BufferLoadDwordx2SnapshotsOverlappingAddress);
for (const u32 wave : {32u, 64u}) {
for (const u32 prefix : {0u, 4u})
cases.push_back(CoherentBufferLoadDwordx2Bounds(wave, prefix));
cases.push_back(CoherentBufferPairContention(wave));
}
AddCase(CoherentBufferLoadDwordx2Swizzled);
AddCase(BufferLoadDwordx3SnapshotsOverlappingAddress);
AddCase(BufferLoadDwordx4SnapshotsOverlappingAddress);
AddCase(BufferLoadDwordx4ZeroesOnlyOutOfBoundsTail);
@@ -42190,6 +42563,7 @@ std::vector<TestCase> MakeCases() {
}
AddCase(DsAtomicReturnVariants);
AddCase(DsAtomicReturn64);
AddCase(DsCompareStoreReturn32);
for (bool add : {false, true}) {
cases.push_back(DsAtomic64Bounds(add));
for (u32 wave_size : {32, 64}) {
@@ -42197,6 +42571,7 @@ std::vector<TestCase> MakeCases() {
}
}
AddCase(BufferAtomicAndX2GlcAndExec);
AddCase(BufferAtomicAddX2GlcAndExec);
AddCase(ImageAtomicSignedMinMax<false>);
AddCase(ImageAtomicSignedMinMax<true>);
AddCase(ImageAtomicSignedMinMax<false, true>);
@@ -42553,6 +42928,30 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
!ResolveComputeBufferFill(compute, 2, 1, 1, 0x41u, descriptor,
packed_clear, size),
"excess invocation coverage was accepted as a fill");
runtime_resources.uniform_fill.bounded_elements = 65;
runtime_resources.buffers[0].dwords[2] = 65;
for (u32 oob = 0; oob < 4; ++oob) {
runtime_resources.buffers[0].dwords[3] = 0x14204u | (oob << 28u);
Require("Ps5GameExampleImageClear", "bounded final workgroup and RDNA2 OOB mode",
ResolveComputeBufferFill(compute, 2, 1, 1, 0x41u, descriptor,
packed_clear, size) == (oob != 3),
"explicit coverage did not respect RDNA2 element bounds");
}
runtime_resources.buffers[0].dwords[3] = 0x14204u;
Require("Ps5GameExampleImageClear", "bounded insufficient dispatch",
!ResolveComputeBufferFill(compute, 1, 1, 1, 0x41u, descriptor,
packed_clear, size),
"a bound was mistaken for actual invocation coverage");
Require("Ps5GameExampleImageClear", "bounded index wrap",
!ResolveComputeBufferFill(compute, 0x04000001u, 1, 1, 0x41u, descriptor,
packed_clear, size),
"a wrapped invocation index was accepted as complete coverage");
runtime_resources.uniform_fill.bounded_elements = 64;
Require("Ps5GameExampleImageClear", "bounded incomplete destination",
!ResolveComputeBufferFill(compute, 2, 1, 1, 0x41u, descriptor,
packed_clear, size),
"a shader bound smaller than the destination was accepted as a fill");
runtime_resources = scalar;
auto alias = scalar.buffers.front();
runtime_resources.buffers.push_back(alias);
Require("Ps5GameExampleImageClear", "aliased scalar input",
@@ -45375,12 +45774,12 @@ void CheckDepthAttachmentWrites() {
target.depth_test_enable = false;
target.depth_write_enable = false;
target.depth_load_clear_enable = true;
target.load_clear_aspects = vk::ImageAspectFlagBits::eDepth;
Require("DepthAttachmentWrites", "depth clear",
target.AttachmentWriteAspects() == vk::ImageAspectFlagBits::eDepth,
"depth attachment clear did not claim depth");
target.depth_load_clear_enable = false;
target.load_clear_aspects = {};
target.stencil_test_enable = true;
target.stencil_front = {vk::StencilOp::eKeep, vk::StencilOp::eKeep,
vk::StencilOp::eKeep, vk::CompareOp::eAlways, 0xff, 0xff, 0};
@@ -45406,7 +45805,7 @@ void CheckDepthAttachmentWrites() {
"masked front write or KEEP-only back face claimed stencil");
target.stencil_test_enable = false;
target.stencil_clear_enable = true;
target.load_clear_aspects = vk::ImageAspectFlagBits::eStencil;
Require("DepthAttachmentWrites", "stencil clear",
target.AttachmentWriteAspects() == vk::ImageAspectFlagBits::eStencil,
"stencil attachment clear did not claim stencil");
@@ -45480,12 +45879,11 @@ void CheckDepthFeedbackAspects() {
feedback(sampled) == vk::ImageAspectFlagBits::eDepth,
"overlapping mip and layer boundaries missed depth feedback");
target.depth_write_enable = false;
target.depth_load_clear_enable = true;
target.load_clear_aspects = vk::ImageAspectFlagBits::eDepth;
Require("DepthFeedbackAspects", "depth clear only",
feedback(sampled) == vk::ImageAspectFlagBits::eDepth,
"a sampled depth attachment clear missed feedback");
target.depth_load_clear_enable = false;
target.stencil_clear_enable = true;
target.load_clear_aspects = vk::ImageAspectFlagBits::eStencil;
sampled.aspect = vk::ImageAspectFlagBits::eStencil;
Require("DepthFeedbackAspects", "stencil clear only",
feedback(sampled) == vk::ImageAspectFlagBits::eStencil,
@@ -45539,7 +45937,7 @@ void CheckDynamicRenderingState() {
depth_attachment_layout(attachment) ==
vk::ImageLayout::eDepthStencilReadOnlyOptimal,
"fully read-only depth/stencil used a writable layout");
attachment.depth_load_clear_enable = true;
attachment.load_clear_aspects = vk::ImageAspectFlagBits::eDepth;
Require("DynamicRenderingState", "deferred depth clear layout",
depth_attachment_layout(attachment) ==
vk::ImageLayout::eDepthAttachmentStencilReadOnlyOptimal,
@@ -45550,9 +45948,9 @@ void CheckDynamicRenderingState() {
depth_attachment_layout(attachment) ==
vk::ImageLayout::eDepthAttachmentStencilReadOnlyOptimal,
"depth-only writes did not retain read-only stencil");
attachment.depth_load_clear_enable = false;
attachment.load_clear_aspects = {};
attachment.depth_write_enable = false;
attachment.stencil_clear_enable = true;
attachment.load_clear_aspects = vk::ImageAspectFlagBits::eStencil;
Require("DynamicRenderingState", "depth-read stencil-write layout",
depth_attachment_layout(attachment) ==
vk::ImageLayout::eDepthReadOnlyStencilAttachmentOptimal,
@@ -47675,6 +48073,13 @@ int main(int argc, char **argv) {
std::setvbuf(stdout, nullptr, _IONBF, 0);
EnsureConfigInitialized();
CheckLeastRecentlyUsedCacheOrdering();
if (argc == 2 && std::strcmp(argv[1], "--cvt-pkrtz-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, CvtPkrtzF16F32SubnormalRoundsTowardZero());
RunCase(&vulkan, CvtPkrtzF16F32RuntimeEdges());
RunCase(&vulkan, CvtPkrtzF16F32SdwaAndOutputModifiers());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--thread-dimensions-only") == 0) {
VulkanHarness vulkan;
CheckComputeThreadDimensions(vulkan);
@@ -47689,7 +48094,8 @@ int main(int argc, char **argv) {
RunCase(&vulkan, DsWideLdsPartialBounds());
RunCase(&vulkan, DsAtomic64Bounds(false));
RunCase(&vulkan, DsAtomic64Bounds(true));
for (auto test : {DsWideLdsPartialBounds(), DsAtomic64Bounds(false), DsAtomic64Bounds(true), DsAtomicReturn64()}) {
for (auto test : {DsWideLdsPartialBounds(), DsAtomic64Bounds(false), DsAtomic64Bounds(true), DsAtomicReturn64(),
DsCompareStoreReturn32()}) {
test.compute_info.lds_storage = true;
std::erase(test.required_spirv, "WorkgroupMemoryExplicitLayoutKHR");
test.required_spirv.push_back("StorageBuffer");
@@ -47735,6 +48141,7 @@ int main(int argc, char **argv) {
}
RunCase(&vulkan, DsAtomicReturn64());
RunCase(&vulkan, BufferAtomicAndX2GlcAndExec());
RunCase(&vulkan, BufferAtomicAddX2GlcAndExec());
RunCase(&vulkan, ImageAtomicSignedMinMax<false>());
RunCase(&vulkan, ImageAtomicSignedMinMax<true>());
return 0;
@@ -48212,6 +48619,12 @@ int main(int argc, char **argv) {
}
if (argc == 2 && std::strcmp(argv[1], "--buffer-publication-only") == 0) {
VulkanHarness vulkan;
for (const u32 wave : {32u, 64u}) {
for (const u32 prefix : {0u, 4u})
RunCase(&vulkan, CoherentBufferLoadDwordx2Bounds(wave, prefix));
RunCase(&vulkan, CoherentBufferPairContention(wave));
}
RunCase(&vulkan, CoherentBufferLoadDwordx2Swizzled());
RunCase(&vulkan, BufferWorkgroupPublication(32));
RunCase(&vulkan, BufferWorkgroupPublication(64));
RunCase(&vulkan, BufferWorkgroupPublication(32, true));
@@ -48418,6 +48831,11 @@ int main(int argc, char **argv) {
RunCase(&vulkan, Vop3MulLoU16CapturedAndSelectors());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--clamp-nan-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, Vop3F32ClampFlushesNaN());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--mad-i16-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, Vop3MadI16CapturedSelectorsAndSaturation());
@@ -48514,6 +48932,7 @@ int main(int argc, char **argv) {
}
if (argc == 2 && std::strcmp(argv[1], "--compute-meta-clear-only") == 0) {
CheckDynamicRenderingState();
CheckPs5GameExampleImageClearRuntimeShape();
VulkanHarness vulkan;
vulkan.CheckComputeMetaClearClassification();
vulkan.CheckNativeIndirectDispatch();
+57
View File
@@ -8,6 +8,7 @@
#include "kernel/pthread.h"
#include "libs/errno.h"
#include "loader/guestInstructionPatcher.h"
#include "loader/jit.h"
#include "loader/runtimeLinker.h"
#include "loader/systemContent.h"
#include "loader/x64InstructionEmulator.h"
@@ -4775,6 +4776,59 @@ void TestSmallFiberStacksAndMigration() {
} // namespace
#if defined(__linux__) || KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
KYTY_MS_ABI uint8_t* TlsStubTestAddress() {
return reinterpret_cast<uint8_t*>(static_cast<uintptr_t>(0x123456789abcull));
}
void TestTlsStubsPreserveFlags() {
const char* test = "TlsStubsPreserveFlags";
const uint64_t stub_size = Loader::Jit::SafeCall::GetSize();
constexpr uint64_t CODE_SIZE = 0x1000;
constexpr uint8_t REG = 1;
const auto mapping = Libs::LibKernel::Memory::AllocateProgramMemory(
0x0000000906000000ull, stub_size + CODE_SIZE, Common::VirtualMemory::Mode::ExecuteReadWrite,
"tls_stub_test");
Check(test, mapping != 0, "failed to allocate TLS stub test code");
auto* safe_call = new (reinterpret_cast<void*>(mapping)) Loader::Jit::SafeCall;
safe_call->SetFunc(TlsStubTestAddress);
const auto stub_address = mapping + Loader::Jit::TlsRegStub::GetOffset(REG);
auto* stub = new (reinterpret_cast<void*>(stub_address)) Loader::Jit::TlsRegStub;
stub->SetFunc(mapping);
stub->SetOutputReg(REG);
using Fixture = uint64_t(KYTY_SYSV_ABI*)(uint64_t*);
bool preserved = true;
for (const auto [input, status]: {std::pair {uint8_t {0x7f}, uint64_t {0x890}},
std::pair {uint8_t {0xff}, uint64_t {0x055}}}) {
Xbyak::CodeGenerator code(CODE_SIZE, reinterpret_cast<void*>(mapping + stub_size));
code.push(code.rbx);
code.mov(code.rbx, code.rdi);
code.mov(code.rax, 0x5555555555555555ull);
code.mov(code.al, input);
code.add(code.al, 1);
code.mov(code.r11, stub_address);
code.call(code.r11);
code.mov(code.qword[code.rbx], code.rcx);
code.mov(code.qword[code.rbx + 8], code.rax);
code.pushf();
code.pop(code.rax);
code.pop(code.rbx);
code.ret();
Check(test, Xbyak::GetError() == 0, "failed to generate TLS stub fixture");
Check(test, Common::VirtualMemory::FlushInstructionCache(mapping, stub_size + CODE_SIZE),
"failed to flush TLS stub test code");
std::array<uint64_t, 2> registers {};
const auto flags = reinterpret_cast<Fixture>(mapping + stub_size)(registers.data());
const auto rax = (0x5555555555555555ull & ~0xffull) | static_cast<uint8_t>(input + 1u);
preserved &= (flags & 0x8d5u) == status && registers[0] == 0x123456789abcull &&
registers[1] == rax;
}
Libs::LibKernel::Memory::FreeGuestMemory(mapping, stub_size + CODE_SIZE);
Check(test, preserved, "TLS stubs changed the caller's flags or registers");
}
#endif
int main(int argc, char** argv) {
InitSubsystems();
if (argc == 2 && std::strcmp(argv[1], "--virtual-ranges-only") == 0) {
@@ -4919,6 +4973,9 @@ int main(int argc, char** argv) {
RunTest(TestMemoryPoolCommitDecommitQueryFlags);
RunTest(TestProgramMemoryAllocationAndProtection);
RunTest(TestModuleRelocationUsesWritableHostMapping);
#if defined(__linux__) || KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
RunTest(TestTlsStubsPreserveFlags);
#endif
if (g_failed_tests != 0) {
std::printf("VirtualMemoryAllocationTests: %d case(s) failed\n", g_failed_tests);
+214 -6
View File
@@ -8119,6 +8119,24 @@ void TestDsReturningAtomic64Decoder() {
"captured DS_CMPST_RTN_B64 lost its comparison/replacement pairs or aliases");
}
void TestDsCompareStore32Decoder() {
using namespace ShaderRecompiler::Decoder;
// CS04b812daaacd532f pc 0x3e8 (AC Valhalla, #281).
const uint32_t words[] = {0xd8c00000u, 0x04130504u};
Instruction decoded;
DecodeInstruction(words, 0u, decoded);
Check(decoded.opcode == Opcode::DS_CMPST_RTN_B32 && decoded.word_count == 2u &&
decoded.data_dwords == 1u && decoded.src_count == 3u && decoded.src0.reg == 4u &&
decoded.src1.reg == 5u && decoded.src2.reg == 19u && decoded.dst.reg == 4u &&
decoded.offset == 0u && !decoded.gds,
"captured DS_CMPST_RTN_B32 lost its comparison/replacement order or aliases");
const uint32_t gds_words[] = {words[0] | (1u << 17u), words[1]};
DecodeInstruction(gds_words, 0u, decoded);
Check(decoded.opcode == Opcode::UNSUPPORTED && decoded.word_count == 2u && decoded.gds,
"DS_CMPST_RTN_B32 must reject the unimplemented GDS variant");
}
void TestDsFloatMinMaxDecoder() {
using namespace ShaderRecompiler::Decoder;
for (bool max_value : {false, true}) {
@@ -10539,6 +10557,8 @@ void TestCapturedBufferAtomicsX2() {
ValueOpcode ir_opcode;
};
const Case cases[] = {
{{0xe1480018u, 0x80000000u}, 0u, 234u, "BUFFER_ATOMIC_ADD_X2",
"BufferAtomicIAdd64", "OpAtomicIAdd", ValueOpcode::BufferAtomicIAdd64},
{{0xe1680018u, 0x80000000u}, 0u, 241u, "BUFFER_ATOMIC_OR_X2",
"BufferAtomicOr64", "OpAtomicOr", ValueOpcode::BufferAtomicOr64},
{{0xe1402000u, 0x80000913u}, 9u, 229u, "BUFFER_ATOMIC_SWAP_X2",
@@ -11088,12 +11108,13 @@ void TestMergedShaderUserDataSnapshot() {
ShaderMapUserData(regs.gs_regs.data_addr, mapped);
HW::Context context;
context.SetShaderStages(0x20);
context.SetMaxOutputPerSubgroup(252);
context.SetMaxOutputPerSubgroup(192);
context.SetGsMaxVertOut(18);
context.SetNggSubgrpCntl(16);
context.SetGsOutPrimType(2);
HW::UserConfig user_config;
user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriList);
user_config.SetGeControl({14, 14});
user_config.SetGeControl({62, 192});
ShaderVertexInputInfo first_input{};
const auto first = PrepareProgram(regs, context, user_config, first_input);
regs.gs_regs.user_data_addr = reinterpret_cast<uint64_t>(second_table.data());
@@ -11106,8 +11127,10 @@ void TestMergedShaderUserDataSnapshot() {
"merged shader parameters did not snapshot ordinary user SGPRs at s8");
Check(first.hash == second.hash &&
MakeStageStaticKey(first_input) == MakeStageStaticKey(second_input) &&
first_input.mesh.scratch_size_dwords == 7,
"merged shader identity or maximum scratch size changed with dynamic user data");
first_input.mesh.scratch_size_dwords == 7 &&
first_input.mesh.primitives_per_group == 10 &&
first_input.mesh.max_primitives == 160 && first_input.mesh.threads_num[0] == 192,
"merged shader lost its identity, scratch size or amplified subgroup bounds");
CompileOptions options{};
options.stage = ShaderType::Mesh;
options.user_data_base = 0;
@@ -11170,6 +11193,7 @@ void TestMergedShaderUserDataSnapshot() {
regs.gs_regs.rsrc2.lds_size = 64;
regs.gs_regs.rsrc1.gs_vgpr_component_count = 1;
context.SetGsMaxVertOut(0);
context.SetNggSubgrpCntl(1);
for (const uint32_t wave_size : {32u, 64u}) {
context.SetShaderStages(wave_size == 32 ? 0x00400000u : 0u);
for (const uint16_t capacity : {64u, 128u, 256u}) {
@@ -11195,18 +11219,31 @@ void TestMergedShaderUserDataSnapshot() {
context.SetShaderStages(0x20);
context.SetMaxOutputPerSubgroup(256);
context.SetGsMaxVertOut(8);
context.SetNggSubgrpCntl(6);
user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriFan);
user_config.SetGeControl({32, 32});
ShaderVertexInputInfo fan_input{};
PrepareProgram(regs, context, user_config, fan_input);
Check(fan_input.mesh.primitives_per_group == 30 && fan_input.mesh.vertices_per_group == 32 &&
fan_input.mesh.max_vertices == 256 && fan_input.mesh.max_primitives == 192,
fan_input.mesh.max_vertices == 256 && fan_input.mesh.max_primitives == 180,
"captured triangle-fan GS configuration lost its subgroup assembly limits");
context.SetShaderStages(0x00482030u);
context.SetShaderStages(0x00402030u);
context.SetMaxOutputPerSubgroup(32);
context.SetGsMaxVertOut(32);
user_config.SetPrimitiveType(Prospero::PrimitiveType::kPointList);
user_config.SetGeControl({1, 1});
for (const uint32_t amplification : {32u, 48u}) {
context.SetNggSubgrpCntl(amplification);
ShaderVertexInputInfo input{};
PrepareProgram(regs, context, user_config, input);
Check(!input.mesh.fast_launch && input.mesh.primitives_per_group == 1 &&
input.mesh.vertices_per_group == 1 && input.mesh.max_vertices == 32 &&
input.mesh.max_primitives == amplification &&
input.mesh.threads_num[0] == (amplification == 32u ? 32u : 64u),
"native NGG output primitives were derived from a vertex strip or omitted from subgroup size");
}
context.SetShaderStages(0x00482030u);
context.SetNggSubgrpCntl(32);
regs.gs_regs.rsrc1.gs_vgpr_component_count = 0;
regs.gs_regs.rsrc2.es_vgpr_component_count = 1;
ShaderVertexInputInfo fast_input{};
@@ -14412,6 +14449,175 @@ void TestComputeImageFill() {
Run(mutation);
}
void TestPatternFill() {
using namespace ShaderRecompiler::IR;
// Bounded DWORD pattern fill, including every instruction through S_ENDPGM.
const uint32_t bounded_shader[] = {
0xbfa00003u, 0xd7460002u, 0x04010c0au, 0x7da80408u,
0xbf88003fu, 0x7e000c09u, 0xbf070980u, 0x858a807eu,
0x7e005700u, 0x100000ffu, 0x4f800000u, 0x7e060f00u,
0xd5766a00u, 0x02020609u, 0x7d8a0280u, 0x4c020080u,
0x02000101u, 0xd56a0001u, 0x00020700u, 0x4c000303u,
0x4a020303u, 0x02000101u, 0xd56a0000u, 0x00020500u,
0xd5690001u, 0x00020009u, 0x4c060302u, 0x7d8c02f9u,
0x06068c02u, 0x7d860609u, 0x87ea6a0cu, 0x50000080u,
0xd5286a00u, 0x003200c1u, 0xd5010000u, 0x002a00c1u,
0xd5690000u, 0x00020009u, 0x4c000102u, 0x7d0a0080u,
0xbe88246au, 0xbf880015u, 0x7d0a0081u, 0xbe8a246au,
0xbf88000cu, 0x7d0a0082u, 0xbeea246au, 0xbf880003u,
0x7e000207u, 0xe0102000u, 0x80000002u, 0x8afe7e6au,
0xbf880003u, 0x7e000206u, 0xe0102000u, 0x80000002u,
0xbefe046au, 0x8afe7e0au, 0xbf880003u, 0x7e000205u,
0xe0102000u, 0x80000002u, 0xbefe040au, 0x8afe7e08u,
0xbf880003u, 0x7e000204u, 0xe0102000u, 0x80000002u,
0xbf810000u,
};
// Pattern fill whose bounds come from dispatch thread dimensions.
const uint32_t thread_dimension_shader[] = {
0xbfa00002u, 0x7e040c09u, 0x81ea0980u, 0xd7460000u,
0x04010c0au, 0x7e020205u, 0x7e045702u, 0x100404ffu,
0x4f7ffffeu, 0x7e040f02u, 0x7ed60502u, 0x936a6b6au,
0x9aea6b6au, 0x4a04046au, 0xd56a0002u, 0x00020102u,
0xd5690002u, 0x00020409u, 0x4c040500u, 0x7d860409u,
0x4e060409u, 0x02040702u, 0x7d860409u, 0x4e060409u,
0x02040702u, 0x7d840481u, 0x7d8404f9u, 0x06868882u,
0x7d8404f9u, 0x06868a83u, 0x02020204u, 0xd5010001u,
0x00200d01u, 0xd5010001u, 0x00280f01u, 0xe0102000u,
0x80000100u, 0xbf810000u,
};
// The 64-lane variant uses vector multiply-high for the remainder calculation.
const uint32_t thread_dimension_wave64_shader[] = {
0xbfa00002u, 0x7e060c09u, 0x4c0412f9u, 0x86860680u,
0xd7460000u, 0x04010c0au, 0x7e020205u, 0x7e065703u,
0x100606ffu, 0x4f7ffffeu, 0x7e060f03u, 0xd5690002u,
0x00020702u, 0xd56a0002u, 0x00020702u, 0x4a040503u,
0xd56a0002u, 0x00020102u, 0xd5690002u, 0x00020409u,
0x4c040500u, 0x7d860409u, 0x4c0612f9u, 0x86060602u,
0x02040702u, 0x7d860409u, 0x4c0612f9u, 0x86060602u,
0x02040702u, 0x7d040481u, 0x7d0404f9u, 0x06868882u,
0x7d0404f9u, 0x06868a83u, 0x02020204u, 0xd5010001u,
0x00200d01u, 0xd5010001u, 0x00280f01u, 0xe0102000u,
0x80000100u, 0xbf810000u,
};
// The 64-lane variant with scalar quotient setup.
const uint32_t thread_dimension_wave64_scalar_shader[] = {
0xbfa00002u, 0x7e040c09u, 0x81ea0980u, 0xd7460000u,
0x04010c0au, 0x7e020205u, 0x7e045702u, 0x100404ffu,
0x4f7ffffeu, 0x7e040f02u, 0x7ed60502u, 0x936a6b6au,
0x9aea6b6au, 0x816a6a6bu, 0xd56a0002u, 0x0002006au,
0xd5690002u, 0x00020409u, 0x4c040500u, 0x7d860409u,
0x4c0612f9u, 0x86060602u, 0x02040702u, 0x7d860409u,
0x4c0612f9u, 0x86060602u, 0x02040702u, 0x7d040481u,
0x7d0404f9u, 0x06868882u, 0x7d0404f9u, 0x06868a83u,
0x02020204u, 0xd5010001u, 0x00200d01u, 0xd5010001u,
0x00280f01u, 0xe0102000u, 0x80000100u, 0xbf810000u,
};
// Same layout, with vector arithmetic for the reciprocal correction.
auto thread_dimension_wave64_vector_shader = std::to_array(thread_dimension_wave64_scalar_shader);
thread_dimension_wave64_vector_shader[13] = 0x4a04046au;
thread_dimension_wave64_vector_shader[15] = 0x00020102u;
struct PatternCase {
std::span<const uint32_t> shader;
uint32_t wave_size;
bool bounded;
size_t literal_word;
};
const PatternCase cases[] = {
{bounded_shader, 64, true, 10},
{thread_dimension_shader, 32, false, 8},
{thread_dimension_shader, 64, false, 8},
{thread_dimension_wave64_shader, 64, false, 9},
{thread_dimension_wave64_scalar_shader, 64, false, 8},
{thread_dimension_wave64_vector_shader, 64, false, 8},
};
for (const auto &[shader, wave_size, bounded, literal_word] : cases) {
std::array<uint32_t, 10> userdata = {
0x00200000u, 4u << 16, 64u, 0x14204u,
0xff000000u, 0u, 0u, 0x08000000u, 64u, 1u,
};
ShaderComputeInputInfo compute{};
compute.threads_num[0] = 64;
compute.threads_num[1] = compute.threads_num[2] = 1;
compute.workgroup_register = 10;
compute.thread_ids_num = 1;
compute.group_id[0] = true;
compute.dispatch_thread_dimensions = !bounded;
compute.wave_size = wave_size;
auto options = MakeCompileOptions(ShaderType::Compute);
options.wave_size = compute.wave_size;
options.input_info.compute = &compute;
options.user_data = userdata;
// Recognition derives identity from code, independently of the caller's hash.
options.shader_hash = 0x12345678u;
const auto Build = [&](std::span<const uint32_t> code) {
auto translated = ShaderRecompiler::TranslateProgram(code, options);
return ExtractResourcePlan(translated.program);
};
auto plan = Build(shader);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto Materialize = [&](bool expected) {
Check(MaterializeResources(plan, {.user_data = userdata}, snapshot, specialization),
"pattern fill did not materialize");
const auto &fill = snapshot.uniform_fill;
Check((fill.kind == UniformFillKind::Buffer) == expected,
"pattern fill used inactive words or retained a stale result");
if (expected)
Check(fill.value == userdata[4] && fill.words == 1 &&
fill.bounded_elements == (bounded ? userdata[8] : 0u) &&
fill.group_stride == std::array<uint32_t, 3>{64, 0, 0},
"pattern fill lost its value or coverage contract");
};
Materialize(true);
userdata[4] ^= 0x123456u;
Materialize(true);
userdata[4] ^= 0x123456u;
for (const uint32_t period : {0u, 2u, 3u, 4u, 5u}) {
userdata[9] = period;
Materialize(false);
}
for (uint32_t period = 2; period <= 4; ++period) {
userdata[9] = period;
userdata[3 + period] = userdata[4];
Materialize(true);
userdata[5] ^= 1u;
Materialize(false);
userdata[5] ^= 1u;
}
userdata[5] = userdata[6] = 0;
userdata[7] = 0x08000000u;
userdata[9] = 1;
Materialize(true);
userdata[8] = 0;
Materialize(!bounded);
userdata[8] = 63;
Materialize(true); // The renderer owns the descriptor-size equality check.
userdata[8] = 64;
Materialize(true);
std::vector<uint32_t> changed(shader.begin(), shader.end());
changed.push_back(0xdeadcafeu); // Non-executable metadata does not change identity.
Check(Build(changed).uniform_fill.fill.kind == UniformFillKind::Buffer,
"pattern recognition included bytes after S_ENDPGM");
changed[literal_word] ^= 0x00800000u;
Check(Build(changed).uniform_fill.fill.kind == UniformFillKind::None,
"pattern recognition accepted modified executable instructions");
for (const uint32_t invalid_input : {0u, 1u, 2u, 3u}) {
// Both wave sizes are covered above for this executable.
if (invalid_input == 1 && shader.data() == thread_dimension_shader) continue;
const auto saved_compute = compute;
const auto saved_wave = options.wave_size;
if (invalid_input == 0) compute.workgroup_register = 11;
if (invalid_input == 1) options.wave_size = wave_size == 64 ? 32 : 64;
if (invalid_input == 2) compute.dispatch_thread_dimensions = bounded;
if (invalid_input == 3) compute.float_mode ^= 1u;
Check(Build(shader).uniform_fill.fill.kind == UniformFillKind::None,
"pattern recognition accepted different shader launch inputs");
compute = saved_compute;
options.wave_size = saved_wave;
}
}
}
void TestGpuProducedWritableDescriptor() {
using namespace ShaderRecompiler::IR;
// Reduced from 994af59d50bedca8: the GPU-built BVH header supplies a relative
@@ -16312,6 +16518,7 @@ int main() {
TestScalarAshrI64Decoder();
TestImageAtomicWidthDecoder();
TestDsReturningAtomic64Decoder();
TestDsCompareStore32Decoder();
TestDsFloatMinMaxDecoder();
TestNewShaderDecoderArchitecture();
TestImageAddressOperands();
@@ -16428,6 +16635,7 @@ int main() {
TestNewShaderRecompilerEarlyZDisabledWhenPixelKillEnabled();
TestTypedDescriptorRealWideMoveTranslation();
TestComputeImageFill();
TestPatternFill();
TestGpuProducedWritableDescriptor();
TestUniformSelectedWritableDescriptor();
TestUniformSelectedDescriptorLoadAddress();