Compare commits

...

13 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
68 changed files with 1319 additions and 367 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);
@@ -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;
}
+252 -41
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
@@ -25850,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;
@@ -31086,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;
@@ -35635,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;
@@ -42064,6 +42256,7 @@ std::vector<TestCase> MakeCases() {
AddCase(Vop3pInt16Ops);
AddCase(CvtPkU8F32PacksSelectedByte);
AddCase(CvtPkrtzF16F32SubnormalRoundsTowardZero);
AddCase(CvtPkrtzF16F32RuntimeEdges);
AddCase(CvtPkrtzF16F32SdwaAndOutputModifiers);
AddCase(PackedMinMaxF16NanAndSignedZeroEdges);
AddCase(VectorMinMaxF16Ops);
@@ -42221,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);
@@ -45575,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};
@@ -45606,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");
@@ -45680,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,
@@ -45739,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,
@@ -45750,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,
@@ -47875,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);
@@ -48414,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;