Compare commits

...

16 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
Matthew Hurley 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
Nawid3333 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
Nawid3333 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
Nawid3333 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
70 changed files with 1368 additions and 380 deletions
+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.
+11 -8
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");
@@ -567,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;
@@ -1060,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();
@@ -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,23 +248,10 @@ 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) {
@@ -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);
@@ -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))));
@@ -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;
@@ -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;
@@ -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)
@@ -4047,18 +4047,18 @@ UniformFillPlan RecognizePatternFill(Program& program, const Decoder::Program& d
struct Pattern {
size_t code_words;
uint64_t hash;
uint32_t wave_size;
uint32_t wave_size_mask;
bool bounded;
};
constexpr std::array patterns {
// Bounded 64-lane variant.
Pattern {.code_words = 69, .hash = 0x0704b9818e3cba06ull, .wave_size = 64, .bounded = true},
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 = 64, .bounded = false},
Pattern {.code_words = 40, .hash = 0x374161027f58bee7ull, .wave_size = 64, .bounded = false},
Pattern {.code_words = 40, .hash = 0x7d7172c56c2e296dull, .wave_size = 64, .bounded = false},
// Dispatch thread dimensions, 32-lane variant.
Pattern {.code_words = 38, .hash = 0xacbda1a1ccdd4198ull, .wave_size = 32, .bounded = false},
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 ||
@@ -4078,7 +4078,7 @@ UniformFillPlan RecognizePatternFill(Program& program, const Decoder::Program& d
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 ||
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) {
@@ -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;
@@ -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 };
+6
View File
@@ -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);
+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)) {
+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 {
+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;
}
+75
View File
@@ -1905,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(
@@ -2145,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;
}
+287 -43
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
@@ -25192,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;
@@ -25822,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;
@@ -25880,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);
@@ -25894,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}};
}
@@ -31054,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;
@@ -35603,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;
@@ -42013,6 +42237,7 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorBfeI32SignExtendsField);
AddCase(VectorAlignByteUsesTwoBitByteOffset);
AddCase(VectorCarryAndBitCountOps);
AddCase(ScalarVector64BitFloatInlineConstants);
AddCase(VectorMbcntUsesThreadMask);
AddCase(VectorAddcWritesPerLaneCarryOut);
AddCase(VectorAddcUsesPerLaneCarryIn);
@@ -42031,6 +42256,7 @@ std::vector<TestCase> MakeCases() {
AddCase(Vop3pInt16Ops);
AddCase(CvtPkU8F32PacksSelectedByte);
AddCase(CvtPkrtzF16F32SubnormalRoundsTowardZero);
AddCase(CvtPkrtzF16F32RuntimeEdges);
AddCase(CvtPkrtzF16F32SdwaAndOutputModifiers);
AddCase(PackedMinMaxF16NanAndSignedZeroEdges);
AddCase(VectorMinMaxF16Ops);
@@ -42188,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);
@@ -45542,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};
@@ -45573,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");
@@ -45647,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,
@@ -45706,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,
@@ -45717,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,
@@ -47842,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);
@@ -48381,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));
+26 -6
View File
@@ -11108,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());
@@ -11126,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;
@@ -11190,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}) {
@@ -11215,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{};
@@ -14508,6 +14525,7 @@ void TestPatternFill() {
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},
@@ -14584,6 +14602,8 @@ void TestPatternFill() {
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;