Compare commits

..

21 Commits

Author SHA1 Message Date
shadowbeat070 7b7f9a70c6 kernel: answer ESRCH for a thread the detached-thread reaper has joined 2026-10-20 15:54:14 +02:00
nmzik 6ad393775f memory: write GPU publishes to backed pages and skip reserved holes like sparse reads 2026-10-20 15:53:23 +02:00
nmzik f492027be2 shader: select pointer-table images through indexed x3 material keys in readlane waterfalls 2026-10-20 15:21:19 +02:00
nmzik 8843291506 gpu: accept any T# perf_mod on storage textures through a shared canonical encoding check 2026-10-20 15:21:19 +02:00
nmzik 63ca9c0b80 shader: decode PS5 image_gather4_c_a and image_gather4_c_o_a 2026-10-20 15:21:19 +02:00
Mr. Big Britches 0caf4a6aa0 audio: route NGS2 custom mastering voices to the render output 2026-10-20 15:21:19 +02:00
Mr. Big Britches c715891242 shader: memoize the tessellation layout per LS/HS pair and VGT configuration 2026-10-20 15:21:19 +02:00
nmzik f3e01aaf1e tests: cover tessellation patch constants, predicated mul24 addresses, and forward-branch joins 2026-10-20 15:21:19 +02:00
Mr. Big Britches 0957f088ad shader: reflect HS patch constants and lower TES patch inputs through predicated addresses 2026-10-20 15:21:19 +02:00
Mr. Big Britches 4a2961cdf9 shader: reflect tessellation strides across forward branches, DS write widths, and packed patch masks 2026-10-20 15:21:19 +02:00
Mr. Big Britches f0e462a8a2 shader: allocate split-wave dispatcher spill IDs in program order 2026-10-20 15:21:19 +02:00
Mr. Big Britches feed075a9f gpu: implement PA_CL_UCP user clip planes through draw push constants 2026-10-20 15:21:19 +02:00
Mr. Big Britches 18e6747dbf audio: allow looping waveform blocks and exit-loop on NGS2 custom samplers 2026-10-20 15:21:19 +02:00
Mr. Big Britches 6b6274fa5a audio: route NGS2 custom rack buffers and run USER_FX side-chain modules 2026-10-20 15:21:19 +02:00
Andre Michi 1a053f9e13 shader: accept dword SETPC tables whose load reuses the selector 2026-10-20 05:11:48 +02:00
nmzik 12ca1599cf net: implement the libSceHttp callback, gzip, chunked, cert and error ABIs 2026-10-20 04:38:12 +02:00
nmzik f186b35706 shader: drop null checks on the stage input info pointers 2026-10-20 03:22:40 +02:00
Matthew Hurley 992a83003d tests: stub VR pad ports in controller settings tests (#1741)
* tests: stub VR pad ports in controller settings tests

#1726 added PadVrPortHand/PadVrPort/PadVrClose calls to controller.cpp,
which controller_settings_tests #includes directly, breaking its link
(the full-emulator tests pick up libs/padVr.cpp via glob and are fine).
Stub the three VR entry points as unavailable, matching the file's
existing DualSenseHaptics fakes.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* tests: stub PadVrCheckHandle for the Windows controller settings link

---------

Co-authored-by: Matthew Hurley <171001439+theantipopau@users.noreply.github.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-20 03:22:27 +02:00
0xcrstn fa8216c4d9 shader: size per-thread LDS emulation by the guest LDS allocation 2026-10-20 02:51:27 +02:00
Leandro Bras 742fe17173 vr: add OpenXR headset output, VR tracker and VR controller input behind --vr (#1726) 2026-10-20 02:18:51 +02:00
Pipe530 0cad4672e9 launcher: add System/Dark/Light theme option 2026-10-19 16:06:00 +02:00
59 changed files with 2924 additions and 511 deletions
+17
View File
@@ -25,6 +25,23 @@ FetchContent_MakeAvailable(opus)
set(BUILD_TESTING "${kyty_build_testing_saved}")
install(FILES "${opus_SOURCE_DIR}/COPYING" DESTINATION "licenses/opus")
# Headsets are driven through OpenXR. Only the static loader is built; the runtime that serves it
# (for example SteamVR, Monado or Virtual Desktop) comes from the user's system.
set(DYNAMIC_LOADER OFF CACHE BOOL "" FORCE)
set(BUILD_WITH_SYSTEM_JSONCPP OFF CACHE BOOL "" FORCE)
FetchContent_Declare(openxr
GIT_REPOSITORY https://github.com/KhronosGroup/OpenXR-SDK.git
GIT_TAG f2448a8797c85814aa892efc1ab8707900fbcc78 # release-1.1.63
GIT_SHALLOW FALSE)
FetchContent_MakeAvailable(openxr)
# Keep the loader's headers and library out of the installed package.
set_property(DIRECTORY "${openxr_SOURCE_DIR}" PROPERTY EXCLUDE_FROM_ALL TRUE)
# The loader uses exceptions internally.
if(NOT KYTY_CLANG_CL)
target_compile_options(openxr_loader PRIVATE -fexceptions)
endif()
install(FILES "${openxr_SOURCE_DIR}/LICENSE" DESTINATION "licenses/openxr")
# The guest instruction patcher needs a decoder/encoder and a small
# trampoline assembler. Pin the same revisions used by shadPS4's implementation.
set(XBYAK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
+3 -1
View File
@@ -336,7 +336,7 @@ if (KYTY_CLANG_CL)
set_source_files_properties(${kyty_emulator_src} PROPERTIES COMPILE_FLAGS "-Wno-pragma-pack -Wno-deprecated-declarations -D_TIMESPEC_DEFINED")
endif()
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus)
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus OpenXR::openxr_loader)
# Linux system libraries.
if(LINUX)
@@ -655,6 +655,8 @@ if(BUILD_TESTING)
add_test(NAME save_data_dialog COMMAND $<TARGET_FILE:save_data_dialog_tests>)
add_test(NAME http_uri_parse COMMAND $<TARGET_FILE:http_uri_parse_tests>)
add_test(NAME shader_recompiler_compute COMMAND $<TARGET_FILE:shader_recompiler_compute_tests>)
add_test(NAME shader_recompiler_tessellation
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --tessellation-only)
add_test(NAME shader_recompiler_alignbyte
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --alignbyte-only)
add_test(NAME graphics_pipeline_rasterization
@@ -291,10 +291,6 @@ static void HwCtxIgnoreAaMaskRegister([[maybe_unused]] uint32_t cmd_offset,
static void HwCtxIgnoreAlphaToMaskRegister([[maybe_unused]] uint32_t value) {}
static void HwCtxIgnoreDisabledUserClipPlane(CommandProcessor& cp, uint32_t value) {
EXIT_NOT_IMPLEMENTED(value != 0 || cp.GetCtx().GetClipControl().user_clip_planes != 0);
}
static void HwCtxIgnorePrimitiveIdReset([[maybe_unused]] uint32_t value) {}
static void HwCtxIgnoreVtxControl([[maybe_unused]] uint32_t value) {}
@@ -2899,7 +2895,7 @@ void GraphicsInitJmpTablesCxIndirect() {
for (auto cmd_offset = Pm4::PA_CL_UCP_0_X; cmd_offset <= Pm4::PA_CL_UCP_5_W; cmd_offset++) {
g_hw_ctx_indirect_func[cmd_offset] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
HwCtxIgnoreDisabledUserClipPlane(cp, value);
cp.GetCtx().SetUserClipPlane(cmd_offset - Pm4::PA_CL_UCP_0_X, value);
};
}
+9
View File
@@ -5,6 +5,8 @@
#include "common/common.h"
#include "graphics/guest_gpu/gpu_defs.h"
#include <array>
namespace Libs::Graphics::HW {
struct ColorBase {
@@ -824,6 +826,11 @@ public:
void SetBlendColor(const BlendColor& color) { m_blend_color = color; }
[[nodiscard]] const ClipControl& GetClipControl() const { return m_clip_control; }
void SetClipControl(const ClipControl& control) { m_clip_control = control; }
// PA_CL_UCP_0_X..PA_CL_UCP_5_W as raw float bits, four per plane.
[[nodiscard]] const std::array<uint32_t, 24>& GetUserClipPlanes() const {
return m_user_clip_planes;
}
void SetUserClipPlane(uint32_t index, uint32_t value) { m_user_clip_planes[index] = value; }
[[nodiscard]] const RenderControl& GetRenderControl() const { return m_render_control; }
void SetRenderControl(const RenderControl& control) { m_render_control = control; }
[[nodiscard]] const DepthRenderOverride& GetDepthRenderOverride() const {
@@ -924,6 +931,8 @@ private:
ColorControl m_color_control;
ScanModeControl m_scan_mode_control;
std::array<uint32_t, 24> m_user_clip_planes {};
AaSampleControl m_aa_sample_control;
AaConfig m_aa_config;
+3 -4
View File
@@ -373,10 +373,9 @@ static void ClipPrint(const char* func, const HW::ClipControl& c) {
static void ClipCheck(const HW::ClipControl& c) {
// dx_linear_attr_clip_enable preserves linear (noperspective) attributes at clip-generated
// vertices, which Vulkan provides as part of clipping and interpolation.
EXIT_NOT_IMPLEMENTED(c.user_clip_planes != 0 || c.user_clip_plane_mode != 0 ||
c.vertex_kill_any || c.user_clip_plane_negate_y ||
c.user_clip_plane_cull_only || c.cull_on_clipping_error_disable ||
c.force_viewport_index_from_vs_enable);
EXIT_NOT_IMPLEMENTED(c.user_clip_plane_mode != 0 || c.vertex_kill_any ||
c.user_clip_plane_negate_y || c.user_clip_plane_cull_only ||
c.cull_on_clipping_error_disable || c.force_viewport_index_from_vs_enable);
}
static void RcPrint(const char* func, const HW::RenderControl& c) {
@@ -143,7 +143,8 @@ NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource
return result;
}
bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bool r128) {
// Reserved and unsupported T# fields must be zero; perf_mod only scales sampler state.
static bool IsCanonicalTextureEncoding(const ShaderTextureResource& descriptor, bool r128) {
constexpr uint32_t field1_reserved_mask = 0x200fff00u;
constexpr uint32_t field2_reserved_mask = 0xf0003000u;
const uint32_t field3_expected = descriptor.DstSelXYZW() |
@@ -154,17 +155,25 @@ bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bo
const uint32_t field4_expected = descriptor.Depth() | (descriptor.BaseArray5() << 16u);
const uint32_t field5_expected = (static_cast<uint32_t>(descriptor.PerfMod5()) << 20u) |
(static_cast<uint32_t>(descriptor.MaxMip()) << 4u);
const bool common = (descriptor.fields[1] & field1_reserved_mask) == 0 &&
(descriptor.fields[2] & field2_reserved_mask) == 0 &&
descriptor.fields[3] == field3_expected;
if (r128) {
return common && descriptor.fields[4] == 0 && descriptor.fields[5] == 0 &&
descriptor.fields[6] == 0 && descriptor.fields[7] == 0;
if ((descriptor.fields[1] & field1_reserved_mask) != 0 ||
(descriptor.fields[2] & field2_reserved_mask) != 0 || descriptor.fields[3] != field3_expected) {
return false;
}
const bool full = common && descriptor.fields[4] == field4_expected &&
descriptor.fields[5] == field5_expected;
if (!full ||
(descriptor.MsaaDepth() && !IsMultisampledTexture(descriptor.Type()))) {
if (r128) {
return descriptor.fields[4] == 0 && descriptor.fields[5] == 0 && descriptor.fields[6] == 0 &&
descriptor.fields[7] == 0;
}
return descriptor.fields[4] == field4_expected && descriptor.fields[5] == field5_expected;
}
bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bool r128) {
if (!IsCanonicalTextureEncoding(descriptor, r128)) {
return false;
}
if (r128) {
return true;
}
if (descriptor.MsaaDepth() && !IsMultisampledTexture(descriptor.Type())) {
return false;
}
const auto metadata_control = descriptor.fields[6] & 0x00ffffffu;
@@ -281,36 +290,12 @@ static bool IsSupportedStorageTextureDescriptor(const ShaderRecompiler::IR::Imag
!descriptor.MsaaDepth();
}
static bool IsSupportedStorageTextureEncoding(const ShaderRecompiler::IR::ImageResource& resource,
const ShaderTextureResource& descriptor) {
constexpr uint32_t field1_reserved_mask = 0x200fff00u;
constexpr uint32_t field2_reserved_mask = 0xf0003000u;
constexpr uint32_t field5_expected = 0x00700000u;
constexpr uint32_t field5_max_mip_mask = 0x000000f0u;
const uint32_t expected_field3 = descriptor.DstSelXYZW() |
(static_cast<uint32_t>(descriptor.BaseLevel()) << 12u) |
(static_cast<uint32_t>(descriptor.LastLevel()) << 16u) |
(static_cast<uint32_t>(descriptor.TileMode()) << 20u) |
(static_cast<uint32_t>(descriptor.Type()) << 28u);
const uint32_t expected_field4 =
descriptor.Depth() | (static_cast<uint32_t>(descriptor.BaseArray5()) << 16u);
const bool common = (descriptor.fields[1] & field1_reserved_mask) == 0 &&
(descriptor.fields[2] & field2_reserved_mask) == 0 &&
descriptor.fields[3] == expected_field3;
if (resource.r128) {
return common && descriptor.fields[4] == 0 && descriptor.fields[5] == 0 &&
descriptor.fields[6] == 0 && descriptor.fields[7] == 0;
}
return common && descriptor.fields[4] == expected_field4 &&
(descriptor.fields[5] & ~field5_max_mip_mask) == field5_expected;
}
void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource,
const ShaderTextureResource& descriptor, uint64_t size) {
const auto format = descriptor.Format();
const bool resource_ok = IsSupportedStorageImageResource(resource);
const bool descriptor_ok = IsSupportedStorageTextureDescriptor(resource, descriptor);
const bool encoding_ok = IsSupportedStorageTextureEncoding(resource, descriptor);
const bool encoding_ok = IsCanonicalTextureEncoding(descriptor, resource.r128);
const bool uint_resource = resource.numeric_class == Prospero::TextureNumericClass::Uint;
const bool raw_sint_storage = uint_resource &&
ShaderRecompiler::IR::RawSignedStorageFormat(resource, format) != Prospero::BufferFormat::kInvalid;
@@ -780,10 +780,12 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms(
}
}
}
if (context.GetClipControl().clip_disable) {
auto& last_stage = vertex_info[tess_active ? 2u : 0u];
const auto& clip_control = context.GetClipControl();
if (clip_control.clip_disable) {
const auto& viewport = context.GetScreenViewport().viewports[0];
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
auto& clip = vertex_info[tess_active ? 2u : 0u].clip_space;
auto& clip = last_stage.clip_space;
clip.scale[0] = viewport.xscale;
clip.scale[1] = viewport.yscale;
clip.offset[0] = viewport.xoffset;
@@ -793,9 +795,16 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms(
clip.half_extent[1] =
static_cast<float>(std::min(limits.maxViewportDimensions[1], 16384u)) * 0.5f;
clip.enabled = true;
} else if (clip_control.user_clip_planes != 0) {
// CLIP_DISABLE also disables user planes. How they combine with shader clip and cull
// distances is unverified; draw push constants only reach VS/mesh and PS, and the
// generated RECTLIST stages do not forward clip distances.
EXIT_NOT_IMPLEMENTED((last_stage.pa_cl_vs_out_cntl & 0xffffu) != 0 || tess_active ||
Prospero::IsRectList(user_config.GetPrimType()));
last_stage.user_clip_planes = clip_control.user_clip_planes;
}
uint32_t push_data_cursor = ShaderRecompiler::IR::PushData::GraphicsDrawDwords(
mesh_active, object_primitive_id);
mesh_active, object_primitive_id, last_stage.user_clip_planes);
GraphicsPrograms result;
if (pixel_active) {
result.pixel = m_program_cache->Get(pixel_params, pixel_info, push_data_cursor);
+13 -6
View File
@@ -1193,13 +1193,14 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
if (!mesh_active) {
CommitIndexBuffer(vk_buffer, index_binding);
}
const auto draw_dwords = ShaderRecompiler::IR::PushData::GraphicsDrawDwords(
mesh_active, state.ps_input_info.object_primitive_id);
using ShaderRecompiler::IR::PushData;
const auto draw_stages =
vk::ShaderStageFlagBits::eFragment |
(mesh_active ? vk::ShaderStageFlagBits::eMeshEXT : vk::ShaderStageFlagBits::eVertex);
const auto draw_dwords =
PushData::GraphicsDrawDwords(mesh_active, state.ps_input_info.object_primitive_id);
if (draw_dwords != 0) {
std::array<uint32_t, ShaderRecompiler::IR::PushData::MeshDrawDwordCount> draw_data {};
const auto draw_stages = vk::ShaderStageFlagBits::eFragment |
(mesh_active ? vk::ShaderStageFlagBits::eMeshEXT
: vk::ShaderStageFlagBits::eVertex);
std::array<uint32_t, PushData::MeshDrawDwordCount> draw_data {};
if (mesh_active) {
draw_data = {
draw.index_count,
@@ -1213,6 +1214,12 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
vk_buffer.pushConstants(pipeline.pipeline_layout, draw_stages, 0,
draw_dwords * sizeof(uint32_t), draw_data.data());
}
if (const auto planes = vertex_stages.back().user_clip_planes; planes != 0) {
vk_buffer.pushConstants(pipeline.pipeline_layout, draw_stages,
PushData::UserClipPlaneDword * sizeof(uint32_t),
std::bit_width(planes) * 4u * sizeof(uint32_t),
buffer.GetRegisters().GetUserClipPlanes().data());
}
auto& dynamic_state = m_context.GetCommandScheduler().GetDynamicState();
UpdateGraphicsDynamicState(buffer, dynamic_state, vertex_stages.back(), state.depth_info,
+762
View File
@@ -0,0 +1,762 @@
#include "graphics/presentation/openxrHeadset.h"
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "common/threads.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/presentation/vrInput.h"
#include "libs/hmd2.h"
#define XR_USE_GRAPHICS_API_VULKAN
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <fmt/format.h>
#include <mutex>
#include <openxr/openxr.h>
#include <openxr/openxr_platform.h>
#include <string>
#include <thread>
namespace Libs::Vr {
namespace {
using namespace Libs::Graphics;
enum Action { TRIGGER, SQUEEZE, STICK, STICK_CLICK, PRIMARY, SECONDARY, MENU, GRIP, ACTION_COUNT };
// Touch controller bindings: the path under /user/hand/<side>/input/ per hand, or null.
constexpr struct {
const char* name;
XrActionType type;
const char* path[2];
} ACTIONS[ACTION_COUNT] = {
{"trigger", XR_ACTION_TYPE_FLOAT_INPUT, {"trigger/value", "trigger/value"}},
{"squeeze", XR_ACTION_TYPE_FLOAT_INPUT, {"squeeze/value", "squeeze/value"}},
{"thumbstick", XR_ACTION_TYPE_VECTOR2F_INPUT, {"thumbstick", "thumbstick"}},
{"thumbstick_click", XR_ACTION_TYPE_BOOLEAN_INPUT, {"thumbstick/click", "thumbstick/click"}},
{"primary", XR_ACTION_TYPE_BOOLEAN_INPUT, {"x/click", "a/click"}},
{"secondary", XR_ACTION_TYPE_BOOLEAN_INPUT, {"y/click", "b/click"}},
{"menu", XR_ACTION_TYPE_BOOLEAN_INPUT, {"menu/click", nullptr}},
{"grip_pose", XR_ACTION_TYPE_POSE_INPUT, {"grip/pose", "grip/pose"}},
};
constexpr const char* HANDS[2] = {"left", "right"};
// Eye images move from the render thread to the headset thread through these. One is being written,
// one is the latest recorded frame and one may be held by the headset thread until the GPU is done.
constexpr int STAGING_COUNT = 3;
struct Staging {
VulkanImage image;
uint64_t tick = 0;
VrPose head; // The head pose the game last read, so the picture can be placed in the world.
};
struct State {
XrInstance instance = XR_NULL_HANDLE;
XrSystemId system = XR_NULL_SYSTEM_ID;
XrSession session = XR_NULL_HANDLE;
XrSpace view_space = XR_NULL_HANDLE;
XrSpace local_space = XR_NULL_HANDLE;
XrSwapchain swapchain = XR_NULL_HANDLE; // Two array layers, one per eye.
std::vector<XrSwapchainImageVulkanKHR> swapchain_images;
vk::Extent2D eye {};
std::vector<std::string> extensions[2]; // Vulkan instance and device extensions.
PFN_xrGetVulkanGraphicsDeviceKHR get_graphics_device = nullptr;
GraphicContext* graphics = nullptr;
std::thread thread;
std::atomic_bool running {false};
// Headset thread only.
XrSessionState session_state = XR_SESSION_STATE_UNKNOWN;
bool session_running = false;
uint64_t shown_tick = 0;
VrPose shown_head;
vk::CommandPool pool;
vk::CommandBuffer command;
vk::Fence fence;
std::mutex mutex;
Staging staging[STAGING_COUNT];
int latest = -1;
int held = -1;
MasterSemaphore* semaphore = nullptr;
XrActionSet action_set = XR_NULL_HANDLE;
XrAction actions[ACTION_COUNT] {};
XrPath hand_path[2] {};
XrSpace grip_space[2] {};
std::mutex input_mutex;
VrControllerInput input[2];
VrPose head;
VrPose last_head_read;
};
State g;
bool Ok(XrResult result, const char* what) {
if (XR_SUCCEEDED(result)) {
return true;
}
char text[XR_MAX_RESULT_STRING_SIZE] = {};
xrResultToString(g.instance, result, text);
Log::WriteToConsoleAndLog(fmt::format("OpenXR: {} failed: {}\n", what, text));
return false;
}
// Destroying the instance also destroys the session, spaces, swapchain and actions.
void DestroyInstance(const char* reason) {
if (reason != nullptr) {
Log::WriteToConsoleAndLog(
fmt::format("OpenXR: {}; showing the eye image in the window only\n", reason));
}
if (g.instance != XR_NULL_HANDLE) {
xrDestroyInstance(g.instance);
}
g.instance = XR_NULL_HANDLE;
g.session = XR_NULL_HANDLE;
}
struct Access {
vk::PipelineStageFlags2 stage;
vk::AccessFlags2 access;
};
constexpr Access NO_ACCESS {vk::PipelineStageFlagBits2::eNone, vk::AccessFlagBits2::eNone};
constexpr Access TRANSFER {vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eNone};
constexpr Access TRANSFER_READ {vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferRead};
constexpr Access TRANSFER_WRITE {vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferWrite};
constexpr Access COLOR_ATTACHMENT {vk::PipelineStageFlagBits2::eColorAttachmentOutput,
vk::AccessFlagBits2::eColorAttachmentRead |
vk::AccessFlagBits2::eColorAttachmentWrite};
void Barrier(vk::CommandBuffer command, vk::Image image, vk::ImageLayout from, vk::ImageLayout to,
uint32_t layers, Access src, Access dst) {
vk::ImageMemoryBarrier2 barrier {};
barrier.srcStageMask = src.stage;
barrier.srcAccessMask = src.access;
barrier.dstStageMask = dst.stage;
barrier.dstAccessMask = dst.access;
barrier.oldLayout = from;
barrier.newLayout = to;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, layers};
vk::DependencyInfo dependency {};
dependency.imageMemoryBarrierCount = 1;
dependency.pImageMemoryBarriers = &barrier;
command.pipelineBarrier2(dependency);
}
XrQuaternionf Multiply(const XrQuaternionf& a, const XrQuaternionf& b) {
return {a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z};
}
XrVector3f Rotate(const XrQuaternionf& q, const XrVector3f& v) {
const XrVector3f t {2.0f * (q.y * v.z - q.z * v.y), 2.0f * (q.z * v.x - q.x * v.z),
2.0f * (q.x * v.y - q.y * v.x)};
return {v.x + q.w * t.x + (q.y * t.z - q.z * t.y), v.y + q.w * t.y + (q.z * t.x - q.x * t.z),
v.z + q.w * t.z + (q.x * t.y - q.y * t.x)};
}
VrPose Locate(XrSpace space, XrTime time) {
VrPose pose;
XrSpaceLocation location {XR_TYPE_SPACE_LOCATION};
if (XR_SUCCEEDED(xrLocateSpace(space, g.local_space, time, &location)) &&
(location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0) {
const auto& [q, p] = location.pose;
pose = {true, {q.x, q.y, q.z, q.w}, {p.x, p.y, p.z}};
}
return pose;
}
// The swapchain stores the eyes in a gamma-encoded format, so a raw copy of the display-encoded
// game image comes out right. Staging uses the same layout with UNORM, which a blit writes from the
// raw UNORM or float presentation frame without conversion.
bool CreateSwapchain() {
uint32_t count = 0;
xrEnumerateSwapchainFormats(g.session, 0, &count, nullptr);
std::vector<int64_t> formats(count);
xrEnumerateSwapchainFormats(g.session, count, &count, formats.data());
constexpr std::pair<vk::Format, vk::Format> candidates[] = {
{vk::Format::eR8G8B8A8Srgb, vk::Format::eR8G8B8A8Unorm},
{vk::Format::eB8G8R8A8Srgb, vk::Format::eB8G8R8A8Unorm},
{vk::Format::eR8G8B8A8Unorm, vk::Format::eR8G8B8A8Unorm},
{vk::Format::eB8G8R8A8Unorm, vk::Format::eB8G8R8A8Unorm},
};
const auto* format =
std::find_if(std::begin(candidates), std::end(candidates), [&](const auto& c) {
return std::find(formats.begin(), formats.end(), static_cast<int64_t>(c.first)) !=
formats.end();
});
if (format == std::end(candidates)) {
return false;
}
XrSwapchainCreateInfo info {XR_TYPE_SWAPCHAIN_CREATE_INFO};
info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
info.format = static_cast<int64_t>(format->first);
info.sampleCount = 1;
info.width = g.eye.width;
info.height = g.eye.height;
info.faceCount = 1;
info.arraySize = 2;
info.mipCount = 1;
if (!Ok(xrCreateSwapchain(g.session, &info, &g.swapchain), "xrCreateSwapchain")) {
return false;
}
xrEnumerateSwapchainImages(g.swapchain, 0, &count, nullptr);
g.swapchain_images.assign(count, {XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR});
xrEnumerateSwapchainImages(
g.swapchain, count, &count,
reinterpret_cast<XrSwapchainImageBaseHeader*>(g.swapchain_images.data()));
vk::ImageCreateInfo staging {};
staging.imageType = vk::ImageType::e2D;
staging.format = format->second;
staging.extent = vk::Extent3D {g.eye.width * 2, g.eye.height, 1};
staging.mipLevels = 1;
staging.arrayLayers = 1;
staging.samples = vk::SampleCountFlagBits::e1;
staging.tiling = vk::ImageTiling::eOptimal;
staging.usage = vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst;
for (auto& slot: g.staging) {
if (!g.graphics->CreateImage(staging, slot.image)) {
return false;
}
}
Log::WriteToConsoleAndLog(fmt::format("OpenXR: eye swapchain {}x{} {}\n", g.eye.width,
g.eye.height, vk::to_string(format->first)));
return true;
}
bool CreateInput() {
XrActionSetCreateInfo set {XR_TYPE_ACTION_SET_CREATE_INFO};
std::strcpy(set.actionSetName, "gameplay");
std::strcpy(set.localizedActionSetName, "Gameplay");
if (!Ok(xrCreateActionSet(g.instance, &set, &g.action_set), "xrCreateActionSet")) {
return false;
}
for (int hand = 0; hand < 2; hand++) {
xrStringToPath(g.instance, fmt::format("/user/hand/{}", HANDS[hand]).c_str(),
&g.hand_path[hand]);
}
std::vector<XrActionSuggestedBinding> bindings;
for (int i = 0; i < ACTION_COUNT; i++) {
XrActionCreateInfo info {XR_TYPE_ACTION_CREATE_INFO};
info.actionType = ACTIONS[i].type;
info.countSubactionPaths = 2;
info.subactionPaths = g.hand_path;
std::strcpy(info.actionName, ACTIONS[i].name);
std::strcpy(info.localizedActionName, ACTIONS[i].name);
if (!Ok(xrCreateAction(g.action_set, &info, &g.actions[i]), "xrCreateAction")) {
return false;
}
for (int hand = 0; hand < 2; hand++) {
if (ACTIONS[i].path[hand] != nullptr) {
XrPath path = XR_NULL_PATH;
xrStringToPath(
g.instance,
fmt::format("/user/hand/{}/input/{}", HANDS[hand], ACTIONS[i].path[hand])
.c_str(),
&path);
bindings.push_back({g.actions[i], path});
}
}
}
XrInteractionProfileSuggestedBinding suggested {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
xrStringToPath(g.instance, "/interaction_profiles/oculus/touch_controller",
&suggested.interactionProfile);
suggested.countSuggestedBindings = static_cast<uint32_t>(bindings.size());
suggested.suggestedBindings = bindings.data();
XrSessionActionSetsAttachInfo attach {XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO};
attach.countActionSets = 1;
attach.actionSets = &g.action_set;
if (!Ok(xrSuggestInteractionProfileBindings(g.instance, &suggested), "suggest bindings") ||
!Ok(xrAttachSessionActionSets(g.session, &attach), "attach action sets")) {
return false;
}
for (int hand = 0; hand < 2; hand++) {
XrActionSpaceCreateInfo space {XR_TYPE_ACTION_SPACE_CREATE_INFO};
space.action = g.actions[GRIP];
space.subactionPath = g.hand_path[hand];
space.poseInActionSpace = {{0, 0, 0, 1}, {0, 0, 0}};
if (!Ok(xrCreateActionSpace(g.session, &space, &g.grip_space[hand]), "grip space")) {
return false;
}
}
return true;
}
// The head is tracked while the frame loop runs; the controllers only while the session has focus.
void PollInput(XrTime time) {
const auto head = Locate(g.view_space, time);
VrControllerInput input[2];
XrActiveActionSet active {g.action_set, XR_NULL_PATH};
XrActionsSyncInfo sync {XR_TYPE_ACTIONS_SYNC_INFO};
sync.countActiveActionSets = 1;
sync.activeActionSets = &active;
if (g.action_set != XR_NULL_HANDLE && g.session_state == XR_SESSION_STATE_FOCUSED &&
XR_SUCCEEDED(xrSyncActions(g.session, &sync))) {
for (int hand = 0; hand < 2; hand++) {
const auto get = [&](Action action, auto read, auto state) {
XrActionStateGetInfo info {XR_TYPE_ACTION_STATE_GET_INFO};
info.action = g.actions[action];
info.subactionPath = g.hand_path[hand];
read(g.session, &info, &state);
return state;
};
const auto value = [&](Action action) {
return get(action, xrGetActionStateFloat,
XrActionStateFloat {XR_TYPE_ACTION_STATE_FLOAT})
.currentState;
};
const auto flag = [&](Action action) {
return get(action, xrGetActionStateBoolean,
XrActionStateBoolean {XR_TYPE_ACTION_STATE_BOOLEAN})
.currentState == XR_TRUE;
};
const auto stick = get(STICK, xrGetActionStateVector2f,
XrActionStateVector2f {XR_TYPE_ACTION_STATE_VECTOR2F});
auto& out = input[hand];
out.trigger = value(TRIGGER);
out.squeeze = value(SQUEEZE);
out.stick_x = stick.currentState.x;
out.stick_y = stick.currentState.y;
out.stick_click = flag(STICK_CLICK);
out.primary = flag(PRIMARY);
out.secondary = flag(SECONDARY);
out.menu = flag(MENU);
out.pose = Locate(g.grip_space[hand], time);
out.connected =
get(GRIP, xrGetActionStatePose, XrActionStatePose {XR_TYPE_ACTION_STATE_POSE})
.isActive == XR_TRUE;
}
}
std::lock_guard lock(g.input_mutex);
g.head = head;
g.input[0] = input[0];
g.input[1] = input[1];
}
void HandleEvents() {
for (XrEventDataBuffer event {XR_TYPE_EVENT_DATA_BUFFER};
xrPollEvent(g.instance, &event) == XR_SUCCESS; event = {XR_TYPE_EVENT_DATA_BUFFER}) {
if (event.type == XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING) {
g.running = false;
continue;
}
if (event.type != XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) {
continue;
}
g.session_state = reinterpret_cast<const XrEventDataSessionStateChanged&>(event).state;
LOGF("OpenXR: session state %d\n", static_cast<int>(g.session_state));
switch (g.session_state) {
case XR_SESSION_STATE_READY: {
XrSessionBeginInfo begin {XR_TYPE_SESSION_BEGIN_INFO};
begin.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
g.session_running = Ok(xrBeginSession(g.session, &begin), "xrBeginSession");
break;
}
case XR_SESSION_STATE_STOPPING: {
xrEndSession(g.session);
g.session_running = false;
std::lock_guard lock(g.input_mutex);
g.head = {};
g.input[0] = {};
g.input[1] = {};
break;
}
case XR_SESSION_STATE_EXITING:
case XR_SESSION_STATE_LOSS_PENDING: g.running = false; break;
default: break;
}
}
}
bool CopyToSwapchain(const VulkanImage& staging) {
uint32_t image = 0;
XrSwapchainImageAcquireInfo acquire {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
{
Common::LockGuard queue_lock(g.graphics->queue_mutex);
if (!Ok(xrAcquireSwapchainImage(g.swapchain, &acquire, &image), "acquire")) {
return false;
}
}
XrSwapchainImageWaitInfo wait {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
wait.timeout = XR_INFINITE_DURATION;
bool copied = Ok(xrWaitSwapchainImage(g.swapchain, &wait), "wait swapchain image");
if (copied) {
const auto target = g.swapchain_images[image].image;
(void)g.command.reset();
vk::CommandBufferBeginInfo begin {};
begin.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
(void)g.command.begin(&begin);
Barrier(g.command, target, vk::ImageLayout::eUndefined,
vk::ImageLayout::eTransferDstOptimal, 2, NO_ACCESS, TRANSFER_WRITE);
vk::ImageCopy copies[2] {};
for (uint32_t eye = 0; eye < 2; eye++) {
copies[eye].srcSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
copies[eye].srcOffset = vk::Offset3D {static_cast<int32_t>(eye * g.eye.width), 0, 0};
copies[eye].dstSubresource = {vk::ImageAspectFlagBits::eColor, 0, eye, 1};
copies[eye].extent = vk::Extent3D {g.eye.width, g.eye.height, 1};
}
g.command.copyImage(staging.image, vk::ImageLayout::eTransferSrcOptimal, target,
vk::ImageLayout::eTransferDstOptimal, 2, copies);
Barrier(g.command, target, vk::ImageLayout::eTransferDstOptimal,
vk::ImageLayout::eColorAttachmentOptimal, 2, TRANSFER_WRITE, COLOR_ATTACHMENT);
(void)g.command.end();
vk::SubmitInfo submit {};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &g.command;
(void)g.graphics->device.resetFences(1, &g.fence);
vk::Result result {};
{
Common::LockGuard queue_lock(g.graphics->queue_mutex);
result = g.graphics->queue.submit(1, &submit, g.fence);
}
copied = result == vk::Result::eSuccess &&
g.graphics->device.waitForFences(1, &g.fence, VK_TRUE, UINT64_MAX) ==
vk::Result::eSuccess;
}
XrSwapchainImageReleaseInfo release {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
Common::LockGuard queue_lock(g.graphics->queue_mutex);
return Ok(xrReleaseSwapchainImage(g.swapchain, &release), "release") && copied;
}
// Holds the newest recorded eye image until the GPU has finished it, without waiting, then copies
// it into the swapchain. The swapchain keeps the last copy, so frames in between show it again.
bool UpdateSwapchain() {
int index = -1;
MasterSemaphore* semaphore = nullptr;
{
std::lock_guard lock(g.mutex);
if (g.held < 0 && g.latest >= 0 && g.staging[g.latest].tick != g.shown_tick) {
g.held = g.latest;
}
index = g.held;
semaphore = g.semaphore;
}
if (index >= 0) {
const auto& slot = g.staging[index];
semaphore->Refresh();
if (semaphore->IsFree(slot.tick)) {
if (CopyToSwapchain(slot.image)) {
g.shown_tick = slot.tick;
g.shown_head = slot.head;
}
std::lock_guard lock(g.mutex);
g.held = -1;
}
}
return g.shown_tick != 0;
}
// The game drew the picture for the head pose it read, so it is placed in the world at that pose
// and the runtime reprojects it as the head moves. Eye offsets come from the runtime.
void PlaceViews(XrTime time, XrCompositionLayerProjection& layer,
XrCompositionLayerProjectionView (&views)[2]) {
XrViewLocateInfo locate {XR_TYPE_VIEW_LOCATE_INFO};
locate.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
locate.displayTime = time;
locate.space = g.view_space;
XrViewState state {XR_TYPE_VIEW_STATE};
XrView located[2] {{XR_TYPE_VIEW}, {XR_TYPE_VIEW}};
uint32_t count = 0;
const bool valid =
XR_SUCCEEDED(xrLocateViews(g.session, &locate, &state, 2, &count, located)) &&
(state.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0;
const auto& head = g.shown_head;
const XrQuaternionf head_q {head.orientation[0], head.orientation[1], head.orientation[2],
head.orientation[3]};
const XrVector3f head_p {head.position[0], head.position[1], head.position[2]};
// The outer side of each eye is its temporal side: left for the left eye.
const float out = std::atan(Hmd2::FOV_TAN_OUT);
const float in = std::atan(Hmd2::FOV_TAN_IN);
const float top = std::atan(Hmd2::FOV_TAN_TOP);
const float bottom = std::atan(Hmd2::FOV_TAN_BOTTOM);
for (int eye = 0; eye < 2; eye++) {
const float side = eye == 0 ? -Hmd2::EYE_OFFSET_M : Hmd2::EYE_OFFSET_M;
auto& view = views[eye];
view.pose = valid ? located[eye].pose : XrPosef {{0, 0, 0, 1}, {side, 0, 0}};
if (head.valid) {
const auto offset = Rotate(head_q, view.pose.position);
view.pose.position = {head_p.x + offset.x, head_p.y + offset.y, head_p.z + offset.z};
view.pose.orientation = Multiply(head_q, view.pose.orientation);
}
view.fov = {eye == 0 ? -out : -in, eye == 0 ? in : out, top, -bottom};
view.subImage.swapchain = g.swapchain;
view.subImage.imageRect = {
{0, 0}, {static_cast<int32_t>(g.eye.width), static_cast<int32_t>(g.eye.height)}};
view.subImage.imageArrayIndex = static_cast<uint32_t>(eye);
}
layer.space = head.valid ? g.local_space : g.view_space;
layer.viewCount = 2;
layer.views = views;
}
void ThreadMain() {
while (g.running) {
HandleEvents();
XrFrameWaitInfo wait {XR_TYPE_FRAME_WAIT_INFO};
XrFrameState frame {XR_TYPE_FRAME_STATE};
if (!g.session_running || !Ok(xrWaitFrame(g.session, &wait, &frame), "xrWaitFrame")) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
continue;
}
PollInput(frame.predictedDisplayTime);
XrFrameBeginInfo begin {XR_TYPE_FRAME_BEGIN_INFO};
{
Common::LockGuard queue_lock(g.graphics->queue_mutex);
if (!Ok(xrBeginFrame(g.session, &begin), "xrBeginFrame")) {
continue;
}
}
XrCompositionLayerProjection layer {XR_TYPE_COMPOSITION_LAYER_PROJECTION};
XrCompositionLayerProjectionView views[2] {{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW},
{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}};
const auto* layers = reinterpret_cast<const XrCompositionLayerBaseHeader*>(&layer);
XrFrameEndInfo end {XR_TYPE_FRAME_END_INFO};
end.displayTime = frame.predictedDisplayTime;
end.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
if (frame.shouldRender == XR_TRUE && UpdateSwapchain()) {
PlaceViews(frame.predictedDisplayTime, layer, views);
end.layerCount = 1;
end.layers = &layers;
}
Common::LockGuard queue_lock(g.graphics->queue_mutex);
(void)Ok(xrEndFrame(g.session, &end), "xrEndFrame");
}
}
} // namespace
void OpenXrInit() {
if (!Config::VrEnabled()) {
return;
}
if (const char* env = std::getenv("KYTY_OPENXR");
env != nullptr && std::strcmp(env, "0") == 0) {
return;
}
const char* extensions[] = {XR_KHR_VULKAN_ENABLE_EXTENSION_NAME};
XrInstanceCreateInfo info {XR_TYPE_INSTANCE_CREATE_INFO};
std::strcpy(info.applicationInfo.applicationName, "KytyPS5");
std::strcpy(info.applicationInfo.engineName, "Kyty");
info.applicationInfo.apiVersion = XR_API_VERSION_1_0;
info.enabledExtensionCount = 1;
info.enabledExtensionNames = extensions;
if (XR_FAILED(xrCreateInstance(&info, &g.instance))) {
DestroyInstance("no runtime available");
return;
}
XrSystemGetInfo system {XR_TYPE_SYSTEM_GET_INFO};
system.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
if (XR_FAILED(xrGetSystem(g.instance, &system, &g.system))) {
DestroyInstance("no headset available");
return;
}
PFN_xrGetVulkanGraphicsRequirementsKHR get_requirements = nullptr;
PFN_xrGetVulkanInstanceExtensionsKHR get_extensions[2] {};
xrGetInstanceProcAddr(g.instance, "xrGetVulkanGraphicsRequirementsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&get_requirements));
xrGetInstanceProcAddr(g.instance, "xrGetVulkanInstanceExtensionsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&get_extensions[0]));
// Both queries share one signature.
xrGetInstanceProcAddr(g.instance, "xrGetVulkanDeviceExtensionsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&get_extensions[1]));
xrGetInstanceProcAddr(g.instance, "xrGetVulkanGraphicsDeviceKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&g.get_graphics_device));
XrGraphicsRequirementsVulkanKHR requirements {XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR};
if (get_requirements == nullptr || get_extensions[0] == nullptr ||
get_extensions[1] == nullptr || g.get_graphics_device == nullptr ||
XR_FAILED(get_requirements(g.instance, g.system, &requirements))) {
DestroyInstance("the runtime does not support XR_KHR_vulkan_enable");
return;
}
for (int i = 0; i < 2; i++) {
uint32_t size = 0;
get_extensions[i](g.instance, g.system, 0, &size, nullptr);
std::string names(size, '\0');
get_extensions[i](g.instance, g.system, size, &size, names.data());
g.extensions[i] = Common::Split(names.c_str(), ' ');
}
XrViewConfigurationView views[2] {{XR_TYPE_VIEW_CONFIGURATION_VIEW},
{XR_TYPE_VIEW_CONFIGURATION_VIEW}};
uint32_t count = 0;
xrEnumerateViewConfigurationViews(g.instance, g.system,
XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 2, &count, views);
g.eye = {views[0].recommendedImageRectWidth, views[0].recommendedImageRectHeight};
XrSystemProperties properties {XR_TYPE_SYSTEM_PROPERTIES};
xrGetSystemProperties(g.instance, g.system, &properties);
Log::WriteToConsoleAndLog(fmt::format("OpenXR: headset '{}' ready\n", properties.systemName));
}
void OpenXrAppendExtensions(std::vector<const char*>& out,
const std::vector<vk::ExtensionProperties>& available, bool device) {
if (g.instance == XR_NULL_HANDLE) {
return;
}
for (const auto& name: g.extensions[device ? 1 : 0]) {
const auto same = [&](const char* other) { return name == other; };
if (std::any_of(available.begin(), available.end(),
[&](const auto& extension) { return same(extension.extensionName); }) &&
std::none_of(out.begin(), out.end(), same)) {
out.push_back(name.c_str());
}
}
}
bool OpenXrPhysicalDevice(vk::Instance instance, vk::PhysicalDevice& out) {
VkPhysicalDevice device = VK_NULL_HANDLE;
if (g.instance == XR_NULL_HANDLE ||
XR_FAILED(g.get_graphics_device(g.instance, g.system, static_cast<VkInstance>(instance),
&device))) {
return false;
}
out = vk::PhysicalDevice(device);
return true;
}
void OpenXrStartSession(GraphicContext& graphics) {
if (g.instance == XR_NULL_HANDLE) {
return;
}
// The runtime must be asked for its GPU before a session is created.
vk::PhysicalDevice required;
if (!OpenXrPhysicalDevice(graphics.instance, required) ||
required != graphics.physical_device) {
DestroyInstance("the headset runs on another GPU; select it with --gpu-index");
return;
}
g.graphics = &graphics;
XrGraphicsBindingVulkanKHR binding {XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR};
binding.instance = static_cast<VkInstance>(graphics.instance);
binding.physicalDevice = static_cast<VkPhysicalDevice>(graphics.physical_device);
binding.device = static_cast<VkDevice>(graphics.device);
binding.queueFamilyIndex = graphics.queue_family;
binding.queueIndex = 0;
XrSessionCreateInfo info {XR_TYPE_SESSION_CREATE_INFO};
info.next = &binding;
info.systemId = g.system;
XrReferenceSpaceCreateInfo space {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
space.poseInReferenceSpace = {{0, 0, 0, 1}, {0, 0, 0}};
const auto reference = [&](XrReferenceSpaceType type, XrSpace& out) {
space.referenceSpaceType = type;
return Ok(xrCreateReferenceSpace(g.session, &space, &out), "xrCreateReferenceSpace");
};
if (!Ok(xrCreateSession(g.instance, &info, &g.session), "xrCreateSession") ||
!reference(XR_REFERENCE_SPACE_TYPE_VIEW, g.view_space) ||
!reference(XR_REFERENCE_SPACE_TYPE_LOCAL, g.local_space) || !CreateSwapchain()) {
DestroyInstance("the headset session could not be created");
return;
}
if (!CreateInput()) {
g.action_set = XR_NULL_HANDLE;
}
vk::CommandPoolCreateInfo pool {};
pool.queueFamilyIndex = graphics.queue_family;
pool.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer;
(void)graphics.device.createCommandPool(&pool, nullptr, &g.pool);
vk::CommandBufferAllocateInfo allocate {};
allocate.commandPool = g.pool;
allocate.level = vk::CommandBufferLevel::ePrimary;
allocate.commandBufferCount = 1;
(void)graphics.device.allocateCommandBuffers(&allocate, &g.command);
vk::FenceCreateInfo fence {};
(void)graphics.device.createFence(&fence, nullptr, &g.fence);
g.running = true;
g.thread = std::thread(ThreadMain);
}
void OpenXrShutdown() {
g.running = false;
if (g.thread.joinable()) {
g.thread.join();
}
DestroyInstance(nullptr);
if (g.graphics == nullptr) {
return;
}
{
Common::LockGuard queue_lock(g.graphics->queue_mutex);
(void)g.graphics->device.waitIdle();
}
for (auto& slot: g.staging) {
if (slot.image.image != nullptr) {
g.graphics->DeleteImage(slot.image);
}
}
g.graphics->device.destroyFence(g.fence, nullptr);
g.graphics->device.destroyCommandPool(g.pool, nullptr);
g.graphics = nullptr;
}
VrPose OpenXrGetHeadPose() {
if (!g.running) {
return {};
}
std::lock_guard lock(g.input_mutex);
if (g.head.valid) {
g.last_head_read = g.head;
}
return g.head;
}
VrControllerInput OpenXrGetController(int hand) {
if (!g.running) {
return {};
}
std::lock_guard lock(g.input_mutex);
return g.input[hand];
}
void OpenXrQueueEyeSource(CommandBuffer& command, const VulkanImage& source) {
if (!g.running) {
return;
}
std::lock_guard lock(g.mutex);
int index = 0;
while (index == g.held || index == g.latest) {
index++;
}
auto& slot = g.staging[index];
const auto vk_command = command.Handle();
Barrier(vk_command, slot.image.image, vk::ImageLayout::eUndefined,
vk::ImageLayout::eTransferDstOptimal, 1, TRANSFER, TRANSFER_WRITE);
vk::ImageBlit blit {};
blit.srcSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
blit.srcOffsets[1] = vk::Offset3D {static_cast<int32_t>(source.extent.width),
static_cast<int32_t>(source.extent.height), 1};
blit.dstSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
blit.dstOffsets[1] = vk::Offset3D {static_cast<int32_t>(slot.image.extent.width),
static_cast<int32_t>(slot.image.extent.height), 1};
vk_command.blitImage(source.image, vk::ImageLayout::eTransferSrcOptimal, slot.image.image,
vk::ImageLayout::eTransferDstOptimal, 1, &blit, vk::Filter::eLinear);
Barrier(vk_command, slot.image.image, vk::ImageLayout::eTransferDstOptimal,
vk::ImageLayout::eTransferSrcOptimal, 1, TRANSFER_WRITE, TRANSFER_READ);
auto& scheduler = command.GetContext().GetCommandScheduler();
slot.tick = scheduler.CurrentTick();
g.semaphore = &scheduler.GetMasterSemaphore();
g.latest = index;
std::lock_guard input_lock(g.input_mutex);
slot.head = g.last_head_read;
}
} // namespace Libs::Vr
+41
View File
@@ -0,0 +1,41 @@
#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_OPENXRHEADSET_H_
#define EMULATOR_SRC_GRAPHICS_PRESENTATION_OPENXRHEADSET_H_
#include "graphics/host_gpu/vulkanCommon.h"
#include <vector>
namespace Libs::Graphics {
struct GraphicContext;
struct VulkanImage;
class CommandBuffer;
} // namespace Libs::Graphics
// Output to a real headset through OpenXR. Everything here is inert unless the virtual headset is
// enabled (--vr) and an OpenXR runtime with a headset is available; KYTY_OPENXR=0 turns it off.
namespace Libs::Vr {
// Creates the OpenXR instance. Call before the Vulkan instance exists, because the runtime names
// the Vulkan extensions and the GPU it needs.
void OpenXrInit();
// Appends the Vulkan instance or device extensions the runtime needs, when available.
void OpenXrAppendExtensions(std::vector<const char*>& out,
const std::vector<vk::ExtensionProperties>& available, bool device);
// The physical device the runtime renders with. False when OpenXR is not in use.
bool OpenXrPhysicalDevice(vk::Instance instance, vk::PhysicalDevice& out);
// Creates the session and starts the headset thread. Call once the device and queue exist.
void OpenXrStartSession(Graphics::GraphicContext& graphics);
// Stops the headset thread and destroys the session. Call before the device is destroyed.
void OpenXrShutdown();
// Render thread, headset flips only: scales the side-by-side eye image into a staging image for the
// headset thread. The source is in transfer-source layout.
void OpenXrQueueEyeSource(Graphics::CommandBuffer& command, const Graphics::VulkanImage& source);
} // namespace Libs::Vr
#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_OPENXRHEADSET_H_
+1 -1
View File
@@ -27,7 +27,7 @@ public:
KYTY_CLASS_NO_COPY(Presenter);
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info,
bool hdr = false);
bool hdr = false, bool headset = false);
[[nodiscard]] Frame& PrepareBlankFrame(uint32_t width, uint32_t height, bool opaque,
CommandBuffer* producer = nullptr);
[[nodiscard]] bool PresentLastFrame();
+69 -8
View File
@@ -21,6 +21,7 @@
#include "graphics/presentation/renderDoc.h"
#include "kernel/pthread.h"
#include "libs/errno.h"
#include "libs/hmd2.h"
#include "libs/libs.h"
#include "loader/systemContent.h"
@@ -51,6 +52,7 @@ constexpr int VIDEO_OUT_FALSE = 0;
constexpr int VIDEO_OUT_BUS_TYPE_MAIN = 0;
constexpr int VIDEO_OUT_BUS_TYPE_OVERLAY = 1;
constexpr int VIDEO_OUT_BUS_TYPE_SUB = 2;
constexpr int VIDEO_OUT_BUS_TYPE_HMD2_MAIN = 32;
constexpr int VIDEO_OUT_FLIP_MODE_VSYNC = 1;
constexpr int VIDEO_OUT_FLIP_MODE_VSYNC_MULTI = 4;
constexpr int VIDEO_OUT_FLIP_MODE_SLAVE = 8;
@@ -66,6 +68,7 @@ constexpr uint64_t VIDEO_OUT_REFRESH_RATE_59_94HZ = 3;
constexpr uint64_t VIDEO_OUT_REFRESH_RATE_119_88HZ = 13;
constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED = 0;
constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED = 1;
constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_HMD2 = 0x10;
constexpr uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY = 8;
constexpr uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED = 32;
@@ -252,8 +255,9 @@ private:
struct VideoOutDriver::Impl {
public:
static constexpr std::array VIDEO_OUT_BUSES {
VIDEO_OUT_BUS_TYPE_MAIN, VIDEO_OUT_BUS_TYPE_OVERLAY, VIDEO_OUT_BUS_TYPE_SUB};
static constexpr std::array VIDEO_OUT_BUSES {VIDEO_OUT_BUS_TYPE_MAIN,
VIDEO_OUT_BUS_TYPE_OVERLAY, VIDEO_OUT_BUS_TYPE_SUB,
VIDEO_OUT_BUS_TYPE_HMD2_MAIN};
static constexpr int VIDEO_OUT_NUM_MAX = VIDEO_OUT_BUSES.size() + 1;
Impl(uint32_t width, uint32_t height, Graphics::Presenter& presenter)
@@ -516,6 +520,10 @@ static bool IsValidFlipMode(int mode) {
mode == VIDEO_OUT_FLIP_MODE_SLAVE || mode == VIDEO_OUT_FLIP_MODE_MASTER;
}
// Set once the headset port shows eye images. The window then shows them instead of the main port,
// so the picture is visible without a headset.
static std::atomic<bool> g_headset_mirror {false};
static int ReserveFlipRequest(VideoOutDriver::Impl& driver, int handle, int index, int flip_mode,
int64_t flip_arg, FlipRequestSource source, uint64_t& request_id) {
auto* video_out = driver.Get(handle);
@@ -534,8 +542,10 @@ static int ReserveFlipRequest(VideoOutDriver::Impl& driver, int handle, int inde
(video_out->slave_count != 0) != (flip_mode == VIDEO_OUT_FLIP_MODE_MASTER)) {
return VIDEO_OUT_ERROR_INVALID_FLIP_MODE;
}
// The headset port can flip before its reprojection buffers are registered; those show blank.
if (video_out->closing ||
(!IsSpecialBufferIndex(index) && !video_out->buffers[index].Occupied())) {
(!IsSpecialBufferIndex(index) && !video_out->buffers[index].Occupied() &&
video_out->bus != VIDEO_OUT_BUS_TYPE_HMD2_MAIN)) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
}
if (!driver.GetFlipQueue().Reserve(*video_out, index, flip_arg, source, request_id)) {
@@ -740,6 +750,9 @@ bool VideoOutDriver::Impl::Close(int handle) {
master->slave_count--;
}
m_flip_queue.Cancel(config);
if (config.bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN) {
g_headset_mirror = false;
}
m_presenter.ClearLayer(config.bus);
DeleteVideoOutEvents(flip_events, VideoOutEventKind::Flip);
DeleteVideoOutEvents(pre_vblank_events, VideoOutEventKind::PreVblankStart);
@@ -1066,16 +1079,19 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
index = request->index;
}
const bool special = IsSpecialBufferIndex(index);
bool special = IsSpecialBufferIndex(index);
Graphics::ImageInfo source_info;
uint32_t width = 0;
uint32_t height = 0;
bool current = false;
bool premultiplied_alpha = false;
bool hdr = false;
bool headset = false;
{
Common::LockGuard lock(cfg->mutex);
current = cfg->opened && !cfg->closing && cfg->generation == generation;
headset = cfg->bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN;
special = special || (headset && !cfg->buffers[index].Occupied());
if (current && special) {
width = cfg->width;
height = cfg->height;
@@ -1120,7 +1136,10 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
frame = &m_presenter.PrepareBlankFrame(width, height, index == VIDEO_OUT_BUFFER_INDEX_BLACK,
&buffer);
} else {
frame = &m_presenter.PrepareFrame(buffer, source_info, hdr);
frame = &m_presenter.PrepareFrame(buffer, source_info, hdr, headset);
if (headset) {
g_headset_mirror = true;
}
}
Common::LockGuard lock(m_mutex);
@@ -1239,6 +1258,8 @@ bool FlipQueue::Flip(uint32_t micros) {
[](const auto& a, const auto& b) { return a.cfg->bus < b.cfg->bus; });
bool due = true;
std::array<Graphics::Presenter::Layer, VideoOutDriver::Impl::VIDEO_OUT_BUSES.size()> layers {};
size_t layer_count = 0;
Graphics::Presenter::Frame* hidden = nullptr; // The main port behind the headset image.
for (size_t i = 0; i < count; i++) {
auto& r = requests[i];
r.cfg->mutex.Lock();
@@ -1246,10 +1267,22 @@ bool FlipQueue::Flip(uint32_t micros) {
if (r.id == group) {
due &= IsFlipDueLocked(*r.cfg, r.generation);
}
layers[i] = {r.frame, r.cfg->bus, r.premultiplied_alpha};
if (r.cfg->bus == VIDEO_OUT_BUS_TYPE_MAIN &&
g_headset_mirror.load(std::memory_order_relaxed)) {
hidden = r.frame;
} else {
const bool headset = r.cfg->bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN;
layers[layer_count++] = {r.frame, headset ? VIDEO_OUT_BUS_TYPE_MAIN : r.cfg->bus,
r.premultiplied_alpha};
}
}
if (due) {
m_presenter.Present(std::span(layers.data(), count));
if (hidden != nullptr) {
m_presenter.Discard(*hidden);
}
if (layer_count != 0) {
m_presenter.Present(std::span(layers.data(), layer_count));
}
if (requests[0].cfg->bus == 0) {
Graphics::PerformanceOverlayRecordFlip(requests[0].cfg->width, requests[0].cfg->height);
}
@@ -1461,6 +1494,33 @@ KYTY_SYSV_ABI int VideoOutRegisterBuffers2(int handle, int set_index, int buffer
if (option != nullptr) {
return VIDEO_OUT_ERROR_INVALID_OPTION;
}
// The headset port takes reprojection buffers with a zeroed attribute. Each names its eye
// texture, which is registered and flipped like a regular surface.
const bool headset = ctx->bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN;
if (headset != (category == VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_HMD2)) {
return VIDEO_OUT_ERROR_INVALID_CATEGORY;
}
VideoOutBufferAttribute2 headset_attribute {};
std::array<VideoOutBuffers, VIDEO_OUT_BUFFER_NUM_MAX> headset_buffers {};
if (headset) {
for (int i = 0; i < buffer_num; i++) {
Hmd2::ReprojectionSurface surface {};
if (buffers[i].metadata != nullptr ||
!Hmd2::GetReprojectionSurface(buffers[i].data, surface) ||
(i != 0 && (surface.width != headset_attribute.width ||
surface.height != headset_attribute.height ||
surface.pixel_format != headset_attribute.pixel_format))) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
}
headset_attribute.pixel_format = surface.pixel_format;
headset_attribute.width = surface.width;
headset_attribute.height = surface.height;
headset_buffers[i].data = reinterpret_cast<const void*>(surface.address);
}
attribute = &headset_attribute;
buffers = headset_buffers.data();
category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED;
}
if (category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED &&
category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED) {
return VIDEO_OUT_ERROR_INVALID_CATEGORY;
@@ -1871,7 +1931,8 @@ static int ValidateOutputConfig(int handle, uint64_t mode, const VideoOutOutputO
}
}
if (mode != VIDEO_OUT_OUTPUT_MODE_DEFAULT && mode != VIDEO_OUT_OUTPUT_MODE_119_88HZ) {
if (mode != VIDEO_OUT_OUTPUT_MODE_DEFAULT && mode != VIDEO_OUT_OUTPUT_MODE_119_88HZ &&
mode != Hmd2::OUTPUT_MODE_89_91HZ && mode != Hmd2::OUTPUT_MODE_119_88HZ) {
return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE;
}
+36
View File
@@ -0,0 +1,36 @@
#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_VRINPUT_H_
#define EMULATOR_SRC_GRAPHICS_PRESENTATION_VRINPUT_H_
namespace Libs::Vr {
// A pose in the tracking space: metres, right-handed, +y up, -z forward (the OpenXR convention).
struct VrPose {
bool valid = false;
float orientation[4] = {0.0f, 0.0f, 0.0f, 1.0f}; // x, y, z, w
float position[3] = {0.0f, 0.0f, 0.0f};
};
// One headset controller. Sticks are -1..1 with +y up, the trigger and squeeze 0..1.
struct VrControllerInput {
bool connected = false;
VrPose pose;
float trigger = 0.0f;
float squeeze = 0.0f;
float stick_x = 0.0f;
float stick_y = 0.0f;
bool primary = false; // X on the left controller, A on the right one.
bool secondary = false; // Y on the left controller, B on the right one.
bool stick_click = false;
bool menu = false;
};
// Latest head pose; not valid, at the origin, while no headset session tracks the head.
[[nodiscard]] VrPose OpenXrGetHeadPose();
// Latest state of the left (0) or right (1) controller. While it is not connected the caller keeps
// using the keyboard or gamepad.
[[nodiscard]] VrControllerInput OpenXrGetController(int hand);
} // namespace Libs::Vr
#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_VRINPUT_H_
@@ -11,6 +11,7 @@
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/presentation/openxrHeadset.h"
#include "graphics/presentation/performanceOverlay.h"
#include "graphics/presentation/presenter.h"
#include "graphics/presentation/systemOverlay.h"
@@ -918,7 +919,8 @@ Presenter::Presenter(WindowContext& window): m_impl(std::make_unique<Impl>(windo
Presenter::~Presenter() = default;
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info, bool hdr) {
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info, bool hdr,
bool headset) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(buffer.IsInvalid());
auto frame_format = info.pixel_format;
@@ -938,6 +940,12 @@ Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo
frame->Configure(m_impl->window.graphic_ctx,
{image.backing.extent.width, image.backing.extent.height}, frame_format);
frame->CopyFrom(buffer, image);
if (headset) {
// The frame holds the raw display-encoded pixels the headset needs.
frame->Transit(buffer.Handle(), vk::ImageLayout::eTransferSrcOptimal,
vk::AccessFlagBits2::eTransferRead);
Vr::OpenXrQueueEyeSource(buffer, frame->image);
}
return *frame;
}
@@ -1,6 +1,3 @@
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include "common/assert.h"
#include "common/common.h"
#include "common/emulatorConfig.h"
@@ -14,6 +11,7 @@
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/presentation/openxrHeadset.h"
#include "graphics/presentation/presenter.h"
#include "graphics/presentation/systemOverlay.h"
#include "graphics/presentation/videoOut.h"
@@ -23,6 +21,8 @@
#include "libs/controller.h"
#include "loader/systemContent.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include <algorithm>
#include <cstdio>
#include <cstring>
@@ -170,6 +170,8 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
LOGF("Vulkan GPU index %d is unavailable; selecting automatically\n",
Config::GetGpuIndex());
}
} else if (vk::PhysicalDevice headset; Vr::OpenXrPhysicalDevice(instance, headset)) {
devices = {headset}; // The headset runtime renders on this GPU.
}
vk::PhysicalDevice best_device = nullptr;
@@ -876,8 +878,11 @@ void WindowContext::CreateVulkan() {
}
VULKAN_HPP_DEFAULT_DISPATCHER.init(get_instance_proc_addr);
Vr::OpenXrInit();
VulkanExtensions r;
VulkanGetExtensions(r);
Vr::OpenXrAppendExtensions(r.required_extensions, r.available_extensions, false);
VulkanCheckInstanceVersion();
vk::ApplicationInfo app_info {};
@@ -1051,6 +1056,7 @@ void WindowContext::CreateVulkan() {
device_extensions.push_back(VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME);
device_extensions.push_back(VK_EXT_ATTACHMENT_FEEDBACK_LOOP_DYNAMIC_STATE_EXTENSION_NAME);
}
Vr::OpenXrAppendExtensions(device_extensions, available_extensions, true);
}
graphic_ctx.device = VulkanCreateDevice(graphic_ctx, device_extensions);
@@ -1068,6 +1074,7 @@ void WindowContext::CreateVulkan() {
render_context = std::make_unique<RenderContext>(graphic_ctx);
LibKernel::Memory::InstallGpuResources(render_context.get());
presenter = std::make_unique<Presenter>(*this);
Vr::OpenXrStartSession(graphic_ctx);
}
void WindowContext::RefreshSurfaceCapabilities() {
@@ -1090,6 +1097,7 @@ void WindowContext::RecreateSurface() {
}
WindowContext::~WindowContext() {
Vr::OpenXrShutdown();
ShutdownSystemOverlayInput();
presenter.reset();
LibKernel::Memory::InstallGpuResources(nullptr);
@@ -209,20 +209,20 @@ int BufferTableAttribFromOffset(uint32_t raw_offset, int dword) {
}
Frontend::EmbeddedFetchPlan DetectEmbeddedVertexFetch(
const Decoder::Program& decoded, const ShaderVertexInputInfo* input_info,
const Decoder::Program& decoded, const ShaderVertexInputInfo& input_info,
uint32_t user_data_base, uint32_t user_data_count, uint32_t wave_size) {
const uint32_t vertex_index_reg = input_info->logical_stage == ShaderType::Local ? 2u : 5u;
const uint32_t instance_index_reg = input_info->logical_stage == ShaderType::Local ? 5u : 8u;
const uint32_t vertex_index_reg = input_info.logical_stage == ShaderType::Local ? 2u : 5u;
const uint32_t instance_index_reg = input_info.logical_stage == ShaderType::Local ? 5u : 8u;
Frontend::EmbeddedFetchPlan data;
data.loads.reserve(input_info->resources_num);
data.loads.reserve(input_info.resources_num);
int32_t vertex_offset_candidate = -1;
int32_t instance_offset_candidate = -1;
bool vertex_offset_conflict = false;
bool instance_offset_conflict = false;
const int shift_regs = 8;
const int attrib_reg = input_info->fetch_attrib_reg + shift_regs;
const int buffer_reg = input_info->fetch_buffer_reg + shift_regs;
const int attrib_reg = input_info.fetch_attrib_reg + shift_regs;
const int buffer_reg = input_info.fetch_buffer_reg + shift_regs;
std::array<EmbeddedFetchSgprInfo, 108> sgprs {};
std::array<bool, 256> vgpr_is_index {};
@@ -488,7 +488,6 @@ void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
if (source.decoded.instructions[i].opcode != Decoder::Opcode::S_SWAPPC_B64) continue;
source.calls.push_back({.instruction = i});
}
EXIT_IF(options.input_info.compute == nullptr);
const auto graph = CFG::BuildGraph(source.decoded);
Program query;
query.stage = options.stage;
@@ -809,9 +808,9 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
const auto& cfg = *selected_cfg;
Frontend::EmbeddedFetchPlan embedded_fetch;
if ((options.stage == ShaderType::Vertex || options.stage == ShaderType::Local) &&
options.input_info.vertex != nullptr && options.input_info.vertex->fetch_embedded) {
options.input_info.vertex->fetch_embedded) {
embedded_fetch = DetectEmbeddedVertexFetch(
decoded, options.input_info.vertex, options.user_data_base,
decoded, *options.input_info.vertex, options.user_data_base,
static_cast<uint32_t>(options.user_data.size()), options.wave_size);
if (!embedded_fetch.loads.empty()) {
LOGF("%s embedded vertex fetch plan: detected=%" PRIu64 "\n", GetDumpLabel(options),
+98 -33
View File
@@ -21,14 +21,20 @@ struct TessellationAddress {
Kind kind = Kind::Unknown;
uint32_t coefficient = 0;
uint32_t constant = 0;
bool operator==(const TessellationAddress&) const = default;
};
uint32_t ReflectTessellationStride(const Decoder::Program& program, bool local,
uint32_t control_points, uint32_t input_stride) {
// Reflects the LS output stride, or the HS output stride and patch constant range.
void ReflectTessellationLayout(const Decoder::Program& program, bool local,
ShaderTessellationInputInfo& info) {
using namespace Decoder;
using Address = TessellationAddress;
using Kind = Address::Kind;
std::array<Address, IR::NumVectorRegs> registers {};
using Address = TessellationAddress;
using Kind = Address::Kind;
using Registers = std::array<Address, IR::NumVectorRegs>;
const auto control_points = local ? info.input_control_points : info.output_control_points;
Registers registers {};
registers[local ? 3u : 1u] =
local ? Address {Kind::Affine, 1u, 0u} : Address {Kind::PackedControlPoint};
const auto constant = [](uint32_t value) { return Address {Kind::Affine, 0u, value}; };
@@ -79,14 +85,28 @@ uint32_t ReflectTessellationStride(const Decoder::Program& program, bool local,
? value
: Address {};
};
uint32_t stride = 0;
// A forward branch joins its target with the fallthrough path. Definitions that
// differ between the paths are unknown there.
std::unordered_map<uint32_t, Registers> joins;
const auto merge = [](Registers& into, const Registers& from) {
for (uint32_t index = 0; index < into.size(); index++) {
if (into[index] != from[index]) into[index] = {};
}
};
uint32_t stride = 0, patch_begin = UINT32_MAX, patch_end = 0;
for (const auto& inst: program.instructions) {
// Stage exits preserve the active path's definitions. An internal join
// would require merging register definitions.
EXIT_NOT_IMPLEMENTED(IsDirectBranch(inst.opcode) &&
inst.branch_target != program.instructions.back().pc);
const bool local_store =
inst.opcode == Opcode::DS_WRITE_B32 || inst.opcode == Opcode::DS_WRITE2_B32;
if (const auto join = joins.find(inst.pc); join != joins.end()) {
merge(registers, join->second);
joins.erase(join);
}
if (IsDirectBranch(inst.opcode)) {
EXIT_NOT_IMPLEMENTED(inst.branch_target <= inst.pc);
if (const auto [join, inserted] = joins.try_emplace(inst.branch_target, registers);
!inserted)
merge(join->second, registers);
}
const bool local_store = IsDsWriteOpcode(inst.opcode);
const bool buffer_store = inst.opcode == Opcode::BUFFER_STORE_DWORD ||
inst.opcode == Opcode::BUFFER_STORE_DWORDX2 ||
inst.opcode == Opcode::BUFFER_STORE_DWORDX3 ||
@@ -102,10 +122,15 @@ uint32_t ReflectTessellationStride(const Decoder::Program& program, bool local,
inst.pc);
}
if (address.coefficient != 0u) {
const auto expected = control_read ? input_stride : stride;
const auto expected = control_read ? info.ls_stride : stride;
EXIT_NOT_IMPLEMENTED((address.coefficient & 3u) != 0u ||
(expected != 0u && expected != address.coefficient));
if (!control_read) stride = address.coefficient;
} else if (control_store) {
// Control-point-independent HS outputs are patch constants.
const auto begin = address.constant + inst.offset;
patch_begin = std::min(patch_begin, begin);
patch_end = std::max(patch_end, begin + 4u * inst.data_dwords);
}
}
// Store vdata is a source, despite occupying the decoder's dst field.
@@ -123,6 +148,13 @@ uint32_t ReflectTessellationStride(const Decoder::Program& program, bool local,
if (rhs.constant == 0u && third.constant == 8u) value = constant(0u);
}
break;
case Opcode::V_AND_B32:
if ((lhs.kind == Kind::PackedControlPoint && rhs == constant(0xffu)) ||
(rhs.kind == Kind::PackedControlPoint && lhs == constant(0xffu)))
value = constant(0u);
break;
case Opcode::V_ADD_NC_U32: value = add(lhs, rhs); break;
case Opcode::V_MUL_LO_U32: value = multiply(lhs, rhs); break;
case Opcode::V_MUL_U32_U24: value = multiply(low24(lhs), low24(rhs)); break;
case Opcode::V_MAD_U32_U24: value = add(multiply(low24(lhs), low24(rhs)), third); break;
case Opcode::V_LSHL_ADD_U32:
@@ -149,8 +181,13 @@ uint32_t ReflectTessellationStride(const Decoder::Program& program, bool local,
}
registers.at(inst.dst.reg) = value;
}
EXIT_NOT_IMPLEMENTED(stride == 0u);
return stride;
EXIT_NOT_IMPLEMENTED(!joins.empty() || stride == 0u);
(local ? info.ls_stride : info.hs_stride) = stride;
if (patch_end != 0u) {
EXIT_NOT_IMPLEMENTED(patch_begin < stride * control_points);
info.patch_base = patch_begin;
info.patch_size = patch_end - patch_begin;
}
}
const IR::Inst* TessellationBufferBase(const IR::Inst& inst) {
@@ -161,6 +198,15 @@ const IR::Inst* TessellationBufferBase(const IR::Inst& inst) {
: nullptr;
}
// Whether an access predicate, possibly a conjunction of EXEC masks, implies a condition.
bool Implies(IR::Value predicate, IR::Value condition) {
predicate = predicate.Resolve();
if (predicate == condition.Resolve()) return true;
const auto* inst = predicate.TryInstruction();
return inst != nullptr && inst->GetOpcode() == IR::ValueOpcode::LogicalAnd &&
(Implies(inst->Arg(0), condition) || Implies(inst->Arg(1), condition));
}
IR::Value ActiveAddress(IR::Block& block, IR::Block::iterator before, IR::Value value,
IR::Value predicate, std::unordered_map<IR::Inst*, IR::Value>& resolved) {
using namespace IR;
@@ -169,18 +215,28 @@ IR::Value ActiveAddress(IR::Block& block, IR::Block::iterator before, IR::Value
if (source == nullptr) return value;
if (const auto found = resolved.find(source); found != resolved.end()) return found->second;
if (source->GetOpcode() == ValueOpcode::SelectU32) {
return source->Arg(0).Resolve() == predicate.Resolve()
return Implies(predicate, source->Arg(0))
? ActiveAddress(block, before, source->Arg(1), predicate, resolved)
: value;
}
if (source->GetOpcode() != ValueOpcode::IAdd32 && source->GetOpcode() != ValueOpcode::ISub32)
// Pure address arithmetic that stays behind EXEC selects; mul24 lowers to IMul32 of BFEs.
const auto opcode = source->GetOpcode();
if (opcode != ValueOpcode::IAdd32 && opcode != ValueOpcode::ISub32 &&
opcode != ValueOpcode::IMul32 && opcode != ValueOpcode::BitFieldUExtract)
return value;
const auto lhs = ActiveAddress(block, before, source->Arg(0), predicate, resolved);
const auto rhs = ActiveAddress(block, before, source->Arg(1), predicate, resolved);
if (lhs != source->Arg(0).Resolve() || rhs != source->Arg(1).Resolve()) {
auto copy = block.PrependNewInst(before, source->GetOpcode(), {lhs, rhs},
source->Flags<uint64_t>());
value = Value(&*copy);
std::array<Value, 3> args;
bool changed = false;
for (size_t index = 0; index < source->NumArgs(); index++) {
args[index] = ActiveAddress(block, before, source->Arg(index), predicate, resolved);
changed |= args[index] != source->Arg(index).Resolve();
}
if (changed) {
const auto flags = source->Flags<uint64_t>();
const auto copy =
source->NumArgs() == 2u
? block.PrependNewInst(before, opcode, {args[0], args[1]}, flags)
: block.PrependNewInst(before, opcode, {args[0], args[1], args[2]}, flags);
value = Value(&*copy);
}
resolved.emplace(source, value);
return value;
@@ -193,11 +249,12 @@ void AnalyzeTessellationPrograms(std::span<const uint32_t> local, std::span<cons
const auto local_program = Decoder::DecodeFrontProgram(local);
Decoder::Program control_program;
Decoder::DecodeProgram(control, control_program);
info.ls_stride = ReflectTessellationStride(local_program, true, info.input_control_points, 0u);
info.hs_stride = ReflectTessellationStride(control_program, false, info.output_control_points,
info.ls_stride);
LOGF("Tessellation interface: input_cp=%u output_cp=%u ls_stride=%u hs_stride=%u\n",
info.input_control_points, info.output_control_points, info.ls_stride, info.hs_stride);
ReflectTessellationLayout(local_program, true, info);
ReflectTessellationLayout(control_program, false, info);
LOGF("Tessellation interface: input_cp=%u output_cp=%u ls_stride=%u hs_stride=%u "
"patch_base=0x%x patch_size=%u\n",
info.input_control_points, info.output_control_points, info.ls_stride, info.hs_stride,
info.patch_base, info.patch_size);
}
void LowerTessellationMemory(IR::Program& program, const CompileOptions& options) {
@@ -218,7 +275,7 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
}
}
ConstantPropagationPass(program.blocks, program.wave_size);
uint32_t reads = 0, writes = 0, factors = 0;
uint32_t reads = 0, writes = 0, factors = 0, patches = 0;
for (auto* block: program.blocks) {
for (auto it = block->begin(); it != block->end(); ++it) {
auto& inst = *it;
@@ -268,8 +325,13 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
: options.stage != ShaderType::TessellationEvaluation);
if (address.IsImmediate() &&
address.U32() >= tess.hs_stride * tess.output_control_points) {
EXIT_NOT_IMPLEMENTED(!write);
kind = TessellationAttribute::PatchOutput;
const auto begin = address.U32() + memory.offset;
EXIT_NOT_IMPLEMENTED(begin < tess.patch_base ||
begin + 4u * components >
tess.patch_base + tess.patch_size);
kind = write ? TessellationAttribute::PatchOutput
: TessellationAttribute::PatchInput;
patches += components;
}
}
} else {
@@ -320,11 +382,14 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
ConstantPropagationPass(program.blocks, program.wave_size);
RemoveIdentities(program);
EliminateDeadCode(program.blocks);
LOGF("%s tessellation lowering: reads=%u writes=%u factors=%u\n",
// Patch constant stores whose EXEC selects were not stripped would land in control points.
EXIT_NOT_IMPLEMENTED(options.stage == ShaderType::TessellationControl &&
tess.patch_size != 0u && patches == 0u);
LOGF("%s tessellation lowering: reads=%u writes=%u factors=%u patches=%u\n",
options.stage == ShaderType::Local ? "LS"
: options.stage == ShaderType::TessellationControl ? "HS"
: "TES",
reads, writes, factors);
reads, writes, factors, patches);
}
} // namespace Libs::Graphics::ShaderRecompiler
@@ -233,8 +233,8 @@ std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo inp
? 2u
: 1u;
DefineModule(state);
ValidateNativeProgram(program, program.stage == ShaderType::Compute &&
input_info.compute != nullptr && input_info.compute->lds_storage);
ValidateNativeProgram(program,
program.stage == ShaderType::Compute && input_info.compute->lds_storage);
EmitProgram(state);
state.builder.AddEntryPoint(ExecutionModelForStage(state.program.stage), state.main_func,
"main", state.interface_variables);
@@ -53,8 +53,7 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
const auto bits = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, TypeBool(state), value, variable);
// SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS selects uint masks or float +/-1.
const bool all_bits =
state.input_info.pixel != nullptr && state.input_info.pixel->ps_front_face_all_bits;
const bool all_bits = state.input_info.pixel->ps_front_face_all_bits;
const auto front = ConstantU32(state, all_bits ? 0xffffffffu : 0x3f800000u);
const auto back = ConstantU32(state, all_bits ? 0u : 0xbf800000u);
state.builder.AddFunction(spv::OpSelect, TypeU32(state), bits, value, front, back);
@@ -350,6 +349,38 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
return vector;
}
uint32_t DistancePointer(EmitterState& state, IR::StageOutputKind kind, uint32_t index) {
if (state.program.stage == ShaderType::Mesh) {
return MeshOutputPointer(state, kind, index);
}
const auto variable = kind == IR::StageOutputKind::ClipDistance ? state.clip_distance_variable
: state.cull_distance_variable;
const auto pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain,
TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer,
variable, ConstantU32(state, index));
return pointer;
}
// The clipper keeps the half space where dot(plane, position) >= 0 for each UCP_ENA plane.
void EmitUserClipDistances(EmitterState& state, uint32_t position) {
uint32_t index = 0;
for (auto mask = state.user_clip_planes; mask != 0; mask &= mask - 1u) {
const auto dword = IR::PushData::UserClipPlaneDword + 4u * std::countr_zero(mask);
uint32_t components[4] {};
for (uint32_t i = 0; i < 4; i++) {
components[i] = Unary(state, spv::OpBitcast, TypeF32(state),
EmitPushDataLoad(state, ConstantU32(state, dword + i)));
}
const auto plane = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4), plane,
components[0], components[1], components[2], components[3]);
state.builder.AddFunction(
spv::OpStore, DistancePointer(state, IR::StageOutputKind::ClipDistance, index++),
Binary(state, spv::OpDot, TypeF32(state), plane, position));
}
}
void EmitAuxPositionExport(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo& exp) {
auto& state = ctx.state;
for (uint32_t component = 0; component < 4; component++) {
@@ -390,23 +421,13 @@ void EmitAuxPositionExport(ValueEmitContext& ctx, uint32_t data, const IR::Expor
state.builder.AddFunction(spv::OpStore, state.point_size_variable, f32);
continue;
}
auto StoreDistance = [&](IR::StageOutputKind kind, uint32_t variable, uint32_t index) {
if (index == UINT32_MAX) {
return;
auto StoreDistance = [&](IR::StageOutputKind kind, uint32_t index) {
if (index != UINT32_MAX) {
state.builder.AddFunction(spv::OpStore, DistancePointer(state, kind, index), f32);
}
uint32_t pointer;
if (state.program.stage == ShaderType::Mesh) {
pointer = MeshOutputPointer(state, kind, index);
} else {
pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain,
TypePointer(state, spv::StorageClassOutput, TypeF32(state)),
pointer, variable, ConstantU32(state, index));
}
state.builder.AddFunction(spv::OpStore, pointer, f32);
};
StoreDistance(IR::StageOutputKind::ClipDistance, state.clip_distance_variable, output.clip_distance);
StoreDistance(IR::StageOutputKind::CullDistance, state.cull_distance_variable, output.cull_distance);
StoreDistance(IR::StageOutputKind::ClipDistance, output.clip_distance);
StoreDistance(IR::StageOutputKind::CullDistance, output.cull_distance);
}
}
@@ -597,6 +618,9 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
state.input_info.vertex->clip_space.enabled) {
value = ConvertPositionToClipSpace(state, value);
}
if (exp.kind == IR::ExportTargetKind::Position && state.user_clip_planes != 0) {
EmitUserClipDistances(state, value);
}
if (state.program.stage == ShaderType::Mesh) {
const auto kind = exp.kind == IR::ExportTargetKind::Position
? IR::StageOutputKind::Position
@@ -609,7 +633,6 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto equal = state.builder.AllocateId();
const auto invalid = state.builder.AllocateId();
const auto distance = state.builder.AllocateId();
const auto distance_pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpFOrdEqual, TypeBoolVector(state, 4), equal,
value, zero);
state.builder.AddFunction(spv::OpAll, TypeBool(state), invalid, equal);
@@ -618,11 +641,10 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
state.builder.AddFunction(spv::OpSelect, TypeF32(state), distance, invalid,
ConstantF32Value(state, -1.0f),
ConstantF32Value(state, 0.0f));
state.builder.AddFunction(
spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)),
distance_pointer, state.clip_distance_variable,
ConstantU32(state, state.invalid_position_clip_distance));
state.builder.AddFunction(spv::OpStore, distance_pointer, distance);
state.builder.AddFunction(spv::OpStore,
DistancePointer(state, IR::StageOutputKind::ClipDistance,
state.invalid_position_clip_distance),
distance);
static std::atomic_bool logged = false;
if (!logged.exchange(true, std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(
@@ -80,7 +80,7 @@ struct EmitterState {
lds_storage_class = spv::StorageClassWorkgroup;
}
if (program.info.uses_lds && program.stage == ShaderType::Compute &&
input_info.compute != nullptr && input_info.compute->lds_storage) {
input_info.compute->lds_storage) {
lds_storage_class = spv::StorageClassStorageBuffer;
}
}
@@ -104,9 +104,8 @@ struct EmitterState {
std::array<uint32_t, 3> u32_vector_types {};
std::array<uint32_t, 3> i32_vector_types {};
std::array<uint32_t, 3> f32_vector_types {};
std::array<uint32_t, 6> tess_variables {};
std::array<uint32_t, IR::NumTessellationAttributes> tess_variables {};
uint32_t tess_inner_variable = 0;
uint32_t tess_patch_base = 0;
uint32_t lane_count = 1;
uint32_t lane_half = 0;
@@ -148,6 +147,7 @@ struct EmitterState {
uint32_t point_size_variable = 0;
uint32_t clip_distance_variable = 0;
uint32_t invalid_position_clip_distance = UINT32_MAX;
uint32_t user_clip_planes = 0;
uint32_t cull_distance_variable = 0;
uint32_t layer_variable = 0;
uint32_t viewport_index_variable = 0;
@@ -157,9 +157,25 @@ void EmitMemoryOffsets(EmitterState& state) {
}
}
// Stages without a workgroup emulate LDS with one private array per invocation. A GPU keeps
// hundreds of thousands of invocations in flight, so this size multiplies into driver local memory:
// a fixed 8192 dwords (32 KB) asked for gigabytes and killed the device (a write fault in the
// first heavy pixel shader that touched LDS). A pixel shader can only address the LDS that
// EXTRA_LDS_SIZE reserves, a vertex shader the LDS_SIZE of its GS stage; hardware reads zero and
// drops writes beyond it, as the bounds checks do.
constexpr uint32_t FUNCTION_LDS_DWORDS = 1024u;
uint32_t LdsDwordCount(const EmitterState& state) {
const auto* workgroup = ShaderWorkgroupInput(state.program.stage, state.input_info);
return workgroup != nullptr ? workgroup->lds_size_dwords : 8192u;
if (const auto* workgroup = ShaderWorkgroupInput(state.program.stage, state.input_info)) {
return workgroup->lds_size_dwords;
}
if (state.program.stage == ShaderType::Pixel) {
return std::max(state.input_info.pixel->lds_size_dwords, 1u);
}
if (state.program.stage == ShaderType::Vertex) {
return std::max(state.input_info.vertex->lds_size_dwords, 1u);
}
return FUNCTION_LDS_DWORDS;
}
void EnsureLdsStorage(EmitterState& state) {
@@ -244,7 +244,7 @@ void DefineDescriptors(EmitterState& state) {
counts[static_cast<size_t>(Kind::ShaderData)] =
bindings.ShaderDataDwords() != 0 && !bindings.UsesPushData();
if (state.program.bindings.UsesPushData() || state.program.stage == ShaderType::Mesh ||
state.object_primitive_id_variable != 0) {
state.object_primitive_id_variable != 0 || state.user_clip_planes != 0) {
const auto type = PushConstantBlockType(state);
state.push_constant_variable = state.builder.DefineGlobalVariable(
TypePointer(state, spv::StorageClassPushConstant, type), spv::StorageClassPushConstant);
@@ -600,14 +600,24 @@ void DefineOutputs(EmitterState& state) {
cull_distance_count = std::max(cull_distance_count, output.index + 1);
}
}
const bool position = std::ranges::any_of(state.outputs, [](const OutputBinding& output) {
return output.kind == IR::StageOutputKind::Position;
});
if (position && state.input_info.vertex->user_clip_planes != 0) {
// PipelineCache rejects UCP_ENA with shader clip or cull distances.
EXIT_IF(clip_distance_count + cull_distance_count != 0);
state.user_clip_planes = state.input_info.vertex->user_clip_planes;
for (auto mask = state.user_clip_planes; mask != 0; mask &= mask - 1u) {
state.outputs.push_back(
{{IR::StageOutputKind::ClipDistance, clip_distance_count++, 0, "gl_ClipDistance"}});
}
}
if (state.program.stage == ShaderType::Mesh) {
DefineMeshOutputs(state, clip_distance_count, cull_distance_count);
return;
}
if (state.program.stage == ShaderType::Vertex && clip_distance_count + cull_distance_count < 8u &&
std::ranges::any_of(state.outputs, [](const OutputBinding& output) {
return output.kind == IR::StageOutputKind::Position;
})) {
if (state.program.stage == ShaderType::Vertex &&
clip_distance_count + cull_distance_count < 8u && position) {
// Reserve one plane for the enabled PA_CL_CLIP_CNTL clipping-error cull.
state.invalid_position_clip_distance = clip_distance_count++;
state.outputs.push_back({{IR::StageOutputKind::ClipDistance,
@@ -623,6 +623,12 @@ void EmitProgram(EmitterState& state) {
}
if (state.program.dispatcher_fallback) {
auto& dispatch = dispatcher.emplace();
// Allocate both halves in program order so the SPIR-V is identical across runs.
const auto add_spill = [&](const IR::Inst* inst) {
for (uint32_t half = 0; half < state.lane_count; half++) {
dispatch.spills[half].emplace(inst, state.builder.AllocateId());
}
};
for (const auto* block: program.blocks) {
for (const auto& inst: *block) {
if (inst.GetOpcode() != IR::ValueOpcode::Phi) {
@@ -632,7 +638,7 @@ void EmitProgram(EmitterState& state) {
ctx.Fail(inst, "cannot be stored by the dispatcher");
break;
}
dispatch.spills[0].emplace(&inst, state.builder.AllocateId());
add_spill(&inst);
}
}
const auto mark_cross_block = [&](IR::Value value, const IR::Block* consumer) {
@@ -647,7 +653,7 @@ void EmitProgram(EmitterState& state) {
return;
}
if (!dispatch.spills[0].contains(definition)) {
dispatch.spills[0].emplace(definition, state.builder.AllocateId());
add_spill(definition);
}
};
for (const auto* block: program.blocks) {
@@ -669,12 +675,7 @@ void EmitProgram(EmitterState& state) {
dispatch.continue_label = state.builder.AllocateId();
dispatch.merge_label = state.builder.AllocateId();
ctx.dispatcher_spills = &dispatch.spills[0];
if (state.lane_count == 2) {
for (const auto& [inst, id]: dispatch.spills[0]) {
dispatch.spills[1].emplace(inst, state.builder.AllocateId());
}
high.dispatcher_spills = &dispatch.spills[1];
}
high.dispatcher_spills = &dispatch.spills[1];
}
DefineGetBdaPointer(state);
DefineIndirectBufferLoads(ctx);
@@ -5,6 +5,17 @@
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
namespace {
bool IsTessellationInput(IR::TessellationAttribute kind) {
using Attribute = IR::TessellationAttribute;
return kind == Attribute::ControlInput || kind == Attribute::EvaluationInput ||
kind == Attribute::PatchInput;
}
bool IsTessellationPatch(IR::TessellationAttribute kind) {
using Attribute = IR::TessellationAttribute;
return kind == Attribute::PatchOutput || kind == Attribute::PatchInput;
}
uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
using Attribute = IR::TessellationAttribute;
@@ -12,8 +23,9 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto& tess = state.input_info.vertex->tess;
const auto variable = state.tess_variables.at(static_cast<uint32_t>(kind));
EXIT_IF(variable == 0);
const auto input = kind == Attribute::ControlInput || kind == Attribute::EvaluationInput;
const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput;
const bool patch = IsTessellationPatch(kind);
const auto storage =
IsTessellationInput(kind) ? spv::StorageClassInput : spv::StorageClassOutput;
const auto pointer = state.builder.AllocateId();
if (kind == Attribute::Factor) {
EXIT_NOT_IMPLEMENTED(!inst.Arg(1).IsImmediate());
@@ -26,15 +38,13 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
return pointer;
}
auto address = ctx.Arg(inst, 1);
if (kind == Attribute::PatchOutput) {
address =
EmitBinaryU32(state, spv::OpISub, address, ConstantU32(state, state.tess_patch_base));
if (patch) {
address = EmitBinaryU32(state, spv::OpISub, address, ConstantU32(state, tess.patch_base));
}
const bool local = kind == Attribute::LocalOutput || kind == Attribute::ControlInput;
const auto stride = local ? tess.ls_stride : tess.hs_stride;
const auto offset = kind == Attribute::PatchOutput ? address
: EmitBinaryU32(state, spv::OpUMod, address,
ConstantU32(state, stride));
const auto offset =
patch ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride));
const auto attribute =
EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 4));
const auto component =
@@ -42,7 +52,7 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 2)),
ConstantU32(state, 3));
const auto type = TypePointer(state, storage, TypeU32(state));
if (kind == Attribute::LocalOutput || kind == Attribute::PatchOutput) {
if (kind == Attribute::LocalOutput || patch) {
state.builder.AddFunction(spv::OpAccessChain, type, pointer, variable, attribute,
component);
} else {
@@ -60,20 +70,12 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
void DefineTessellationInterfaces(EmitterState& state) {
using Attribute = IR::TessellationAttribute;
std::array<bool, 6> used {};
uint32_t patch_begin = UINT32_MAX, patch_end = 0;
std::array<bool, IR::NumTessellationAttributes> used {};
for (const auto* block: state.program.blocks) {
for (const auto& inst: *block) {
if (inst.GetOpcode() != IR::ValueOpcode::GetTessellationAttribute &&
inst.GetOpcode() != IR::ValueOpcode::SetTessellationAttribute)
continue;
const auto kind = inst.Arg(0).U32();
used.at(kind) = true;
if (kind == static_cast<uint32_t>(Attribute::PatchOutput)) {
EXIT_NOT_IMPLEMENTED(!inst.Arg(1).IsImmediate());
patch_begin = std::min(patch_begin, inst.Arg(1).U32());
patch_end = std::max(patch_end, inst.Arg(1).U32() + 4u);
}
if (inst.GetOpcode() == IR::ValueOpcode::GetTessellationAttribute ||
inst.GetOpcode() == IR::ValueOpcode::SetTessellationAttribute)
used.at(inst.Arg(0).U32()) = true;
}
}
if (std::ranges::none_of(used, [](bool value) { return value; })) return;
@@ -83,15 +85,16 @@ void DefineTessellationInterfaces(EmitterState& state) {
};
for (uint32_t index = 0; index < used.size(); index++) {
if (!used[index]) continue;
const auto kind = static_cast<Attribute>(index);
const bool input = kind == Attribute::ControlInput || kind == Attribute::EvaluationInput;
const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput;
uint32_t type;
const auto kind = static_cast<Attribute>(index);
const bool patch = IsTessellationPatch(kind);
const auto storage =
IsTessellationInput(kind) ? spv::StorageClassInput : spv::StorageClassOutput;
uint32_t type;
if (kind == Attribute::Factor) {
type = array(TypeF32(state), 4u);
} else if (kind == Attribute::PatchOutput) {
state.tess_patch_base = patch_begin;
type = array(TypeU32Vector(state, 4), (patch_end - patch_begin + 15u) / 16u);
} else if (patch) {
// HS and TES share the reflected patch layout so their interfaces match.
type = array(TypeU32Vector(state, 4), (tess.patch_size + 15u) / 16u);
} else {
const bool local = kind == Attribute::LocalOutput || kind == Attribute::ControlInput;
const auto stride = local ? tess.ls_stride : tess.hs_stride;
@@ -112,11 +115,10 @@ void DefineTessellationInterfaces(EmitterState& state) {
state.builder.AddAnnotation(spv::OpDecorate, state.tess_inner_variable,
spv::DecorationPatch);
} else {
state.builder.AddAnnotation(
spv::OpDecorate, variable, spv::DecorationLocation,
kind == Attribute::PatchOutput ? (tess.hs_stride + 15u) / 16u : 0u);
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation,
patch ? (tess.hs_stride + 15u) / 16u : 0u);
}
if (kind == Attribute::Factor || kind == Attribute::PatchOutput) {
if (kind == Attribute::Factor || patch) {
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationPatch);
}
}
@@ -470,8 +470,10 @@ bool ResolveSetpcDwordJumpTable(const Decoder::Program& program, uint32_t setpc_
if (!ResolveJumpTableEntryCount(program, load_index, load.src1, 2u, &entry_count)) {
return false;
}
// Code-table loads are not translated, so a selector that is also the load destination
// keeps its scaled index at the branch.
uint32_t selector_code = UINT32_MAX;
if (!ScalarOperandCode(load.src1, selector_code) || selector_code == offset_code ||
if (!ScalarOperandCode(load.src1, selector_code) ||
ScalarCodeWrittenInRange(program, load_index + 1u, setpc_index, selector_code)) {
return false;
}
@@ -195,6 +195,9 @@ constexpr MimgGatherInfo MIMG_GATHER_OPCODE_LIST[] = {
{0x5fu, Opcode::IMAGE_GATHER4_C_LZ_O,
ImageSampleFlagCompare | ImageSampleFlagLevelZero | ImageSampleFlagOffset},
{0x61u, Opcode::IMAGE_GATHER4H, ImageSampleFlagGatherHorizontal},
{0xc8u, Opcode::IMAGE_GATHER4_C, ImageSampleFlagCompare | ImageSampleFlagAdjust},
{0xd8u, Opcode::IMAGE_GATHER4_C_O,
ImageSampleFlagCompare | ImageSampleFlagOffset | ImageSampleFlagAdjust},
};
constexpr MimgAtomicInfo MIMG_ATOMIC_OPCODE_LIST[] = {
@@ -166,6 +166,8 @@ void ApplyMemoryInfo(Instruction& inst, const MemoryOpcodeInfo* info) {
inst.formatted = info->formatted;
}
} // namespace
bool IsDsWriteOpcode(Opcode opcode) {
switch (opcode) {
case Opcode::DS_WRITE_B8:
@@ -184,6 +186,8 @@ bool IsDsWriteOpcode(Opcode opcode) {
}
}
namespace {
bool IsDsAtomicOpcode(Opcode opcode) {
switch (opcode) {
case Opcode::DS_ADD_U32:
@@ -882,6 +882,7 @@ Program DecodeFrontProgram(std::span<const uint32_t> front);
void DecodeProgram(std::span<const uint32_t> code, Program& program);
bool IsConditionalBranch(Opcode opcode);
bool IsDirectBranch(Opcode opcode);
bool IsDsWriteOpcode(Opcode opcode);
void DecodeScalarSource(uint32_t code, uint32_t pc, Operand& operand);
void DecodeScalarDestination(uint32_t code, uint32_t pc, Operand& operand);
@@ -987,30 +987,6 @@ void ValidateTranslateOptions(const TranslateOptions& options) {
options.stage != ShaderType::Local) {
EXIT("embedded fetch requires a vertex or local shader");
}
switch (options.stage) {
case ShaderType::Vertex:
case ShaderType::Local:
case ShaderType::TessellationControl:
case ShaderType::TessellationEvaluation:
case ShaderType::Mesh:
if (options.input_info.vertex == nullptr) {
EXIT("vertex shader translation has no vertex input metadata");
}
return;
case ShaderType::Pixel:
if (options.input_info.pixel == nullptr) {
EXIT("pixel shader translation has no pixel input metadata");
}
return;
case ShaderType::Compute:
if (options.input_info.compute == nullptr) {
EXIT("compute shader translation has no compute input metadata");
}
return;
default:
EXIT("shader translation has unsupported stage %u",
static_cast<uint32_t>(options.stage));
}
}
} // namespace
@@ -1044,7 +1020,9 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
case ShaderType::Compute:
result.scratch_dwords = options.input_info.compute->scratch_size_dwords;
break;
default: break; // ValidateTranslateOptions rejects unsupported stages.
default:
EXIT("shader translation has unsupported stage %u",
static_cast<uint32_t>(options.stage));
}
result.dispatcher_fallback = cfg.irreducible || cfg.unsupported;
result.cfg_failure_kind = cfg.failure_kind;
+11 -1
View File
@@ -220,8 +220,11 @@ enum class TessellationAttribute {
ControlOutput,
EvaluationInput,
PatchOutput,
PatchInput,
Factor
};
constexpr uint32_t NumTessellationAttributes =
static_cast<uint32_t>(TessellationAttribute::Factor) + 1u;
enum class StageInputKind {
VertexIndex,
@@ -356,7 +359,14 @@ struct PushData {
static constexpr uint32_t NoStart = UINT32_MAX;
std::array<uint32_t, DwordCount> dwords {};
[[nodiscard]] static constexpr uint32_t GraphicsDrawDwords(bool mesh, bool object_id) {
// User clip planes 0..N follow the mesh draw parameters or object ID, four dwords each.
static constexpr uint32_t UserClipPlaneDword = MeshDrawDwordCount;
[[nodiscard]] static constexpr uint32_t GraphicsDrawDwords(bool mesh, bool object_id,
uint32_t user_clip_planes = 0) {
if (user_clip_planes != 0) {
return UserClipPlaneDword + 4u * std::bit_width(user_clip_planes);
}
return mesh ? MeshDrawDwordCount : static_cast<uint32_t>(object_id);
}
@@ -362,8 +362,7 @@ class Tracker {
public:
Tracker(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg,
ShaderStageInputInfo input)
: m_program(program), m_decoded(decoded), m_native_cfg(native_cfg),
m_workgroup(program.stage == ShaderType::Compute ? input.compute : nullptr),
: m_program(program), m_decoded(decoded), m_native_cfg(native_cfg), m_input(input),
m_scalar_writes(std::move(program.scalar_writes)), m_info(program.info) {
std::ranges::sort(m_scalar_writes, {}, &Program::ScalarWrite::pc);
m_info.buffers.clear();
@@ -1560,12 +1559,17 @@ private:
const Inst* IndexedMaterialRead(const Inst* read, Value active,
DescriptorSource::IndirectDescriptor::SelectorRead& selector) const {
if (read == nullptr) return nullptr;
uint32_t lane = 0;
if (read != nullptr && read->GetOpcode() == ValueOpcode::CompositeExtractU32x2) {
if (!ImmediateU32(read->Arg(1), lane) || lane >= 2u) return nullptr;
const auto op = read->GetOpcode();
if (op == ValueOpcode::CompositeExtractU32x2 || op == ValueOpcode::CompositeExtractU32x3) {
const bool x3 = op == ValueOpcode::CompositeExtractU32x3;
if (!ImmediateU32(read->Arg(1), lane) || lane >= (x3 ? 3u : 2u)) return nullptr;
read = read->Arg(0).Resolve().TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32x2) return nullptr;
} else if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32) {
if (read == nullptr ||
read->GetOpcode() != (x3 ? ValueOpcode::LoadBufferU32x3 : ValueOpcode::LoadBufferU32x2))
return nullptr;
} else if (op != ValueOpcode::LoadBufferU32) {
return nullptr;
}
const auto& memory = m_program.memory_info[read->Flags<MemoryFlags>().index];
@@ -1587,8 +1591,9 @@ private:
const auto* read = SelectorReadValue(selected->Arg(1), active, selector);
if (indexed) {
read = IndexedMaterialRead(read, active, selector);
// This optional domain is checked against the dispatch's writable buffers.
// Other stages and unbounded/image writes need the original heap domain.
// Bounded heaps fall back when the material aliases a written buffer; pointer
// tables rely on the renderer's specialization-read alias check. Other stages
// and unbounded/image writes keep the original domain.
if (read == nullptr || m_program.stage != ShaderType::Compute ||
std::ranges::any_of(m_program.blocks, [](const Block* block) {
return std::ranges::any_of(*block, [](const Inst& inst) {
@@ -1853,7 +1858,7 @@ private:
if (inst->GetOpcode() == ValueOpcode::GetBuiltin &&
inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)) &&
ImmediateU32(inst->Arg(1), axis) && axis < 3u &&
m_workgroup->threads_num[axis] <= (uint64_t {1} << bits)) return axis + 1u;
m_input.compute->threads_num[axis] <= (uint64_t {1} << bits)) return axis + 1u;
if (!allow_induction || inst->GetOpcode() != ValueOpcode::Phi ||
(!induction.IsEmpty() && induction != at)) return {};
induction = at;
@@ -1864,20 +1869,20 @@ private:
bool LinearLocalIndex(const std::array<uint64_t, 5>& index) const {
uint64_t stride = 1u;
for (uint32_t axis = 0; axis < 3u; ++axis) {
if (m_workgroup->threads_num[axis] != 1u && index[axis + 1u] != stride) return false;
stride *= m_workgroup->threads_num[axis];
if (m_input.compute->threads_num[axis] != 1u && index[axis + 1u] != stride) return false;
stride *= m_input.compute->threads_num[axis];
}
return index[0] == 0u;
}
void ForwardPrivateLds() {
if (m_workgroup == nullptr) return;
if (m_program.stage != ShaderType::Compute) return;
uint64_t bytes = 4u;
for (const auto size: m_workgroup->threads_num) {
for (const auto size: m_input.compute->threads_num) {
if (size == 0u || size > 1024u) return;
bytes *= size;
}
if (bytes > uint64_t {m_workgroup->lds_size_dwords} * 4u) return;
if (bytes > uint64_t {m_input.compute->lds_size_dwords} * 4u) return;
struct Access { Inst* inst; uint32_t offset; };
std::vector<Access> reads, writes;
Value address;
@@ -1893,7 +1898,7 @@ private:
if ((op != ValueOpcode::LoadSharedU32 && op != ValueOpcode::WriteSharedU32) ||
memory.data_bits != 32u || memory.secondary_offset != 0u ||
(memory.offset & 3u) != 0u ||
uint64_t {memory.offset} + bytes > uint64_t {m_workgroup->lds_size_dwords} * 4u)
uint64_t {memory.offset} + bytes > uint64_t {m_input.compute->lds_size_dwords} * 4u)
return;
if (address.IsEmpty()) address = inst.Arg(0).Resolve();
else if (!EquivalentValue(m_program, address, inst.Arg(0))) return;
@@ -2039,16 +2044,16 @@ private:
Value SharedLoadBound(Value value, Value active) {
const auto* read = ActiveValue(value, active).TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadSharedU32 ||
m_workgroup == nullptr || m_program.stage != ShaderType::Compute ||
m_program.stage != ShaderType::Compute ||
read->Arg(1).Resolve() != Value(true)) return {};
uint64_t group_size = 1u;
for (const auto dimension: m_workgroup->threads_num) {
if (dimension == 0u || dimension > m_workgroup->wave_size) return {};
for (const auto dimension: m_input.compute->threads_num) {
if (dimension == 0u || dimension > m_input.compute->wave_size) return {};
group_size *= dimension;
}
// Indexed LDS operations within one native wave execute in order. Multiwave
// append lists require a separate workgroup synchronization proof.
if (group_size != m_workgroup->wave_size || group_size != m_program.wave_size ||
if (group_size != m_input.compute->wave_size || group_size != m_program.wave_size ||
(group_size != 32u && group_size != 64u)) return {};
const auto memory = [&](const Inst& inst) -> const MemoryInfo* {
const auto index = inst.Flags<MemoryFlags>().index;
@@ -2101,7 +2106,7 @@ private:
if (op != ValueOpcode::SharedAtomicIAdd32 ||
ActiveValue(inst.Arg(1), guard) != Value(1u) ||
!constant_address(inst, address) ||
uint64_t {address} + 4u > uint64_t {m_workgroup->lds_size_dwords} * 4u ||
uint64_t {address} + 4u > uint64_t {m_input.compute->lds_size_dwords} * 4u ||
address >= 0x10000u ||
std::ranges::any_of(appends, [&](const Append& at) { return at.counter == address; })) return {};
appends.push_back({.atomic = &inst, .counter = address});
@@ -3063,6 +3068,7 @@ private:
}
if (indirect.key_count.IsEmpty() &&
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, false) &&
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, true) &&
!MatchReversedImageRange(key, indirect)) {
const auto maximum = SelectorMaximum(key);
if (maximum >= MaxIndirectDescriptorProbes) return false;
@@ -4059,7 +4065,7 @@ private:
Program& m_program;
const Decoder::Program& m_decoded;
const CFG::Graph& m_native_cfg;
const ShaderWorkgroupInputInfo* m_workgroup;
ShaderStageInputInfo m_input;
std::vector<Program::ScalarWrite> m_scalar_writes;
std::vector<ResolvedHandle> m_resolved_handles;
std::vector<const Inst*> m_srt_visiting;
@@ -4092,8 +4098,8 @@ UniformFillPlan RecognizePatternFill(Program& program, const Decoder::Program& d
// 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 ||
if (program.stage != ShaderType::Compute || decoded.instructions.empty() ||
program.user_data_base != 0 || program.user_data_count < 10 ||
program.info.buffers.size() != 1 || !program.info.images.empty()) return {};
const auto& cs = *input.compute;
const bool matching_inputs = cs.workgroup_register == 10 && cs.thread_ids_num == 1 &&
@@ -7,7 +7,7 @@ namespace Libs::Graphics::ShaderRecompiler::IR {
// Resolves native descriptor sources, plans their scalar reads, and assigns dense resource bindings.
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg,
ShaderStageInputInfo input = {});
ShaderStageInputInfo input);
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -47,26 +47,17 @@ void ValidateOptions(const Program& program, ShaderStageInputInfo input_info) {
case ShaderType::TessellationControl:
case ShaderType::TessellationEvaluation:
case ShaderType::Mesh:
if (input_info.vertex == nullptr) {
return Fail("vertex shader has no input metadata");
}
if (input_info.vertex->resources_num < 0 ||
input_info.vertex->resources_num > ShaderVertexInputInfo::RES_MAX) {
return Fail("vertex resource count is out of range");
}
return;
case ShaderType::Pixel:
if (input_info.pixel == nullptr) {
return Fail("pixel shader has no input metadata");
}
if (input_info.pixel->input_num > std::size(input_info.pixel->interpolator_settings)) {
return Fail("pixel input count is out of range");
}
return;
case ShaderType::Compute:
if (input_info.compute == nullptr) {
return Fail("compute shader has no input metadata");
}
if (input_info.compute->thread_ids_num < 0 || input_info.compute->thread_ids_num > 3) {
return Fail("compute thread ID count is out of range");
}
@@ -191,7 +182,7 @@ void AddBuiltinInput(ShaderInfo& info, StageInputKind kind) {
void CollectOutput(const Program& program, ShaderStageInputInfo input_info,
ShaderInfo& info, const Inst& inst) {
const bool alpha_remap = program.stage == ShaderType::Pixel && input_info.pixel != nullptr &&
const bool alpha_remap = program.stage == ShaderType::Pixel &&
input_info.pixel->alpha_blend_source != ShaderAlphaBlendSource::None;
const auto& export_info = program.export_info[inst.Flags<ExportFlags>().index];
if (export_info.kind == ExportTargetKind::MrtZ) {
+24 -4
View File
@@ -47,6 +47,8 @@ struct ShaderMapEntry {
static std::unique_ptr<std::unordered_map<uint64_t, ShaderMapEntry>> g_shader_map;
static std::mutex g_shader_map_mutex;
static std::unordered_map<uint64_t, ShaderTessellationInputInfo> g_tess_layouts;
static std::mutex g_tess_layouts_mutex;
void ShaderInit() {
EXIT_IF(g_shader_map != nullptr);
@@ -315,6 +317,7 @@ static void ShaderGetStaticInputInfoPS(
ps_info = {};
ps_info.scratch_size_dwords = data.scratch_size_dwords;
ps_info.lds_size_dwords = static_cast<uint32_t>(regs.ps_regs.rsrc2.extra_lds_size) * 128u;
// SPI_PS_IN_CONTROL: NUM_INTERP occupies bits 5:0 and PS_W32_EN is bit 15.
ps_info.input_num = sh.ps_in_control & 0x3fu;
@@ -411,7 +414,9 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
key.push_back(info.resources_num);
key.push_back(info.wave_size);
key.push_back(info.scratch_size_dwords);
key.push_back(info.lds_size_dwords);
key.push_back(info.pa_cl_vs_out_cntl);
key.push_back(info.user_clip_planes);
key.push_back(static_cast<uint32_t>(info.clip_space.enabled));
if (info.clip_space.enabled) {
for (const float value: info.clip_space.scale) {
@@ -438,7 +443,8 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
if (info.tess.input_control_points != 0) {
const auto& tess = info.tess;
key.insert(key.end(), {tess.output_control_points, tess.ls_stride, tess.hs_stride,
tess.domain, tess.partitioning, tess.output_topology});
tess.patch_base, tess.patch_size, tess.domain, tess.partitioning,
tess.output_topology});
}
for (int i = 0; i < info.resources_num; i++) {
@@ -454,6 +460,7 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
EXIT_IF(info.input_num > std::size(info.interpolator_settings));
key.clear();
key.push_back(info.scratch_size_dwords);
key.push_back(info.lds_size_dwords);
key.push_back(info.input_num);
key.push_back(info.wave_size);
key.push_back(info.ps_system_input_base);
@@ -528,6 +535,7 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
EXIT("failed to prepare vertex shader program\n");
}
info.wave_size = (context.GetShaderStages() & 0x00400000u) != 0 ? 32u : 64u;
info.lds_size_dwords = static_cast<uint32_t>(regs.gs_regs.rsrc2.lds_size) * 128u;
return params;
}
// NGG user SGPRs start at s8; a separately compiled GS back half also receives
@@ -646,7 +654,18 @@ PrepareTessellationPrograms(const HW::VertexShaderInfo& regs, const HW::Context&
};
EXIT_IF(tess.input_control_points == 0 || tess.input_control_points > 32 ||
tess.output_control_points == 0 || tess.output_control_points > 32);
ShaderRecompiler::AnalyzeTessellationPrograms(params[0].code, params[1].code, tess);
// Reflection decodes both programs; the key also covers the VGT fields cached in tess.
const uint64_t key[] = {local_hash, control_hash,
uint64_t {sh.m_vgtLsHsConfig & 0xfff00u} << 32u | sh.m_vgtTfParam};
{
std::scoped_lock lock(g_tess_layouts_mutex);
auto [entry, inserted] = g_tess_layouts.try_emplace(XXH3_64bits(key, sizeof(key)), tess);
if (inserted) {
ShaderRecompiler::AnalyzeTessellationPrograms(params[0].code, params[1].code,
entry->second);
}
tess = entry->second;
}
for (auto& stage: input_info) {
stage.tess = tess;
}
@@ -746,7 +765,8 @@ void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info) {
"\t ps_no_perspective = %s\n"
"\t ps_pixel_kill_enable = %s\n"
"\t ps_early_z = %s\n"
"\t ps_execute_on_noop = %s\n",
"\t ps_execute_on_noop = %s\n"
"\t lds_size_dwords = %u\n",
info.input_num, info.ps_system_input_base, info.custom_interpolation_mask,
info.ps_perspective_center_vgpr, info.ps_perspective_sample_vgpr,
info.ps_perspective_centroid_vgpr, info.ps_pos_x ? "true" : "false",
@@ -755,7 +775,7 @@ void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info) {
info.ps_front_face_all_bits ? "true" : "false", info.ps_ancillary ? "true" : "false",
info.ps_sample_shading ? "true" : "false", info.ps_no_perspective ? "true" : "false",
info.ps_pixel_kill_enable ? "true" : "false", info.ps_early_z ? "true" : "false",
info.ps_execute_on_noop ? "true" : "false");
info.ps_execute_on_noop ? "true" : "false", info.lds_size_dwords);
for (uint32_t i = 0; i < info.input_num; i++) {
LOGF("\t interpolator_settings[%u] = %u\n", i, info.interpolator_settings[i]);
+8
View File
@@ -130,6 +130,8 @@ struct ShaderTessellationInputInfo {
uint32_t output_control_points = 0;
uint32_t ls_stride = 0;
uint32_t hs_stride = 0;
uint32_t patch_base = 0;
uint32_t patch_size = 0;
uint32_t domain = 0;
uint32_t partitioning = 0;
uint32_t output_topology = 0;
@@ -149,7 +151,11 @@ struct ShaderVertexInputInfo {
int buffers_num = 0;
uint32_t wave_size = 64;
uint32_t scratch_size_dwords = 0;
// LDS_SIZE of the GS stage a non-NGG vertex shader runs on; usually zero.
uint32_t lds_size_dwords = 0;
uint32_t pa_cl_vs_out_cntl = 0;
// PA_CL_CLIP_CNTL.UCP_ENA; set only on the last pre-rasterization stage.
uint32_t user_clip_planes = 0;
ShaderClipSpaceTransform clip_space;
ShaderMeshInputInfo mesh;
ShaderTessellationInputInfo tess;
@@ -191,6 +197,8 @@ struct ShaderPixelInputInfo {
// CB round mode per converted export: zero adds half an LSB, one truncates.
uint8_t target_round_mode = 0;
uint32_t scratch_size_dwords = 0;
// EXTRA_LDS_SIZE: LDS reserved beyond the interpolants (VGPR spills to LDS).
uint32_t lds_size_dwords = 0;
bool ps_pos_x = false;
bool ps_pos_y = false;
bool ps_pos_z = false;
+51 -47
View File
@@ -28,9 +28,9 @@ static bool CoalescePlaceholders(uint64_t vaddr, uint64_t size) {
}
#endif
template <typename Regions, typename Source, typename Gap>
static void CopySparseRange(const Regions& regions, uint64_t vaddr, void* data, uint64_t size,
Source&& source, Gap&& gap) {
template <typename Regions, typename Region, typename Gap>
static void ForEachSparseRange(const Regions& regions, uint64_t vaddr, uint64_t size,
Region&& region, Gap&& gap) {
const auto end = vaddr + size;
auto mapping = regions.lower_bound(vaddr);
if (mapping != regions.begin()) {
@@ -40,23 +40,34 @@ static void CopySparseRange(const Regions& regions, uint64_t vaddr, void* data,
}
}
auto* destination = static_cast<uint8_t*>(data);
auto cursor = vaddr;
auto cursor = vaddr;
for (; mapping != regions.end() && mapping->second.vaddr < end; ++mapping) {
const auto& entry = mapping->second;
const auto start = std::max(vaddr, entry.vaddr);
const auto bytes = std::min(end, entry.vaddr + entry.size) - start;
if (cursor < start) {
gap(cursor, destination + (cursor - vaddr), start - cursor);
gap(cursor, start - cursor);
}
std::memcpy(destination + (start - vaddr), source(entry, start), bytes);
region(entry, start, bytes);
cursor = start + bytes;
}
if (cursor < end) {
gap(cursor, destination + (cursor - vaddr), end - cursor);
gap(cursor, end - cursor);
}
}
template <typename Regions, typename Source, typename Gap>
static void CopySparseRange(const Regions& regions, uint64_t vaddr, void* data, uint64_t size,
Source&& source, Gap&& gap) {
auto* destination = static_cast<uint8_t*>(data);
ForEachSparseRange(
regions, vaddr, size,
[&](const auto& entry, uint64_t start, uint64_t bytes) {
std::memcpy(destination + (start - vaddr), source(entry, start), bytes);
},
[&](uint64_t start, uint64_t bytes) { gap(start, destination + (start - vaddr), bytes); });
}
class GuestBackingStore {
public:
enum class FailureReason {
@@ -200,36 +211,16 @@ public:
return false;
}
// Skip unbacked gaps, mirroring sparse reads that zero-fill them.
std::lock_guard lock(m_mutex);
const auto end = vaddr + size;
auto first = m_maps.upper_bound(vaddr);
if (first == m_maps.begin()) {
return false;
}
--first;
auto current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
if (mapping == m_maps.end() || current < mapping->second.vaddr ||
current >= mapping->second.vaddr + mapping->second.size) {
return false;
}
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
if (!RangeOk(entry.backing_offset + current - entry.vaddr, bytes)) {
return false;
}
current += bytes;
}
const auto* source = static_cast<const uint8_t*>(data);
current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
const auto offset = entry.backing_offset + current - entry.vaddr;
std::memcpy(m_backing_base + offset, source + (current - vaddr), bytes);
current += bytes;
}
const auto* source = static_cast<const uint8_t*>(data);
ForEachSparseRange(
m_maps, vaddr, size,
[&](const auto& entry, uint64_t start, uint64_t bytes) {
std::memcpy(m_backing_base + (entry.backing_offset + start - entry.vaddr),
source + (start - vaddr), bytes);
},
[](uint64_t, uint64_t) {});
return true;
}
@@ -775,8 +766,14 @@ public:
});
}
// Writes the backed bytes of a range that holds no private mappings.
bool TryWriteBacking(uint64_t vaddr, const void* data, uint64_t size) {
return m_backing->TryWriteBacking(vaddr, data, size);
if (!Owns(vaddr, size)) {
return false;
}
std::shared_lock lock(m_mutex);
return !OverlapsPrivateUnlocked(vaddr, size) &&
m_backing->TryWriteBacking(vaddr, data, size);
}
bool TryReadBacking(uint64_t vaddr, void* data, uint64_t size) {
@@ -790,15 +787,8 @@ public:
}
std::shared_lock lock(m_mutex);
if constexpr (!copy_private) {
auto mapping = m_mapped.upper_bound(vaddr);
if (mapping != m_mapped.begin()) {
--mapping;
}
for (; mapping != m_mapped.end() && mapping->second.vaddr < vaddr + size; ++mapping) {
const auto& region = mapping->second;
if (region.type == MappingType::Private && region.vaddr + region.size > vaddr) {
return false;
}
if (OverlapsPrivateUnlocked(vaddr, size)) {
return false;
}
}
return m_backing->TryReadSparseBacking(
@@ -1111,6 +1101,20 @@ private:
return true;
}
[[nodiscard]] bool OverlapsPrivateUnlocked(uint64_t vaddr, uint64_t size) const {
auto mapping = m_mapped.upper_bound(vaddr);
if (mapping != m_mapped.begin()) {
--mapping;
}
for (; mapping != m_mapped.end() && mapping->second.vaddr < vaddr + size; ++mapping) {
const auto& region = mapping->second;
if (region.type == MappingType::Private && region.vaddr + region.size > vaddr) {
return true;
}
}
return false;
}
bool AddMappedUnlocked(uint64_t vaddr, uint64_t size, MappingType type) {
if (size == 0 || UINT64_MAX - vaddr < size) {
return false;
+12 -4
View File
@@ -3389,6 +3389,11 @@ int KYTY_SYSV_ABI PthreadCancel(Pthread thread) {
return KERNEL_ERROR_EINVAL;
}
// A joined thread's native handle is gone; the guest may still hold its handle.
if (thread->free) {
return KERNEL_ERROR_ESRCH;
}
int result = pthread_cancel(thread->p);
LOGF("\tthread cancel: %s, %d\n", thread->name.c_str(), result);
@@ -3492,10 +3497,8 @@ int KYTY_SYSV_ABI PthreadGetprio(Pthread thread, int* prio) {
EXIT_NOT_IMPLEMENTED(prio == nullptr);
sched_param native_param {};
int native_policy = 0;
if (pthread_getschedparam(thread->p, &native_policy, &native_param) != 0) {
return KERNEL_ERROR_EINVAL;
if (thread->free) {
return KERNEL_ERROR_ESRCH;
}
*prio = thread->attr->guest_priority;
@@ -3510,6 +3513,11 @@ int KYTY_SYSV_ABI PthreadSetprio(Pthread thread, int prio) {
return KERNEL_ERROR_ESRCH;
}
// A joined thread's native handle is gone; the guest may still hold its handle.
if (thread->free) {
return KERNEL_ERROR_ESRCH;
}
sched_param param {};
int pol = 0;
+1 -1
View File
@@ -1,5 +1,5 @@
find_package(Qt6 COMPONENTS Concurrent Network Widgets REQUIRED)
find_package(Qt6 6.8 COMPONENTS Concurrent Network Widgets REQUIRED)
add_subdirectory("${KYTY_SOURCE_DIR}/updater" "${CMAKE_CURRENT_BINARY_DIR}/updater")
set(CMAKE_AUTOUIC_SEARCH_PATHS forms)
+43
View File
@@ -112,6 +112,49 @@
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="themeLayout">
<item>
<widget class="QLabel" name="label_theme">
<property name="text">
<string>Theme:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="theme_combo">
<item>
<property name="text">
<string>System</string>
</property>
</item>
<item>
<property name="text">
<string>Dark</string>
</property>
</item>
<item>
<property name="text">
<string>Light</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="themeSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
+28 -1
View File
@@ -12,6 +12,7 @@
#include <QApplication>
#include <QByteArray>
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QDir>
#include <QFile>
@@ -26,6 +27,7 @@
#include <QRegularExpression>
#include <QSettings>
#include <QStringList>
#include <QStyleHints>
#include <QTextStream>
#include <QVariant>
#include <QtCore>
@@ -61,7 +63,8 @@ constexpr DWORD CMD_Y_CHARS = 1000;
#endif
constexpr char SETTINGS_MAIN_DIALOG[] = "MainDialog";
constexpr char SETTINGS_MAIN_LAST_GEOMETRY[] = "geometry";
constexpr char SETTINGS_CHECK_UPDATES[] = "check_updates_on_startup";
constexpr char SETTINGS_CHECK_UPDATES[] = "check_updates_on_startup";
constexpr char SETTINGS_THEME[] = "theme";
class MainDialogPrivate: public QObject {
Q_OBJECT
@@ -86,6 +89,17 @@ public:
private:
static QByteArray g_last_geometry;
static bool g_check_updates_on_startup;
static int g_theme;
static void ApplyTheme(int theme) {
auto scheme = Qt::ColorScheme::Unknown;
switch (theme) {
case 1: scheme = Qt::ColorScheme::Dark; break;
case 2: scheme = Qt::ColorScheme::Light; break;
default: break;
}
QApplication::styleHints()->setColorScheme(scheme);
}
Ui::MainDialog* m_ui = {nullptr};
MainDialog* m_main_dialog = nullptr;
@@ -101,6 +115,7 @@ private:
QByteArray MainDialogPrivate::g_last_geometry;
bool MainDialogPrivate::g_check_updates_on_startup = true;
int MainDialogPrivate::g_theme = 0;
MainDialog::MainDialog(QWidget* parent): QDialog(parent), m_p(new MainDialogPrivate(this)) {
m_p->Setup(this);
@@ -119,6 +134,8 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
m_main_dialog = main_dialog;
m_update_checker = new UpdateChecker(main_dialog);
m_ui->check_updates_on_startup->setChecked(g_check_updates_on_startup);
m_ui->theme_combo->setCurrentIndex(g_theme);
ApplyTheme(g_theme);
m_ui->check_updates_link->setVisible(UpdateChecker::IsSupported());
m_ui->check_updates_on_startup->setVisible(UpdateChecker::IsSupported());
@@ -149,6 +166,11 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
g_check_updates_on_startup = checked;
m_ui->widget->WriteSettings();
});
connect(m_ui->theme_combo, &QComboBox::currentIndexChanged, this, [this](int index) {
g_theme = index;
ApplyTheme(index);
m_ui->widget->WriteSettings();
});
connect(main_dialog, &MainDialog::Resize, [this]() {
g_last_geometry = m_main_dialog->saveGeometry();
m_ui->widget->WriteSettings();
@@ -516,6 +538,7 @@ void MainDialogPrivate::WriteSettings(QSettings& s) {
s.setValue(SETTINGS_MAIN_LAST_GEOMETRY, g_last_geometry);
}
s.setValue(SETTINGS_CHECK_UPDATES, g_check_updates_on_startup);
s.setValue(SETTINGS_THEME, g_theme);
s.endGroup();
}
@@ -525,6 +548,10 @@ void MainDialogPrivate::ReadSettings(QSettings& s) {
g_last_geometry = s.value(SETTINGS_MAIN_LAST_GEOMETRY, g_last_geometry).toByteArray();
g_check_updates_on_startup = s.value(SETTINGS_CHECK_UPDATES, true).toBool();
g_theme = s.value(SETTINGS_THEME, 0).toInt();
if (g_theme < 0 || g_theme > 2) {
g_theme = 0;
}
s.endGroup();
}
+24 -9
View File
@@ -25,12 +25,8 @@ namespace Libs::Controller {
LIB_NAME("Pad", "Pad");
constexpr int PAD_ERROR_INVALID_ARG = -2137915391; /* 0x80920001 */
constexpr int PAD_ERROR_INVALID_HANDLE = -2137915389; /* 0x80920003 */
constexpr int PAD_ERROR_ALREADY_OPENED = -2137915388; /* 0x80920004 */
constexpr int PAD_ERROR_NOT_INITIALIZED = -2137915387; /* 0x80920005 */
constexpr int PAD_ERROR_NO_HANDLE = -2137915384; /* 0x80920008 */
constexpr int PAD_HANDLE = 1;
constexpr int PAD_HANDLE = 1;
static_assert(PAD_HANDLE + Config::MAX_PLAYERS <= PAD_HANDLE_VR_LEFT);
constexpr uint32_t RUMBLE_DURATION_MS = 0xffff;
constexpr uint32_t RELEASE_FLUSH_MS = 50;
@@ -215,9 +211,11 @@ static GameController* PlayerForDevice(int id) {
}
static GameController* PlayerForHandle(int handle) {
return handle >= PAD_HANDLE && handle < PAD_HANDLE + Config::MAX_PLAYERS
? g_players[handle - PAD_HANDLE].get()
: nullptr;
if (handle >= PAD_HANDLE && handle < PAD_HANDLE + Config::MAX_PLAYERS) {
return g_players[handle - PAD_HANDLE].get();
}
// The Sense controller ports belong to the initial user.
return PadVrHand(handle) >= 0 ? g_players[0].get() : nullptr;
}
static void pad_fill_data(PadData* data, const ControllerState& state, bool connected,
@@ -1204,10 +1202,18 @@ int PadCheckHandle(int handle) {
if (g_players[0] == nullptr) {
return PAD_ERROR_NOT_INITIALIZED;
}
if (PadVrHand(handle) >= 0) {
return PadVrCheckHandle(handle);
}
auto* player = PlayerForHandle(handle);
return player != nullptr ? player->CheckPortHandle(handle) : PAD_ERROR_INVALID_HANDLE;
}
// Handle 0 is never valid, so this only reports whether scePadInit has run.
bool PadInitialized() {
return g_players[0] != nullptr && g_players[0]->CheckPortHandle(0) != PAD_ERROR_NOT_INITIALIZED;
}
int KYTY_SYSV_ABI PadInit() {
PRINT_NAME();
for (const auto& player: g_players) {
@@ -1225,6 +1231,9 @@ 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));
if (const int hand = PadVrPortHand(type); hand >= 0) {
return PadVrPort(user_id, hand, index, true);
}
const int player = Config::GetPlayerFromUserId(user_id);
return player >= 0 ? g_players[player]->OpenPort(user_id, type, index) : PAD_ERROR_INVALID_ARG;
}
@@ -1237,6 +1246,9 @@ int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index) {
"\t index = %d\n",
user_id, type, index);
if (const int hand = PadVrPortHand(type); hand >= 0) {
return PadVrPort(user_id, hand, index, false);
}
const int player = Config::GetPlayerFromUserId(user_id);
return player >= 0 ? g_players[player]->GetPortHandle(user_id, type, index)
: PAD_ERROR_NO_HANDLE;
@@ -1245,6 +1257,9 @@ int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index) {
int KYTY_SYSV_ABI PadClose(int handle) {
PRINT_NAME();
LOGF("\t handle = %d\n", handle);
if (PadVrHand(handle) >= 0) {
return PadVrClose(handle);
}
auto* player = PlayerForHandle(handle);
if (player == nullptr) {
return PAD_ERROR_INVALID_HANDLE;
+32 -1
View File
@@ -18,6 +18,12 @@ struct Lifecycle {
constexpr int HOST_INPUT_CONTROLLER_ID = -1000;
constexpr int PAD_ERROR_INVALID_ARG = -2137915391; /* 0x80920001 */
constexpr int PAD_ERROR_INVALID_HANDLE = -2137915389; /* 0x80920003 */
constexpr int PAD_ERROR_ALREADY_OPENED = -2137915388; /* 0x80920004 */
constexpr int PAD_ERROR_NOT_INITIALIZED = -2137915387; /* 0x80920005 */
constexpr int PAD_ERROR_NO_HANDLE = -2137915384; /* 0x80920008 */
constexpr uint32_t PAD_BUTTON_L3 = 0x00000002;
constexpr uint32_t PAD_BUTTON_R3 = 0x00000004;
constexpr uint32_t PAD_BUTTON_OPTIONS = 0x00000008;
@@ -86,7 +92,32 @@ enum class Setting { SpeakerVolume, VibrationIntensity, TriggerEffectIntensity }
void CycleSetting(Setting setting);
float GetSettingScale(Setting setting);
int PadCheckHandle(int handle);
int PadCheckHandle(int handle);
bool PadInitialized();
// Virtual headset only (padVr.cpp): the Sense controller ports of the initial user. Their handles
// stay outside the per-player range and otherwise act on the first player.
constexpr int PAD_HANDLE_VR_LEFT = 0x10; // The right controller follows it.
// 0 for the left and 1 for the right Sense controller handle, -1 for any other handle.
constexpr int PadVrHand(int handle) {
return handle == PAD_HANDLE_VR_LEFT || handle == PAD_HANDLE_VR_LEFT + 1
? handle - PAD_HANDLE_VR_LEFT
: -1;
}
// Hand of a Sense controller port type, -1 for other types or without the virtual headset.
int PadVrPortHand(int type);
// scePadOpen (open) or scePadGetHandle for a Sense controller port.
int PadVrPort(int user_id, int hand, int index, bool open);
int PadVrCheckHandle(int handle);
int PadVrClose(int handle);
struct PadVrControllerData;
int KYTY_SYSV_ABI PadVrControllerReadState(int handle, PadVrControllerData* data);
int KYTY_SYSV_ABI PadVrControllerSetTriggerEffects(int left_handle, int right_handle,
const PadTriggerEffectParam* param);
int KYTY_SYSV_ABI PadInit();
int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param);
+29
View File
@@ -12,6 +12,17 @@ inline constexpr uint64_t OUTPUT_MODE_89_91HZ = 0x2000c;
inline constexpr uint64_t OUTPUT_MODE_119_88HZ = 0x2000f;
inline constexpr int32_t ERROR_REPROJECTION_NOT_INITIALIZED = -1972240373; // 0x8a72000b
// Virtual ER15 field of view per eye: 55/42.5/51.2/51.2 degrees. "Out" is the temporal side and
// "in" the nasal side. These tangents describe that geometry, not bit-exact firmware output.
inline constexpr float FOV_TAN_OUT = 1.4281480312f;
inline constexpr float FOV_TAN_IN = 0.9163311720f;
inline constexpr float FOV_TAN_TOP = 1.2437491417f;
inline constexpr float FOV_TAN_BOTTOM = 1.2437491417f;
// Each eye sits half the lens separation to the side of the head.
inline constexpr uint8_t LENS_SEPARATION_MM = 63;
inline constexpr float EYE_OFFSET_M = LENS_SEPARATION_MM / 2000.0f;
struct ReprojectionState {
std::atomic<uint64_t> output_mode {0};
std::atomic<uint32_t> timing_us {3000};
@@ -19,6 +30,24 @@ struct ReprojectionState {
[[nodiscard]] ReprojectionState* GetReprojectionState();
struct SceFVector3 {
float x;
float y;
float z;
};
// The headset video-out port registers SceHmd2ReprojectionBuffer objects. The eye texture of their
// first layer is the surface the game flips; both eyes sit side by side in it.
struct ReprojectionSurface {
uint64_t address;
uint32_t width;
uint32_t height;
uint64_t pixel_format; // SceVideoOutPixelFormat
};
[[nodiscard]] bool GetReprojectionSurface(const void* reprojection_buffer,
ReprojectionSurface& out);
} // namespace Libs::Hmd2
#endif // EMULATOR_SRC_LIBS_HMD2_H_
+215 -4
View File
@@ -1,15 +1,20 @@
#include "common/assert.h"
#include "common/emulatorConfig.h"
#include "graphics/guest_gpu/tile.h"
#include "graphics/host_gpu/renderer/image/imageInfo.h"
#include "graphics/shader/shaderBindings.h"
#include "kernel/pthread.h"
#include "libs/agc.h"
#include "libs/errno.h"
#include "libs/hmd2.h"
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <mutex>
#include <new>
@@ -21,10 +26,19 @@ namespace Hmd2 {
constexpr int32_t ERROR_ALREADY_INITIALIZED = -1972240383; // 0x8a720001
constexpr int32_t ERROR_NOT_INITIALIZED = -1972240382; // 0x8a720002
constexpr int32_t ERROR_HANDLE_INVALID = -1972240381; // 0x8a720003
constexpr int32_t ERROR_PARAMETER_NULL = -1972240376; // 0x8a720008
constexpr int32_t ERROR_PARAMETER_INVALID = -1972240375; // 0x8a720009
constexpr int32_t ERROR_REPROJECTION_ALREADY_INITIALIZED = -1972240372; // 0x8a72000c
constexpr int32_t ERROR_REPROJECTION_IN_VR_MODE = -1972240356; // 0x8a72001c
// Any non-negative value is a valid headset handle.
constexpr int32_t HMD_HANDLE = 0x0f000001;
constexpr uint32_t REPROJECTION_LAYER_TYPE_VR3D = 0x10;
constexpr uint32_t REPROJECTION_LAYER_TYPE_VR2D = 0x12;
constexpr uint32_t REPROJECTION_MAX_SOURCE_COUNT = 5;
constexpr int32_t GAZE_TRACKING_STATUS_DISABLE = -1;
struct SceHmd2InitializeParam {
void* reserved0;
uint8_t reserved[8];
@@ -69,6 +83,58 @@ struct SceHmd2ReprojectionInitializeParam {
uint32_t reserved[5];
};
struct SceHmd2ReprojectionSource {
uint32_t layerType;
uint32_t data[73]; // VR3D: srcL texture, srcR texture, ...; VR2D: src texture, ...
const void* option;
};
struct SceHmd2ReprojectionBuffer {
const SceHmd2ReprojectionSource* src;
uint32_t srcCount;
};
struct SceHmd2ReprojectionRenderConfig {
uint32_t syncMode;
uint32_t phase;
uint32_t reserved[2];
};
struct SceHmd2ReprojectionParam {
const void* settings;
int32_t settingCount;
const void* option;
};
struct SceHmd2GazeGetResultParam {
uint32_t sizeOfThis;
int32_t handle;
int64_t flipArg;
};
struct SceHmd2GazeEyeResult {
int32_t isPupilDiameterValid;
float pupilDiameter;
int32_t isBlink;
};
struct SceHmd2GazeResult {
uint32_t sizeOfThis;
int32_t status;
uint64_t timestamp;
int32_t isGazeValid;
SceFVector3 gazeDirection;
SceHmd2GazeEyeResult left;
SceHmd2GazeEyeResult right;
};
struct SceHmd2GazeResultForFoveatedRendering {
uint32_t sizeOfThis;
int32_t status;
SceFVector3 gazeDirection;
uint32_t reserved;
};
static_assert(sizeof(SceHmd2InitializeParam) == 16);
static_assert(offsetof(SceHmd2InitializeParam, reserved) == 8);
static_assert(sizeof(SceHmd2DeviceInformation) == 28);
@@ -84,6 +150,19 @@ static_assert(offsetof(SceHmd2ReprojectionInitializeParam, type) == 32);
static_assert(offsetof(SceHmd2ReprojectionInitializeParam, pSeeThroughBuff) == 40);
static_assert(offsetof(SceHmd2ReprojectionInitializeParam, reprojectionTiming) == 48);
static_assert(offsetof(SceHmd2ReprojectionInitializeParam, reserved) == 52);
static_assert(sizeof(SceHmd2ReprojectionSource) == 304);
static_assert(offsetof(SceHmd2ReprojectionSource, option) == 296);
static_assert(sizeof(SceHmd2ReprojectionBuffer) == 16);
static_assert(sizeof(SceHmd2ReprojectionRenderConfig) == 16);
static_assert(sizeof(SceHmd2ReprojectionParam) == 24);
static_assert(offsetof(SceHmd2ReprojectionParam, option) == 16);
static_assert(sizeof(SceHmd2GazeGetResultParam) == 16);
static_assert(offsetof(SceHmd2GazeGetResultParam, flipArg) == 8);
static_assert(sizeof(SceHmd2GazeResult) == 56);
static_assert(offsetof(SceHmd2GazeResult, gazeDirection) == 20);
static_assert(offsetof(SceHmd2GazeResult, right) == 44);
static_assert(sizeof(SceHmd2GazeResultForFoveatedRendering) == 24);
static_assert(offsetof(SceHmd2GazeResultForFoveatedRendering, gazeDirection) == 8);
static std::atomic<bool> g_initialized = false;
static std::mutex g_reprojection_mutex;
@@ -127,7 +206,7 @@ static int32_t KYTY_SYSV_ABI Hmd2GetDeviceInformation(SceHmd2DeviceInformation*
if (Config::VrEnabled()) {
// Virtual headset defaults: READY, MOUNT, 63 mm lens separation.
info->hmuMount = 1;
info->lensSeparationDistance = 63;
info->lensSeparationDistance = LENS_SEPARATION_MM;
// Headset characteristics: 4000x2040 panel and 2 m virtual image distance.
info->deviceInfo.panelResolution = {PANEL_WIDTH, PANEL_HEIGHT};
info->virtualImageDistance = 2.0f;
@@ -149,9 +228,7 @@ static int32_t KYTY_SYSV_ABI Hmd2GetFieldOfViewWithoutHandle(SceHmd2FieldOfView*
if (fov == nullptr) {
return ERROR_PARAMETER_NULL;
}
// Virtual ER15 field of view: 55/42.5/51.2/51.2 degrees.
// These tangents describe that geometry, not bit-exact firmware output.
*fov = {1.4281480312f, 0.9163311720f, 1.2437491417f, 1.2437491417f};
*fov = {FOV_TAN_OUT, FOV_TAN_IN, FOV_TAN_TOP, FOV_TAN_BOTTOM};
LOGF("Hmd2: FOV tangents out=%f in=%f top=%f bottom=%f\n", fov->tanOut, fov->tanIn,
fov->tanTop, fov->tanBottom);
return OK;
@@ -212,6 +289,133 @@ static int32_t KYTY_SYSV_ABI Hmd2ReprojectionEnableVrMode(uint64_t output_mode)
return OK;
}
static int32_t KYTY_SYSV_ABI Hmd2Open(int32_t user_id, int32_t type, int32_t index,
const void* param) {
PRINT_NAME();
if (!g_initialized.load()) {
return ERROR_NOT_INITIALIZED;
}
if (user_id != Config::GetUserId() || type != 0 || index != 0 || param != nullptr) {
return ERROR_PARAMETER_INVALID;
}
return HMD_HANDLE;
}
static int32_t KYTY_SYSV_ABI Hmd2ReprojectionBeginFrame(int64_t flip_arg) {
PRINT_NAME();
LOGF("\t flip_arg = %" PRId64 "\n", flip_arg);
return OK;
}
// Microseconds of process time, the clock VrTracker2 predicts poses for. The headset shows a frame
// one refresh after it begins.
static int32_t KYTY_SYSV_ABI Hmd2ReprojectionGetPredictedDisplayTime(int64_t flip_arg,
uint64_t* timestamp) {
PRINT_NAME();
if (timestamp == nullptr) {
return ERROR_PARAMETER_NULL;
}
const auto* state = GetReprojectionState();
const bool fast = state != nullptr && state->output_mode.load() == OUTPUT_MODE_119_88HZ;
*timestamp = LibKernel::KernelGetProcessTime() + (fast ? 8342 : 11122);
LOGF("\t flip_arg = %" PRId64 ", time = %" PRIu64 "\n", flip_arg, *timestamp);
return OK;
}
static int32_t KYTY_SYSV_ABI Hmd2ReprojectionSetRenderConfig(SceHmd2ReprojectionRenderConfig config,
const void* option) {
PRINT_NAME();
LOGF("\t sync_mode = %u, phase = %u\n", config.syncMode, config.phase);
if (GetReprojectionState() == nullptr) {
return ERROR_REPROJECTION_NOT_INITIALIZED;
}
return option != nullptr ? ERROR_PARAMETER_INVALID : OK;
}
static int32_t KYTY_SYSV_ABI Hmd2ReprojectionSetParam(int32_t index, int64_t flip_arg,
const SceHmd2ReprojectionParam* param,
const void* option) {
PRINT_NAME();
if (GetReprojectionState() == nullptr) {
return ERROR_REPROJECTION_NOT_INITIALIZED;
}
if (param == nullptr || param->settings == nullptr) {
return ERROR_PARAMETER_NULL;
}
LOGF("\t index = %d, flip_arg = %" PRId64 ", layers = %d\n", index, flip_arg,
param->settingCount);
return option != nullptr ? ERROR_PARAMETER_INVALID : OK;
}
// No eye tracker: report DISABLE, which a PS5 also returns when the user turned eye tracking off.
template <typename Result>
static int32_t GazeResult(const SceHmd2GazeGetResultParam* param, Result* result) {
if (param == nullptr || result == nullptr) {
return ERROR_PARAMETER_NULL;
}
if (param->sizeOfThis != sizeof(*param) || result->sizeOfThis != sizeof(*result)) {
return ERROR_PARAMETER_INVALID;
}
if (param->handle != HMD_HANDLE) {
return ERROR_HANDLE_INVALID;
}
*result = {};
result->sizeOfThis = sizeof(*result);
result->status = GAZE_TRACKING_STATUS_DISABLE;
result->gazeDirection = {0.0f, 0.0f,
-1.0f}; // The display center; +z points away from the lens.
return OK;
}
static int32_t KYTY_SYSV_ABI Hmd2GazeGetResult(const SceHmd2GazeGetResultParam* param,
SceHmd2GazeResult* result) {
PRINT_NAME();
const auto error = GazeResult(param, result);
if (error == OK) {
result->timestamp = LibKernel::KernelGetProcessTime();
}
return error;
}
static int32_t KYTY_SYSV_ABI Hmd2GazeGetResultForFoveatedRendering(
const SceHmd2GazeGetResultParam* param, SceHmd2GazeResultForFoveatedRendering* result) {
PRINT_NAME();
return GazeResult(param, result);
}
bool GetReprojectionSurface(const void* reprojection_buffer, ReprojectionSurface& out) {
const auto* buffer = static_cast<const SceHmd2ReprojectionBuffer*>(reprojection_buffer);
if (buffer == nullptr || buffer->src == nullptr || buffer->srcCount == 0 ||
buffer->srcCount > REPROJECTION_MAX_SOURCE_COUNT ||
(buffer->src->layerType != REPROJECTION_LAYER_TYPE_VR3D &&
buffer->src->layerType != REPROJECTION_LAYER_TYPE_VR2D)) {
return false;
}
Graphics::ShaderTextureResource texture;
std::memcpy(texture.fields, buffer->src->data, sizeof(texture.fields));
// Eye textures are RGBA ordered render targets; flip them through the matching SDR format.
const auto& policies = Graphics::VIDEO_OUT_FORMAT_POLICIES;
const auto policy = std::find_if(policies.begin(), policies.end(), [&](const auto& p) {
return p.info.guest_format == texture.Format() &&
(p.pixel_format & 0xff000000u) == 0x22000000u &&
!Graphics::IsHdrVideoOutPixelFormat(p.pixel_format);
});
if (texture.IsNull() || texture.TileMode() != Graphics::Prospero::TileMode::kRenderTarget ||
policy == policies.end()) {
LOGF("Hmd2: unsupported eye texture format=%u tile=%u\n",
static_cast<uint32_t>(texture.Format()), static_cast<uint32_t>(texture.TileMode()));
return false;
}
if (buffer->src->layerType == REPROJECTION_LAYER_TYPE_VR3D &&
std::memcmp(buffer->src->data, buffer->src->data + std::size(texture.fields),
sizeof(texture.fields)) != 0) {
Log::WriteToConsoleAndLog(
"Hmd2: separate eye textures are not supported; showing the left one\n");
}
out = {texture.Base40(), texture.Width5() + 1u, texture.Height5() + 1u, policy->pixel_format};
return true;
}
static int32_t KYTY_SYSV_ABI Hmd2ReprojectionSetTiming(int32_t timing) {
auto* state = GetReprojectionState();
if (state == nullptr) {
@@ -235,6 +439,13 @@ LIB_DEFINE(InitHmd2_1) {
LIB_FUNC("C0rPwER-yxg", Hmd2::Hmd2ReprojectionInitialize);
LIB_FUNC("VVvFh51o20s", Hmd2::Hmd2ReprojectionEnableVrMode);
LIB_FUNC("FkQX7rjFomk", Hmd2::Hmd2ReprojectionSetTiming);
LIB_FUNC("f3kPeoTZnIE", Hmd2::Hmd2Open);
LIB_FUNC("Ocf081WpBpA", Hmd2::Hmd2ReprojectionBeginFrame);
LIB_FUNC("SVEG+1D7qHA", Hmd2::Hmd2ReprojectionGetPredictedDisplayTime);
LIB_FUNC("hA9LshbSkzw", Hmd2::Hmd2ReprojectionSetRenderConfig);
LIB_FUNC("xMo9ENEu2E0", Hmd2::Hmd2ReprojectionSetParam);
LIB_FUNC("lAoFUedcfqA", Hmd2::Hmd2GazeGetResult);
LIB_FUNC("retc+-uRMhk", Hmd2::Hmd2GazeGetResultForFoveatedRendering);
}
} // namespace Libs
+9
View File
@@ -739,6 +739,15 @@ LIB_DEFINE(InitNet_1_Http) {
LIB_FUNC("yigr4V0-HTM", Http::HttpSetRecvTimeOut);
LIB_FUNC("T-mGo9f3Pu4", Http::HttpSetAutoRedirect);
LIB_FUNC("qFg2SuyTJJY", Http::HttpSetAuthEnabled);
LIB_FUNC("h9wmFZX4i-4", Http::HttpSetRedirectCallback);
LIB_FUNC("Kh6bS2HQKbo", Http::HttpSetCookieRecvCallback);
LIB_FUNC("vO4B-42ef-k", Http::HttpSetRequestStatusCallback);
LIB_FUNC("i9mhafzkEi8", Http::HttpSetInflateGZIPEnabled);
LIB_FUNC("PDxS48xGQLs", Http::HttpSetChunkedTransferEnabled);
LIB_FUNC("DK+GoXCNT04", Http::HttpsLoadCert);
LIB_FUNC("zXqcE0fizz0", Http::HttpsUnloadCert);
LIB_FUNC("JBN6N-EY+3M", Http::HttpsGetSslError);
LIB_FUNC("0onIrKx9NIE", Http::HttpGetLastErrno);
LIB_FUNC("IWalAn-guFs", LibHttp::HttpUriParse);
LIB_FUNC("YuOW3dDAKYc", LibHttp::HttpUriEscape);
LIB_FUNC("5LZA+KPISVA", LibHttp::HttpUriBuild);
+2
View File
@@ -189,6 +189,8 @@ LIB_DEFINE(InitPad_1) {
LIB_FUNC("RR4novUEENY", Controller::PadSetLightBar);
LIB_FUNC("6ncge5+l5Qs", Controller::PadClose);
LIB_FUNC("n3kSX62fgNo", PadUnknownN3kSX62fgNo);
LIB_FUNC("iA-DdobUen8", Controller::PadVrControllerReadState);
LIB_FUNC("v8P+9PRqg10", Controller::PadVrControllerSetTriggerEffects);
}
namespace LibMouse {
+228
View File
@@ -0,0 +1,228 @@
#include "common/abi.h"
#include "common/logging/log.h"
#include "graphics/presentation/vrInput.h"
#include "kernel/pthread.h"
#include "libs/errno.h"
#include "libs/hmd2.h"
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace Libs {
LIB_VERSION("VrTracker2", 1, "VrTracker2", 1, 1);
namespace VrTracker2 {
constexpr int32_t ERROR_NOT_INIT = -1972109311; // 0x8a740001
constexpr int32_t ERROR_ALREADY_INITIALIZED = -1972109310; // 0x8a740002
constexpr int32_t ERROR_ARGUMENT_INVALID = -1972109306; // 0x8a740006
constexpr uint32_t MEMORY_SIZE = 32u << 20u;
constexpr uint32_t MEMORY_ALIGNMENT = 0x10000;
// Device types: the headset, then the right and the left controller.
constexpr int32_t DEVICE_TYPE_HMD = 0;
constexpr int32_t DEVICE_TYPE_VR_CONTROLLER_LEFT = 2;
constexpr uint32_t STATUS_TRACKING = 1;
constexpr uint32_t QUALITY_FULL = 9;
constexpr uint8_t MOTION_SENSOR_DATA_VALID = 3; // Velocity and acceleration.
struct SceFQuaternion {
float x;
float y;
float z;
float w;
};
struct SceVrTracker2QueryMemoryResult {
uint32_t sizeOfThis;
uint32_t memorySize;
uint32_t memoryAlignment;
uint32_t reserved[5];
};
struct SceVrTracker2InitializeParam {
uint32_t sizeOfThis;
uint32_t profile;
int32_t highThreadPriority;
int32_t lowThreadPriority;
uint64_t highThreadAffinity;
uint64_t lowThreadAffinity;
void* memoryPointer;
uint8_t options[32]; // Memory size and tracking options; nothing here depends on them.
};
struct SceVrTracker2GetResultParam {
uint32_t sizeOfThis;
int32_t handle;
uint32_t reserved00;
uint64_t predictionTime;
uint32_t reserved01;
int64_t flipArg;
uint32_t reserved02[2];
};
struct SceVrTracker2PoseData {
Hmd2::SceFVector3 position;
uint32_t reserved00;
SceFQuaternion orientation;
uint32_t reserved01[8];
};
struct SceVrTracker2ResultData {
int32_t handle;
uint32_t connected;
uint32_t playAreaUpdateCounter;
uint32_t playAreaMode;
uint64_t timestamp;
uint32_t status;
uint32_t positionQuality;
uint32_t orientationQuality;
Hmd2::SceFVector3 velocity;
Hmd2::SceFVector3 acceleration;
Hmd2::SceFVector3 angularVelocity;
uint32_t reserved01[3];
uint8_t motionSensorDataValidFlags;
uint8_t reserved010[3];
int64_t flipArg;
union {
struct {
SceVrTracker2PoseData devicePose;
SceVrTracker2PoseData leftEyePose;
SceVrTracker2PoseData rightEyePose;
SceVrTracker2PoseData headPose;
SceVrTracker2PoseData gazeOriginPose;
} hmdInfo;
struct {
SceVrTracker2PoseData devicePose;
uint64_t lastValidVisionTimestamp;
uint64_t lastValidSensorTimestamp;
uint32_t reserved[60];
} vrControllerInfo;
};
uint32_t reserved02;
};
static_assert(sizeof(SceVrTracker2QueryMemoryResult) == 0x20);
static_assert(sizeof(SceVrTracker2InitializeParam) == 0x48);
static_assert(offsetof(SceVrTracker2InitializeParam, memoryPointer) == 0x20);
static_assert(sizeof(SceVrTracker2GetResultParam) == 0x30);
static_assert(offsetof(SceVrTracker2GetResultParam, flipArg) == 0x20);
static_assert(sizeof(SceVrTracker2PoseData) == 0x40);
static_assert(sizeof(SceVrTracker2ResultData) == 0x1a8);
static_assert(offsetof(SceVrTracker2ResultData, status) == 0x18);
static_assert(offsetof(SceVrTracker2ResultData, motionSensorDataValidFlags) == 0x54);
static_assert(offsetof(SceVrTracker2ResultData, flipArg) == 0x58);
static_assert(offsetof(SceVrTracker2ResultData, hmdInfo.devicePose) == 0x60);
static_assert(offsetof(SceVrTracker2ResultData, hmdInfo.gazeOriginPose) == 0x160);
static_assert(offsetof(SceVrTracker2ResultData, vrControllerInfo.lastValidVisionTimestamp) == 0xa0);
static std::atomic<bool> g_initialized = false;
// Handle registered for each device type.
static std::atomic<int32_t> g_devices[DEVICE_TYPE_VR_CONTROLLER_LEFT + 1] = {-1, -1, -1};
// Writes a tracking-space pose moved along its own x axis by shift metres. w stays non-negative so
// that a rotation always has one representation, whatever sign the runtime reports.
static void SetPose(SceVrTracker2PoseData& out, const Vr::VrPose& pose, float shift = 0.0f) {
const auto [x, y, z, w] = pose.orientation;
const float s = w < 0.0f ? -1.0f : 1.0f;
out.position = {pose.position[0] + shift * (1.0f - 2.0f * (y * y + z * z)),
pose.position[1] + shift * 2.0f * (x * y + w * z),
pose.position[2] + shift * 2.0f * (x * z - w * y)};
out.orientation = {s * x, s * y, s * z, s * w};
}
static int32_t KYTY_SYSV_ABI QueryMemory(const void* param,
SceVrTracker2QueryMemoryResult* result) {
PRINT_NAME();
if (param == nullptr || result == nullptr) {
return ERROR_ARGUMENT_INVALID;
}
result->memorySize = MEMORY_SIZE;
result->memoryAlignment = MEMORY_ALIGNMENT;
return OK;
}
static int32_t KYTY_SYSV_ABI Initialize(const SceVrTracker2InitializeParam* param) {
PRINT_NAME();
if (param == nullptr || param->memoryPointer == nullptr) {
return ERROR_ARGUMENT_INVALID;
}
return g_initialized.exchange(true) ? ERROR_ALREADY_INITIALIZED : OK;
}
static int32_t KYTY_SYSV_ABI RegisterDevice(int32_t device_type, int32_t handle) {
PRINT_NAME();
LOGF("\t device_type = %d\n"
"\t handle = %d\n",
device_type, handle);
if (!g_initialized.load()) {
return ERROR_NOT_INIT;
}
if (device_type < DEVICE_TYPE_HMD || device_type > DEVICE_TYPE_VR_CONTROLLER_LEFT ||
handle < 0) {
return ERROR_ARGUMENT_INVALID;
}
g_devices[device_type].store(handle);
return OK;
}
// Without a headset runtime every device reports the identity pose at the tracking origin.
static int32_t KYTY_SYSV_ABI GetResult(const SceVrTracker2GetResultParam* param,
SceVrTracker2ResultData* result) {
PRINT_NAME();
if (param == nullptr || result == nullptr) {
return ERROR_ARGUMENT_INVALID;
}
if (!g_initialized.load()) {
return ERROR_NOT_INIT;
}
std::memset(result, 0, sizeof(*result));
result->handle = param->handle;
result->flipArg = param->flipArg;
result->timestamp =
param->predictionTime != 0 ? param->predictionTime : LibKernel::KernelGetProcessTime();
int32_t type = DEVICE_TYPE_HMD;
while (type <= DEVICE_TYPE_VR_CONTROLLER_LEFT && g_devices[type].load() != param->handle) {
type++;
}
if (param->handle < 0 || type > DEVICE_TYPE_VR_CONTROLLER_LEFT) {
return OK; // Not registered: not connected, tracking not started.
}
result->connected = 1;
result->status = STATUS_TRACKING;
result->positionQuality = QUALITY_FULL;
result->orientationQuality = QUALITY_FULL;
result->motionSensorDataValidFlags = MOTION_SENSOR_DATA_VALID;
if (type == DEVICE_TYPE_HMD) {
const auto pose = Vr::OpenXrGetHeadPose();
auto& hmd = result->hmdInfo;
SetPose(hmd.devicePose, pose);
SetPose(hmd.leftEyePose, pose, -Hmd2::EYE_OFFSET_M);
SetPose(hmd.rightEyePose, pose, Hmd2::EYE_OFFSET_M);
hmd.headPose = hmd.devicePose;
hmd.gazeOriginPose = hmd.devicePose;
return OK;
}
auto& controller = result->vrControllerInfo;
SetPose(controller.devicePose,
Vr::OpenXrGetController(type == DEVICE_TYPE_VR_CONTROLLER_LEFT ? 0 : 1).pose);
controller.lastValidVisionTimestamp = result->timestamp;
controller.lastValidSensorTimestamp = result->timestamp;
return OK;
}
} // namespace VrTracker2
LIB_DEFINE(InitVrTracker2_1) {
LIB_FUNC("TwqZnaIjWv4", VrTracker2::QueryMemory);
LIB_FUNC("6Jy73SRfG-o", VrTracker2::Initialize);
LIB_FUNC("Dog+g25QYjw", VrTracker2::RegisterDevice);
LIB_FUNC("J4Vh3VVX0iU", VrTracker2::GetResult);
}
} // namespace Libs
+2
View File
@@ -92,6 +92,7 @@ LIB_DEFINE(InitFont_1);
LIB_DEFINE(InitFontFt_1);
LIB_DEFINE(InitAgcDriver_1);
LIB_DEFINE(InitHmd2_1);
LIB_DEFINE(InitVrTracker2_1);
LIB_DEFINE(InitLibKernel_1);
LIB_DEFINE(InitNet_1);
LIB_DEFINE(InitPad_1);
@@ -131,6 +132,7 @@ void InitAll(Loader::SymbolDatabase* s) {
InitDeviceService_1(s);
InitUsbd_1(s);
InitHmd2_1(s);
InitVrTracker2_1(s);
InitLibKernel_1(s);
LibMouse::InitMouse_1(s);
LibKeyboard::InitKeyboard_1(s);
+144
View File
@@ -136,6 +136,7 @@ public:
bool HttpValidTemplate(Id tmpl_id);
bool HttpValidConnection(Id conn_id);
bool HttpValidRequest(Id req_id);
bool HttpValidBase(Id id);
Id HttpCreateConnection(Id tmpl_id, const char* server_name, const char* scheme, uint16_t port,
bool enable_keep_alive);
Id HttpCreateConnectionWithURL(Id tmpl_id, const char* url, bool enable_keep_alive);
@@ -410,6 +411,12 @@ bool Network::HttpValidRequest(Id req_id) {
m_requests[req_id.GetId()].used);
}
bool Network::HttpValidBase(Id id) {
Common::LockGuard lock(m_mutex);
return FindHttpBase(id, true) != nullptr;
}
Network::HttpBase* Network::FindHttpBase(Id id, bool include_request) {
const auto index = id.GetId();
@@ -3013,6 +3020,143 @@ int KYTY_SYSV_ABI HttpSetAuthEnabled(int id, int enable) {
return OK;
}
static int HttpCheckBase(int id) {
EXIT_IF(g_net == nullptr);
return g_net->HttpValidBase(Network::Id(id)) ? OK : HTTP_ERROR_INVALID_ID;
}
int KYTY_SYSV_ABI HttpSetRedirectCallback(int id, void* /*cbfunc*/, void* /*user_arg*/) {
PRINT_NAME();
LOGF("\t id = %d\n", id);
return HttpCheckBase(id);
}
int KYTY_SYSV_ABI HttpSetCookieRecvCallback(int id, void* /*cbfunc*/, void* /*user_arg*/) {
PRINT_NAME();
LOGF("\t id = %d\n", id);
return HttpCheckBase(id);
}
int KYTY_SYSV_ABI HttpSetRequestStatusCallback(int id, void* /*cbfunc*/, void* /*user_arg*/) {
PRINT_NAME();
LOGF("\t id = %d\n", id);
return HttpCheckBase(id);
}
int KYTY_SYSV_ABI HttpSetInflateGZIPEnabled(int id, int is_enable) {
PRINT_NAME();
LOGF("\t id = %d\n"
"\t is_enable = %d\n",
id, is_enable);
return HttpCheckBase(id);
}
int KYTY_SYSV_ABI HttpSetChunkedTransferEnabled(int id, int is_enabled) {
PRINT_NAME();
LOGF("\t id = %d\n"
"\t is_enabled = %d\n",
id, is_enabled);
EXIT_IF(g_net == nullptr);
if (is_enabled != 0 && is_enabled != 1) {
return HTTP_ERROR_INVALID_VALUE;
}
if (!g_net->HttpValidRequest(Network::Id(id))) {
return HTTP_ERROR_INVALID_ID;
}
return OK;
}
int KYTY_SYSV_ABI HttpsLoadCert(int http_ctx_id, int ca_cert_num, const void** /*ca_list*/,
const void* /*cert*/, const void* /*priv_key*/) {
PRINT_NAME();
LOGF("\t http_ctx_id = %d\n"
"\t ca_cert_num = %d\n",
http_ctx_id, ca_cert_num);
EXIT_IF(g_net == nullptr);
if (!g_net->HttpValid(Network::Id(http_ctx_id))) {
return HTTP_ERROR_INVALID_ID;
}
return OK;
}
int KYTY_SYSV_ABI HttpsUnloadCert(int http_ctx_id) {
PRINT_NAME();
LOGF("\t http_ctx_id = %d\n", http_ctx_id);
EXIT_IF(g_net == nullptr);
if (!g_net->HttpValid(Network::Id(http_ctx_id))) {
return HTTP_ERROR_INVALID_ID;
}
return OK;
}
int KYTY_SYSV_ABI HttpsGetSslError(int request_id, int32_t* err_num, uint32_t* detail) {
PRINT_NAME();
LOGF("\t request_id = %d\n"
"\t err_num = 0x%016" PRIx64 "\n"
"\t detail = 0x%016" PRIx64 "\n",
request_id, reinterpret_cast<uint64_t>(err_num), reinterpret_cast<uint64_t>(detail));
if (err_num == nullptr || detail == nullptr) {
return HTTP_ERROR_INVALID_VALUE;
}
*err_num = 0;
*detail = 0;
EXIT_IF(g_net == nullptr);
if (!g_net->HttpValidRequest(Network::Id(request_id))) {
return HTTP_ERROR_INVALID_ID;
}
return OK;
}
int KYTY_SYSV_ABI HttpGetLastErrno(int request_id, int* err_num) {
PRINT_NAME();
LOGF("\t request_id = %d\n"
"\t err_num = 0x%016" PRIx64 "\n",
request_id, reinterpret_cast<uint64_t>(err_num));
if (err_num == nullptr) {
return HTTP_ERROR_INVALID_VALUE;
}
*err_num = 0;
EXIT_IF(g_net == nullptr);
if (!g_net->HttpValidRequest(Network::Id(request_id))) {
return HTTP_ERROR_INVALID_ID;
}
return OK;
}
int KYTY_SYSV_ABI HttpAddRequestHeader(int id, const char* name, const char* value, uint32_t mode) {
PRINT_NAME();
+10
View File
@@ -136,6 +136,16 @@ int KYTY_SYSV_ABI HttpSetSendTimeOut(int id, uint32_t usec);
int KYTY_SYSV_ABI HttpSetRecvTimeOut(int id, uint32_t usec);
int KYTY_SYSV_ABI HttpSetAutoRedirect(int id, int enable);
int KYTY_SYSV_ABI HttpSetAuthEnabled(int id, int enable);
int KYTY_SYSV_ABI HttpSetRedirectCallback(int id, void* cbfunc, void* user_arg);
int KYTY_SYSV_ABI HttpSetCookieRecvCallback(int id, void* cbfunc, void* user_arg);
int KYTY_SYSV_ABI HttpSetRequestStatusCallback(int id, void* cbfunc, void* user_arg);
int KYTY_SYSV_ABI HttpSetInflateGZIPEnabled(int id, int is_enable);
int KYTY_SYSV_ABI HttpSetChunkedTransferEnabled(int id, int is_enabled);
int KYTY_SYSV_ABI HttpsLoadCert(int http_ctx_id, int ca_cert_num, const void** ca_list,
const void* cert, const void* priv_key);
int KYTY_SYSV_ABI HttpsUnloadCert(int http_ctx_id);
int KYTY_SYSV_ABI HttpsGetSslError(int request_id, int32_t* err_num, uint32_t* detail);
int KYTY_SYSV_ABI HttpGetLastErrno(int request_id, int* err_num);
} // namespace Http
+162 -75
View File
@@ -344,7 +344,16 @@ enum class Ngs2RackType {
CustomSampler,
};
struct Ngs2UserFxContext {
struct Ngs2UserFxProcessContext {
float** channels;
std::array<uintptr_t, 3> user_data;
uint32_t flags;
uint32_t num_channels;
uint32_t grain_samples;
uint32_t sample_rate;
};
struct Ngs2UserFx2Context {
void* common;
void* param;
void* work;
@@ -354,7 +363,7 @@ struct Ngs2UserFxContext {
uint64_t reserved[4] {};
};
struct Ngs2UserFxProcessContext {
struct Ngs2UserFx2ProcessContext {
float** channels;
void* common;
const void* param;
@@ -370,18 +379,27 @@ struct Ngs2UserFxProcessContext {
uint64_t reserved2[4] {};
};
struct Ngs2UserFxOption {
struct Ngs2UserFx2Option {
Ngs2CustomModuleOption header;
int KYTY_SYSV_ABI (*setup)(Ngs2UserFxContext*);
int KYTY_SYSV_ABI (*cleanup)(Ngs2UserFxContext*);
int KYTY_SYSV_ABI (*setup)(Ngs2UserFx2Context*);
int KYTY_SYSV_ABI (*cleanup)(Ngs2UserFx2Context*);
uintptr_t control;
int KYTY_SYSV_ABI (*process)(Ngs2UserFxProcessContext*);
int KYTY_SYSV_ABI (*process)(Ngs2UserFx2ProcessContext*);
size_t common_size;
size_t param_size;
size_t work_size;
uintptr_t user_data;
};
static bool Ngs2RackIsCustom(Ngs2RackType type) {
switch (type) {
case Ngs2RackType::CustomSubmixer:
case Ngs2RackType::CustomMastering:
case Ngs2RackType::CustomSampler: return true;
default: return false;
}
}
struct Ngs2RackInternal {
Ngs2Internal* ngs = nullptr;
Ngs2RackInternal* next = nullptr;
@@ -390,7 +408,16 @@ struct Ngs2RackInternal {
Ngs2ContextBufferInfo buffer_info;
Ngs2BufferAllocator allocator;
std::array<std::vector<uint8_t>, 24> common;
std::array<Ngs2UserFxOption, 24> fx {};
std::array<Ngs2UserFx2Option, 24> fx {};
[[nodiscard]] uint32_t NumBuffers() const {
return Ngs2RackIsCustom(type) ? option.custom_sampler.custom_rack_option.num_buffers : 1;
}
[[nodiscard]] uint32_t PortBufferId(size_t port) const {
const auto& custom = option.custom_sampler.custom_rack_option;
return Ngs2RackIsCustom(type) ? custom.port[port].source_buffer_id : 0;
}
};
enum class Ngs2VoicePlayState : uint32_t { Empty = 0, Playing = 3, Paused = 5, Stopped = 0xb };
@@ -519,10 +546,12 @@ struct Ngs2VoiceInternal {
uint32_t repeated_count = 0;
};
struct Module {
std::vector<uint8_t> param, work, state;
uint32_t flags = 1;
bool control_warned = false;
bool render_warned = false;
std::vector<uint8_t> param, work, state;
void KYTY_SYSV_ABI (*handler)(Ngs2UserFxProcessContext*) = nullptr;
std::array<uintptr_t, 3> user_data {};
uint32_t flags = 1;
bool control_warned = false;
bool render_warned = false;
};
Ngs2VoicePlayState state = Ngs2VoicePlayState::Empty;
uint32_t state_flags = 0;
@@ -572,6 +601,15 @@ struct Ngs2VoiceInternal {
channels * sizeof(int16_t));
}
// Custom racks keep num_buffers consecutive channel-major buffers; other racks keep one.
void ClearSamples(uint32_t grain) {
samples.assign(size_t(grain) * channels * rack->NumBuffers(), 0.0f);
}
float* Buffer(uint32_t id, uint32_t grain) {
return samples.data() + size_t(id) * grain * channels;
}
void ResetSetupState() {
SetEvent(4);
std::ranges::fill(ports, Port {});
@@ -665,7 +703,6 @@ struct Ngs2VoiceInternal {
info.num_skip_samples = 0;
info.num_samples = 0;
}
EXIT_NOT_IMPLEMENTED(info.num_repeats != 0 && rack->type != Ngs2RackType::Sampler);
EXIT_NOT_IMPLEMENTED(decoder == nullptr &&
(uint64_t(info.num_skip_samples) + info.num_samples) *
channels * sizeof(int16_t) >
@@ -839,9 +876,10 @@ static Ngs2RackInternal* g_racks_list = nullptr;
static std::atomic_uint32_t g_next_ngs_uid = 1;
static Common::Mutex g_racks_mutex;
static_assert(sizeof(Ngs2UserFxContext) == 72);
static_assert(sizeof(Ngs2UserFxProcessContext) == 104);
static_assert(sizeof(Ngs2UserFxOption) == 72);
static_assert(sizeof(Ngs2UserFxProcessContext) == 48);
static_assert(sizeof(Ngs2UserFx2Context) == 72);
static_assert(sizeof(Ngs2UserFx2ProcessContext) == 104);
static_assert(sizeof(Ngs2UserFx2Option) == 72);
static_assert(sizeof(Ngs2SystemOption) == 144);
static_assert(sizeof(Ngs2SystemInfo) == 184);
static_assert(sizeof(Ngs2RackOption) == 176);
@@ -900,15 +938,6 @@ static Ngs2Internal* Ngs2FindSystem(uintptr_t system_handle) {
return nullptr;
}
static bool Ngs2RackIsCustom(Ngs2RackType type) {
switch (type) {
case Ngs2RackType::CustomSubmixer:
case Ngs2RackType::CustomMastering:
case Ngs2RackType::CustomSampler: return true;
default: return false;
}
}
int KYTY_SYSV_ABI Ngs2SystemQueryBufferSize(const Ngs2SystemOption* option,
Ngs2ContextBufferInfo* buffer_info) {
PRINT_NAME();
@@ -1188,14 +1217,23 @@ static void Ngs2InitModules(Ngs2RackInternal& rack) {
return;
}
const auto& custom = rack.option.custom_sampler.custom_rack_option;
EXIT_NOT_IMPLEMENTED(custom.num_buffers != 1);
EXIT_NOT_IMPLEMENTED(custom.num_buffers == 0 || custom.num_buffers > 4 ||
rack.option.common.max_ports > 16);
for (uint32_t p = 0; p < rack.option.common.max_ports; ++p) {
EXIT_NOT_IMPLEMENTED(custom.port[p].source_buffer_id >= custom.num_buffers);
}
auto* voices = reinterpret_cast<Ngs2VoiceInternal*>(&rack + 1);
for (uint32_t m = 0; m < custom.num_modules; ++m) {
if (custom.module[m].module_id != 0x1f) {
const auto& info = custom.module[m];
// Modules process their source buffer in place, so skipped modules pass audio through.
EXIT_NOT_IMPLEMENTED(info.source_buffer_id != info.dest_buffer_id ||
std::max(info.source_buffer_id, info.extra_buffer_id) >=
custom.num_buffers);
if (info.module_id != 0x1f) {
continue;
}
auto& fx = rack.fx[m];
fx = *reinterpret_cast<const Ngs2UserFxOption*>(custom.module[m].option);
fx = *reinterpret_cast<const Ngs2UserFx2Option*>(info.option);
rack.common[m].resize(fx.common_size);
for (uint32_t i = 0; i < rack.option.common.max_voices; ++i) {
auto& module = voices[i].modules[m];
@@ -1203,7 +1241,7 @@ static void Ngs2InitModules(Ngs2RackInternal& rack) {
module.work.resize(fx.work_size);
module.state.resize(custom.module[m].state_size);
if (fx.setup != nullptr) {
Ngs2UserFxContext context {
Ngs2UserFx2Context context {
rack.common[m].data(), module.param.data(), module.work.data(),
fx.user_data, rack.option.common.max_voices, i};
EXIT_NOT_IMPLEMENTED(fx.setup(&context) != OK);
@@ -1430,12 +1468,12 @@ int KYTY_SYSV_ABI Ngs2RackDestroy(uintptr_t rack_handle, Ngs2ContextBufferInfo*
const auto& fx = rack->fx[m];
if (fx.cleanup != nullptr) {
auto& module = voices[i].modules[m];
Ngs2UserFxContext context {rack->common[m].data(),
module.param.data(),
module.work.data(),
fx.user_data,
rack->option.common.max_voices,
i};
Ngs2UserFx2Context context {rack->common[m].data(),
module.param.data(),
module.work.data(),
fx.user_data,
rack->option.common.max_voices,
i};
EXIT_NOT_IMPLEMENTED(fx.cleanup(&context) != OK);
}
}
@@ -1729,7 +1767,7 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
}
if (voice.source_generation != generation) {
// A callback replaced the source, possibly changing its channel layout.
voice.samples.assign(size_t(grain) * voice.channels, 0.0f);
voice.ClearSamples(grain);
output = 0;
}
voice.has_samples = output != 0;
@@ -1747,7 +1785,7 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
}
EXIT_NOT_IMPLEMENTED(voice.rendering);
voice.rendering = true;
voice.samples.assign(grain * voice.channels, 0.0f);
voice.ClearSamples(grain);
voice.has_samples = false;
if (voice.state == Ngs2VoicePlayState::Playing) {
if (voice.rack->type == Ngs2RackType::Sampler ||
@@ -1770,7 +1808,8 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
}
}
for (auto* source: voices) {
for (const auto& port: source->ports) {
for (size_t p = 0; p < source->ports.size(); ++p) {
const auto& port = source->ports[p];
if (port.dest != &voice || port.volume == 0.0f) {
continue;
}
@@ -1778,7 +1817,10 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
if (!source->has_samples) {
continue;
}
EXIT_NOT_IMPLEMENTED(port.input != 0);
// Input N of a custom rack voice feeds its buffer N.
EXIT_NOT_IMPLEMENTED(port.input >= voice.rack->NumBuffers());
const auto* input = source->Buffer(source->rack->PortBufferId(p), grain);
auto* output = voice.Buffer(port.input, grain);
const auto* matrix = port.matrix < 0 ? nullptr : &source->matrices.at(port.matrix);
const size_t output_channels =
matrix == nullptr
@@ -1791,8 +1833,7 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
? (src == dst ? 1.0f : 0.0f)
: (*matrix)[dst * source->channels + src]);
for (uint32_t i = 0; i < grain; ++i) {
voice.samples[dst * grain + i] +=
source->samples[src * grain + i] * level;
output[dst * grain + i] += input[src * grain + i] * level;
}
}
}
@@ -1801,36 +1842,50 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
}
if (voice.has_samples) {
const auto& custom = voice.rack->option.custom_sampler.custom_rack_option;
const auto rate = voice.rack->ngs->option.sample_rate;
for (size_t m = 0; m < voice.modules.size(); ++m) {
if (custom.module[m].module_id != 0x1f) {
if (!voice.modules[m].render_warned) {
const auto& info = custom.module[m];
auto& module = voice.modules[m];
if (info.module_id != 0x19 && info.module_id != 0x1f) {
if (!module.render_warned) {
Log::WriteToConsoleAndLog(fmt::sprintf(
"warning: NGS2 custom module 0x%02x at index %zu is ignored during render\n",
custom.module[m].module_id, m));
voice.modules[m].render_warned = true;
info.module_id, m));
module.render_warned = true;
}
continue;
}
EXIT_NOT_IMPLEMENTED(custom.module[m].source_buffer_id != 0 ||
custom.module[m].dest_buffer_id != 0);
auto& module = voice.modules[m];
const auto& fx = voice.rack->fx[m];
std::vector<float*> channels(voice.channels);
for (uint32_t c = 0; c < voice.channels; ++c) {
channels[c] = voice.samples.data() + c * grain;
if (info.module_id == 0x19 && module.handler == nullptr) {
continue;
}
// Handlers receive the source channels followed by the extra buffer's channels.
std::vector<float*> channels(2 * size_t(voice.channels));
auto* source = voice.Buffer(info.source_buffer_id, grain);
auto* extra = voice.Buffer(info.extra_buffer_id, grain);
for (uint32_t c = 0; c < voice.channels; ++c) {
channels[c] = source + c * grain;
channels[voice.channels + c] = extra + c * grain;
}
if (info.module_id == 0x19) {
Ngs2UserFxProcessContext context {channels.data(), module.user_data,
module.flags, voice.channels,
grain, rate};
module.handler(&context);
} else {
const auto& fx = voice.rack->fx[m];
Ngs2UserFx2ProcessContext context {channels.data(),
voice.rack->common[m].data(),
module.param.data(),
module.work.data(),
module.state.data(),
fx.user_data,
module.flags,
voice.channels,
voice.channels,
grain,
rate};
EXIT_NOT_IMPLEMENTED(fx.process(&context) != OK);
}
Ngs2UserFxProcessContext context {channels.data(),
voice.rack->common[m].data(),
module.param.data(),
module.work.data(),
module.state.data(),
fx.user_data,
module.flags,
voice.channels,
voice.channels,
grain,
voice.rack->ngs->option.sample_rate};
EXIT_NOT_IMPLEMENTED(fx.process(&context) != OK);
module.flags = 0;
}
}
@@ -1867,7 +1922,9 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff
const auto grain = ngs->option.num_grain_samples;
for (auto* voice: voices) {
Ngs2RenderVoice(*voice, voices, grain);
if (voice->rack->type != Ngs2RackType::Mastering || !voice->has_samples) {
const bool mastering = voice->rack->type == Ngs2RackType::Mastering ||
voice->rack->type == Ngs2RackType::CustomMastering;
if (!mastering || !voice->has_samples) {
continue;
}
EXIT_NOT_IMPLEMENTED(voice->output_id >= num_buffer_info);
@@ -2387,24 +2444,39 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
EXIT_NOT_IMPLEMENTED(!Ngs2RackIsCustom(voice->rack->type));
const auto index = param->id & 0x1fu;
EXIT_NOT_IMPLEMENTED(index >= voice->modules.size());
if ((param->id & 0xffffffe0u) != 0x40001f00u ||
voice->rack->option.custom_sampler.custom_rack_option.module[index].module_id !=
0x1f) {
if (!voice->modules[index].control_warned) {
const auto module_id =
voice->rack->option.custom_sampler.custom_rack_option.module[index].module_id;
auto& module = voice->modules[index];
if ((param->id & 0xffffffe0u) != (0x40000000u | module_id << 8u) ||
(module_id != 0x19 && module_id != 0x1f)) {
if (!module.control_warned) {
Log::WriteToConsoleAndLog(fmt::sprintf(
"warning: NGS2 custom control 0x%08" PRIx32 " is ignored\n",
param->id));
voice->modules[index].control_warned = true;
module.control_warned = true;
}
break;
}
struct FxParam {
if (module_id == 0x19) {
struct FxParam {
Ngs2VoiceParamHeader header;
decltype(module.handler) handler;
std::array<uintptr_t, 3> user_data;
};
EXIT_NOT_IMPLEMENTED(param->size != sizeof(FxParam));
const auto& fx = *reinterpret_cast<const FxParam*>(param);
// Enabling the handler requests a reset; later updates mark user data dirty.
module.flags |= module.handler == nullptr ? 1u : 2u;
module.handler = fx.handler;
module.user_data = fx.user_data;
break;
}
struct Fx2Param {
Ngs2VoiceParamHeader header;
const void* data;
size_t size;
};
const auto& fx = *reinterpret_cast<const FxParam*>(param);
auto& module = voice->modules[index];
const auto& fx = *reinterpret_cast<const Fx2Param*>(param);
EXIT_NOT_IMPLEMENTED(voice->rack->fx[index].control != 0 ||
fx.size != module.param.size());
std::memcpy(module.param.data(), fx.data, fx.size);
@@ -2439,7 +2511,6 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
break;
}
case 4: {
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Sampler);
for (auto& block: voice->blocks) {
if (block.info.num_repeats != 0) {
block.info.num_repeats = 0;
@@ -2483,9 +2554,25 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
voice->channels = channels[0];
break;
}
case 0x4003:
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomMastering);
case 0x4003: {
// SceNgs2CustomMasteringVoiceSetupParam {numInputChannels, flags} or
// SceNgs2CustomMasteringVoiceOutputParam {outputId, reserved}.
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomMastering ||
param->size != sizeof(Ngs2VoiceParamHeader) + 8u);
const auto* values = reinterpret_cast<const uint32_t*>(param + 1);
switch (param->id & 0xffffu) {
case 0:
EXIT_NOT_IMPLEMENTED(values[0] == 0 ||
values[0] > voice->rack->option.common.max_voice_channels ||
values[1] != 0);
voice->channels = values[0];
break;
case 1: voice->output_id = values[0]; break;
default:
EXIT("unsupported custom mastering control: 0x%08" PRIx32 "\n", param->id);
}
break;
}
default: EXIT("unknown rack_id: 0x%" PRIx32 "\n", rack_id);
}
+158
View File
@@ -0,0 +1,158 @@
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "graphics/presentation/vrInput.h"
#include "kernel/pthread.h"
#include "libs/controller.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "libs/padData.h"
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <cstring>
// Virtual headset only: the PS VR2 Sense controller ports. A headset runtime drives them when one
// is running; otherwise each hand reads its half of the first player's keyboard or gamepad.
namespace Libs::Controller {
LIB_NAME("Pad", "Pad");
constexpr int PAD_PORT_TYPE_VR_CONTROLLER_LEFT = 3;
// The buttons of each Sense controller. The left Create button is reserved for the system.
constexpr uint32_t HAND_BUTTONS[2] = {
PAD_BUTTON_L1 | PAD_BUTTON_L2 | PAD_BUTTON_L3 | PAD_BUTTON_TRIANGLE | PAD_BUTTON_SQUARE,
PAD_BUTTON_R1 | PAD_BUTTON_R2 | PAD_BUTTON_R3 | PAD_BUTTON_CIRCLE | PAD_BUTTON_CROSS |
PAD_BUTTON_OPTIONS};
struct PadVrControllerData {
uint32_t buttons;
uint8_t stick_x;
uint8_t stick_y;
uint8_t stick_padding[2];
uint8_t analog_l2;
uint8_t analog_r2;
uint8_t analog_padding[2];
uint32_t finger_touch;
uint8_t analog_finger_touch[4];
float acceleration[3];
float angular_velocity[3];
float orientation[4];
uint8_t reserve1[4];
bool connected;
uint8_t connected_count;
uint64_t timestamp;
uint8_t reserve2[24];
};
static_assert(sizeof(PadVrControllerData) == 104);
static_assert(offsetof(PadVrControllerData, finger_touch) == 12);
static_assert(offsetof(PadVrControllerData, acceleration) == 20);
static_assert(offsetof(PadVrControllerData, orientation) == 44);
static_assert(offsetof(PadVrControllerData, connected) == 64);
static_assert(offsetof(PadVrControllerData, timestamp) == 72);
static std::atomic<bool> g_open[2];
int PadVrPortHand(int type) {
const bool vr =
type == PAD_PORT_TYPE_VR_CONTROLLER_LEFT || type == PAD_PORT_TYPE_VR_CONTROLLER_LEFT + 1;
return vr && Config::VrEnabled() ? type - PAD_PORT_TYPE_VR_CONTROLLER_LEFT : -1;
}
int PadVrPort(int user_id, int hand, int index, bool open) {
if (!PadInitialized()) {
return PAD_ERROR_NOT_INITIALIZED;
}
const int handle = PAD_HANDLE_VR_LEFT + hand;
if (user_id != Config::GetUserId() || index != 0) {
return open ? PAD_ERROR_INVALID_ARG : PAD_ERROR_NO_HANDLE;
}
if (!open) {
return g_open[hand].load() ? handle : PAD_ERROR_NO_HANDLE;
}
return g_open[hand].exchange(true) ? PAD_ERROR_ALREADY_OPENED : handle;
}
int PadVrCheckHandle(int handle) {
const int hand = PadVrHand(handle);
return hand >= 0 && g_open[hand].load() ? OK : PAD_ERROR_INVALID_HANDLE;
}
int PadVrClose(int handle) {
const int hand = PadVrHand(handle);
return hand >= 0 && g_open[hand].exchange(false) ? OK : PAD_ERROR_INVALID_HANDLE;
}
static uint8_t ToByte(float value) {
return static_cast<uint8_t>(std::clamp(value, 0.0f, 1.0f) * 255.0f + 0.5f);
}
int KYTY_SYSV_ABI PadVrControllerReadState(int handle, PadVrControllerData* data) {
PRINT_NAME();
const int hand = PadVrHand(handle);
if (hand < 0) {
return PAD_ERROR_INVALID_HANDLE;
}
if (data == nullptr) {
return PAD_ERROR_INVALID_ARG;
}
PadData pad {};
if (const auto error = PadReadState(handle, &pad); error != OK) {
return error;
}
*data = {};
auto& trigger = hand == 0 ? data->analog_l2 : data->analog_r2;
data->buttons = pad.buttons & HAND_BUTTONS[hand];
data->stick_x = hand == 0 ? pad.left_stick_x : pad.right_stick_x;
data->stick_y = hand == 0 ? pad.left_stick_y : pad.right_stick_y;
trigger = hand == 0 ? pad.analog_buttons_l2 : pad.analog_buttons_r2;
std::memcpy(data->acceleration, &pad.acceleration_x, sizeof(data->acceleration));
std::memcpy(data->angular_velocity, &pad.angular_velocity_x, sizeof(data->angular_velocity));
std::memcpy(data->orientation, &pad.orientation_x, sizeof(data->orientation));
data->connected = pad.connected;
data->connected_count = pad.connected_count;
data->timestamp = pad.timestamp;
if (const auto xr = Vr::OpenXrGetController(hand); xr.connected) {
// Touch controllers have no button left for the right Options, so the left Menu stands in.
const bool options = hand == 1 && Vr::OpenXrGetController(0).menu;
// Each mapping names both hands; HAND_BUTTONS keeps this hand's half.
const uint32_t buttons = (xr.trigger > 0.5f ? PAD_BUTTON_L2 | PAD_BUTTON_R2 : 0u) |
(xr.squeeze > 0.5f ? PAD_BUTTON_L1 | PAD_BUTTON_R1 : 0u) |
(xr.stick_click ? PAD_BUTTON_L3 | PAD_BUTTON_R3 : 0u) |
(xr.primary ? PAD_BUTTON_SQUARE | PAD_BUTTON_CROSS : 0u) |
(xr.secondary ? PAD_BUTTON_TRIANGLE | PAD_BUTTON_CIRCLE : 0u) |
(options ? PAD_BUTTON_OPTIONS : 0u);
data->buttons = buttons & HAND_BUTTONS[hand];
data->stick_x = ToByte(xr.stick_x * 0.5f + 0.5f);
data->stick_y = ToByte(0.5f - xr.stick_y * 0.5f);
trigger = ToByte(xr.trigger);
if (xr.pose.valid) {
std::memcpy(data->orientation, xr.pose.orientation, sizeof(data->orientation));
}
data->connected = true;
data->connected_count = std::max<uint8_t>(data->connected_count, 1);
data->timestamp = LibKernel::KernelGetProcessTime();
}
return OK;
}
int KYTY_SYSV_ABI PadVrControllerSetTriggerEffects(int left_handle, int right_handle,
const PadTriggerEffectParam* param) {
PRINT_NAME();
LOGF("\t left_handle = %d\n"
"\t right_handle = %d\n",
left_handle, right_handle);
if (PadVrHand(left_handle) != 0 || PadVrCheckHandle(left_handle) != OK ||
PadVrHand(right_handle) != 1 || PadVrCheckHandle(right_handle) != OK) {
return PAD_ERROR_INVALID_HANDLE;
}
return param != nullptr ? OK : PAD_ERROR_INVALID_ARG;
}
} // namespace Libs::Controller
+8
View File
@@ -90,6 +90,14 @@ void Delay(Uint32) {}
#undef SDL_CloseGamepad
#undef SDL_Delay
namespace Libs::Controller {
// No virtual headset in these tests (padVr.cpp is not linked here).
int PadVrPortHand(int) { return -1; }
int PadVrPort(int, int, int, bool) { return PAD_ERROR_INVALID_ARG; }
int PadVrCheckHandle(int) { return PAD_ERROR_INVALID_HANDLE; }
int PadVrClose(int) { return PAD_ERROR_INVALID_HANDLE; }
} // namespace Libs::Controller
namespace Libs::Controller::DualSenseHaptics {
bool SetVibration(int, uint8_t large_motor, uint8_t small_motor, uint32_t duration_ms) {
haptics.push_back({large_motor, small_motor, duration_ms});
+57 -6
View File
@@ -47,6 +47,7 @@ struct Fixture {
} else {
rack.option.custom_sampler.max_waveform_blocks = capacity;
rack.option.custom_sampler.custom_rack_option.state_size = sizeof(Ngs2CustomSamplerVoiceState);
rack.option.custom_sampler.custom_rack_option.num_buffers = 1;
}
}
@@ -703,6 +704,53 @@ void TestSubmixerSetupAndRouting() {
}
}
uint32_t g_side_chain_flags = 0;
void KYTY_SYSV_ABI MultiplyBySideChain(Ngs2UserFxProcessContext* context) {
Check(context->num_channels == 1 && context->grain_samples == 1 &&
context->user_data[0] == 7,
"user FX context did not describe the custom voice");
g_side_chain_flags = context->flags;
context->channels[0][0] *= context->channels[1][0];
}
void TestCustomSideChainUserFx() {
Fixture main(48000), side(48000);
Ngs2RackInternal custom_rack {};
auto& custom = custom_rack.option.custom_submixer.custom_rack_option;
custom_rack.ngs = &main.system;
custom_rack.type = Ngs2RackType::CustomSubmixer;
custom.num_buffers = 2;
custom.num_modules = 1;
custom.module[0] = {nullptr, 0x19, 0, 1, 0};
Ngs2VoiceInternal voice;
voice.rack = &custom_rack;
voice.ports.resize(1);
voice.modules.resize(1);
struct {
Ngs2VoiceParamHeader header {24, 0, 0x40020000};
uint32_t inputs = 1, outputs = 1, flags = 0, reserved = 0;
} setup;
struct {
Ngs2VoiceParamHeader header;
void KYTY_SYSV_ABI (*handler)(Ngs2UserFxProcessContext*);
uintptr_t user_data[3];
} fx {{40, 0, 0x40001900}, MultiplyBySideChain, {7, 0, 0}};
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&voice), &setup.header) == OK &&
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&voice), &fx.header) == OK,
"custom submixer user FX setup failed");
voice.state = Ngs2VoicePlayState::Playing;
main.voice.ports.push_back({&voice, 0, 1.0f, -1});
side.voice.ports.push_back({&voice, 1, 1.0f, -1});
const int16_t main_pcm = 8192, side_pcm = 16384;
main.Queue(&main_pcm, 1, 0);
side.Queue(&side_pcm, 1, 0);
Ngs2RenderVoice(voice, {&main.voice, &side.voice, &voice}, 1);
Check(voice.has_samples && voice.samples[0] == 0.125f && voice.samples[1] == 0.5f &&
g_side_chain_flags == 1,
"user FX did not receive the side-chain buffer after the source channels");
}
void TestPcm16RenderOutput() {
Ngs2Internal system;
system.option = Ngs2DefaultSystemOption();
@@ -1145,9 +1193,9 @@ void KYTY_SYSV_ABI RecordLoopCallback(const Ngs2VoiceCallbackInfo* info) {
}
}
void TestLoopCallbacksAndExit() {
void TestLoopCallbacksAndExit(Ngs2RackType type) {
const int16_t pcm[] = {8192, 16384, 24576};
Fixture f(48000);
Fixture f(48000, 1, type);
LoopCallbacks log;
f.voice.callback = reinterpret_cast<uintptr_t>(&RecordLoopCallback);
f.voice.callback_data = reinterpret_cast<uintptr_t>(&log);
@@ -1158,7 +1206,7 @@ void TestLoopCallbacksAndExit() {
"loop and completion callbacks lost the completed repeat count");
Check(f.voice.blocks.empty(), "finite loop did not complete");
Fixture active(48000);
Fixture active(48000, 1, type);
log.events.clear();
log.pause = true;
active.voice.callback = reinterpret_cast<uintptr_t>(&RecordLoopCallback);
@@ -1170,7 +1218,8 @@ void TestLoopCallbacksAndExit() {
Check(log.events.size() == 1 && active.voice.decoded_samples == 2 &&
active.voice.blocks.front().cursor == 0 && active.voice.samples[2] == 0,
"loop callback pause consumed another iteration");
Ngs2VoiceParamHeader exit_loop {sizeof(Ngs2VoiceParamHeader), 0, 0x10000004};
Ngs2VoiceParamHeader exit_loop {sizeof(Ngs2VoiceParamHeader), 0,
type == Ngs2RackType::Sampler ? 0x10000004u : 0x40010004u};
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&active.voice), &exit_loop);
active.voice.SetEvent(32);
active.Render(5);
@@ -1178,7 +1227,7 @@ void TestLoopCallbacksAndExit() {
active.voice.blocks.empty() && log.events.size() == 1,
"exiting infinite loop lost its final iteration or ignored callback flags");
Fixture pending(48000);
Fixture pending(48000, 1, type);
pending.Queue(pcm, 1, 1);
pending.Queue(pcm + 1, 1, 1, UINT32_MAX);
pending.Queue(pcm + 2, 1, 0);
@@ -1608,13 +1657,15 @@ int main() {
TestAtrac9Discrete();
TestPcm16RenderOutput();
TestSubmixerSetupAndRouting();
TestCustomSideChainUserFx();
TestWaveformBlockGeometry();
TestAtrac9ResampleAfterStarvation();
TestAtrac9ResampleAcrossFrames();
TestResamplingPhaseAfterStarvation();
TestSamplerReuseResetsRouting();
TestDeferredSamplerSetup();
TestLoopCallbacksAndExit();
TestLoopCallbacksAndExit(Ngs2RackType::Sampler);
TestLoopCallbacksAndExit(Ngs2RackType::CustomSampler);
TestAtrac9Looping();
TestAtrac9WaveformJoining();
TestWaveformReadAddress();
+103 -1
View File
@@ -52,6 +52,7 @@ void CheckFatal(F &&function, std::string_view expected, const char *message) {
struct Fixture {
Program program;
Block *block = nullptr;
ShaderComputeInputInfo compute_info{};
explicit Fixture(ShaderType stage = ShaderType::Compute) {
program.stage = stage;
@@ -131,7 +132,9 @@ struct Fixture {
Value(0u), Value(0u), Value(0u)});
}
void PlanAndTrack(Libs::Graphics::ShaderStageInputInfo input = {}) {
void PlanAndTrack() { PlanAndTrack({.compute = &compute_info}); }
void PlanAndTrack(Libs::Graphics::ShaderStageInputInfo input) {
for (size_t index = 0; index < program.blocks.size(); ++index) {
const auto condition = program.blocks[index]->condition;
if (!condition.IsEmpty())
@@ -3418,6 +3421,104 @@ void TestIndexedMaterialImageKeys() {
}
}
void TestPointerTableIndexedMaterialKeys() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
// A waterfall scalarizes one lane's indexed x3 key; matching lanes sample the
// descriptor at the user-data pointer plus key * 32.
Fixture f;
auto *entry = f.block, *body = f.AddBlock(), *done = f.AddBlock();
entry->AddBranch(body);
entry->AddBranch(done);
body->AddBranch(done);
const auto lane = f.Emit(ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
const auto active = f.Emit(ValueOpcode::INotEqual32, {lane, Value(UINT32_MAX)});
const auto material = f.Buffer({f.UserData(0), f.UserData(1), f.UserData(2), f.UserData(3)});
MemoryInfo request;
request.kind = ResourceKind::Buffer;
request.idxen = true;
const auto loaded = f.Emit(ValueOpcode::LoadBufferU32x3,
{material, lane, Value(0u), Value(0u), active}, f.AddMemory(request, 0x28));
const auto selected = f.Emit(ValueOpcode::SelectU32,
{active, f.Emit(ValueOpcode::CompositeExtractU32x3, {loaded, Value(2u)}), Value(0u)});
const auto key = f.Emit(ValueOpcode::ReadLane, {selected, f.UserData(6)});
const auto guard = f.Emit(ValueOpcode::LogicalAnd,
{active, f.Emit(ValueOpcode::IEqual32, {key, selected})});
entry->condition = f.Emit(ValueOpcode::ConditionRef, {guard}, CFG::BranchCondition::ExecNonZero);
entry->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = body, .false_block = done};
body->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = done};
done->terminator.kind = CFG::TerminatorKind::Return;
f.block = body;
const auto table = f.Address(f.UserData(4), f.UserData(5));
const auto offset = f.Emit(ValueOpcode::ShiftLeftLogical32, {key, Value(5u)});
std::array<Value, 8> words;
for (uint32_t word = 0; word < words.size(); ++word) {
MemoryInfo scalar;
scalar.kind = ResourceKind::ScalarAddress;
scalar.offset = word * 4u;
words[word] = f.Emit(ValueOpcode::LoadAddressU32, {table, offset, Value(0u), Value(true)},
f.AddMemory(scalar, 0x60));
}
MemoryInfo sample;
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
f.Emit(ValueOpcode::ImageSampleRaw,
{f.Image(words, 0x6c), f.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)}),
f.ImageAddress(), guard},
f.AddMemory(sample, 0x6c));
f.PlanAndTrack();
const auto plan = ExtractResourcePlan(f.program);
Check(plan.info.images.size() == 1u, "pointer-table waterfall image was not tracked");
const auto &indirect = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
Check(indirect.selector && indirect.selector->stride == 0u && indirect.selector->offset == 8u &&
!indirect.selector->global && !indirect.table_scalar && indirect.table_stride == 32u &&
plan.descriptor_sources[indirect.table_source].dword_count == 2u,
"pointer-table waterfall lost its indexed x3 material selector");
constexpr uint32_t base = 0x1000u, heap = 0x2000u, records = 6u;
struct Memory {
std::vector<uint32_t> words = std::vector<uint32_t>((heap - base + 16u * 32u) / 4u);
uint32_t descriptors = 0;
} memory;
const auto put = [&](uint32_t key) {
auto *texture = memory.words.data() + (heap - base + key * 32u) / 4u;
texture[0] = 0x100u + key;
texture[1] = static_cast<uint32_t>(Libs::Graphics::Prospero::BufferFormat::k16UNorm) << 20u;
texture[3] = (9u << 28u) | Libs::Graphics::DstSel(4, 5, 6, 7);
};
for (const uint32_t key : {0u, 5u, 9u}) put(key);
for (uint32_t row = 0; row < records; ++row)
memory.words[(row * 12u + 8u) / 4u] = row % 2u == 0u ? 9u : 5u;
const std::array<uint32_t, 7> user_data{base, 12u << 16u, records, 0x5204u, heap, 0u, 0u};
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &m = *static_cast<Memory *>(data);
if (address < base || address - base + words.size_bytes() > m.words.size() * 4u) return false;
if (address >= heap) {
const auto key = (address - heap) / 32u;
if (words.size() != 8u || (key != 0u && key != 5u && key != 9u)) return false;
++m.descriptors;
}
std::copy_n(m.words.begin() + (address - base) / 4u, words.size(), words.begin());
return true;
};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = read};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.descriptors == 3u &&
snapshot.images.size() == 3u,
"pointer-table selector did not read exactly the zero and material-key descriptors");
Check(std::ranges::find(snapshot.specialization_reads, std::pair<uint64_t, uint64_t>{base + 8u, 64u}) !=
snapshot.specialization_reads.end(),
"pointer-table selector rows escaped writable-alias tracking");
const auto mapping = specialization.images[0].indirect_mapping_offset;
Check(snapshot.images[0].dwords[0] == 0x100u && specialization.images[0].indirect_search_iterations == 2u &&
snapshot.flattened_srt[mapping] == 3u && snapshot.flattened_srt[mapping + 3u] == 5u &&
snapshot.flattened_srt[mapping + 5u] == 9u,
"pointer-table selector lost sorted raw keys or the OOB-zero root descriptor");
}
void TestImageDescriptorFields() {
constexpr std::array<std::pair<uint32_t, uint32_t>, 5> reserved{
{{1u, 0x20000000u}, {2u, 0x70003000u}, {4u, 0xe000e000u},
@@ -8698,6 +8799,7 @@ int main() {
Run("bounded compute image loop", TestBoundedComputeImageLoop);
Run("uniformized material image keys", TestUniformizedMaterialImageKeys);
Run("indexed material image keys", TestIndexedMaterialImageKeys);
Run("pointer-table indexed material keys", TestPointerTableIndexedMaterialKeys);
Run("image descriptor fields", TestImageDescriptorFields);
Run("draw-uniform scalar image", TestUniformScalarBufferImage);
Run("compact scalar image table", TestCompactScalarImageTable);
+5 -3
View File
@@ -14,6 +14,7 @@
namespace {
using namespace Libs::Graphics::ShaderRecompiler::IR;
using Libs::Graphics::ShaderComputeInputInfo;
using Libs::Graphics::ShaderType;
void Check(bool condition, const char *message) {
@@ -37,6 +38,7 @@ void CheckFatal(F &&function, std::string_view expected, const char *message) {
struct Fixture {
Program program;
ShaderComputeInputInfo compute{};
explicit Fixture(uint32_t block_count = 1) {
program.stage = ShaderType::Compute;
@@ -73,7 +75,7 @@ struct Fixture {
return static_cast<uint32_t>(program.memory_info.size() - 1u);
}
void Plan() { TrackResources(program, {}, {}); }
void Plan() { TrackResources(program, {}, {}, {.compute = &compute}); }
};
struct TestMemory {
@@ -158,7 +160,7 @@ void TestScalarMemoryDomainMismatchFails() {
Fixture raw;
const auto raw_memory = raw.AddMemory(ResourceKind::ScalarBuffer);
RawRead(raw, Address(raw, Value(0x1000u), Value(0u)), Value(0u), raw_memory);
CheckFatal([&] { TrackResources(raw.program, {}, {}); },
CheckFatal([&] { raw.Plan(); },
"incompatible scalar memory metadata",
"raw scalar load accepted descriptor-buffer metadata");
@@ -169,7 +171,7 @@ void TestScalarMemoryDomainMismatchFails() {
{Value(0x1000u), Value(0u), Value(16u), Value(0u)});
buffer.EmitMemory(ValueOpcode::ReadConstBuffer, {resource, Value(0u)},
buffer_memory);
CheckFatal([&] { TrackResources(buffer.program, {}, {}); },
CheckFatal([&] { buffer.Plan(); },
"incompatible scalar memory metadata",
"descriptor scalar load accepted raw-address metadata");
}
+229 -83
View File
@@ -84,6 +84,7 @@
#include <cstring>
#include <initializer_list>
#include <limits>
#include <map>
#include <memory>
#include <mutex>
#include <numeric>
@@ -43692,6 +43693,58 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
std::printf("[host] %-32s ok\n", "Ps5GameExampleImageClear");
}
struct TessellationStage {
// Immediate byte addresses of the lowered accesses, per attribute kind.
std::map<ShaderRecompiler::IR::TessellationAttribute, std::set<u32>> addresses;
std::string assembly;
};
TessellationStage CompileTessellationStage(const char *name, const std::vector<u32> &code,
ShaderType stage,
const ShaderTessellationInputInfo &tess) {
using namespace ShaderRecompiler;
ShaderVertexInputInfo vertex;
vertex.logical_stage = stage;
vertex.tess = tess;
CompileOptions options;
options.stage = stage;
options.input_info.vertex = &vertex;
auto translated = TranslateProgram(code, options);
TessellationStage result;
for (const auto *block : translated.program.blocks) {
for (const auto &inst : *block) {
if ((inst.GetOpcode() == IR::ValueOpcode::GetTessellationAttribute ||
inst.GetOpcode() == IR::ValueOpcode::SetTessellationAttribute) &&
inst.Arg(1).IsImmediate()) {
result.addresses[static_cast<IR::TessellationAttribute>(inst.Arg(0).U32())].insert(
inst.Arg(1).U32());
}
}
}
auto compiled = CompileProgram(std::move(translated), options, {});
ValidateSpirv(name, compiled.spirv);
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2);
Require(name, "disassembly", tools.Disassemble(compiled.spirv, &result.assembly),
"failed to disassemble tessellation shader");
return result;
}
// Whether a Patch uvec4[vectors] variable of a storage class sits at a Location.
bool HasPatchInterface(const std::string &assembly, const std::string &storage, u32 vectors,
u32 location) {
const auto variable = " = OpVariable %_ptr_" + storage + "__arr_v4uint_uint_" +
std::to_string(vectors) + " " + storage + "\n";
const auto end = assembly.find(variable);
if (end == std::string::npos) {
return false;
}
const auto begin = assembly.rfind('%', end);
const auto id = assembly.substr(begin, end - begin);
return assembly.find("OpDecorate " + id + " Patch\n") != std::string::npos &&
assembly.find("OpDecorate " + id + " Location " + std::to_string(location) + "\n") !=
std::string::npos;
}
void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) {
using namespace ShaderRecompiler;
const bool multiplied_stride = hs_stride == 112;
@@ -43700,8 +43753,7 @@ void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) {
const u32 relative_patch = multiplied_stride ? 28 : 32;
const u32 output_address = multiplied_stride ? 31 : 35;
const u32 input_address = multiplied_stride ? 25 : 29;
std::array<std::vector<u32>, 3> code;
auto &local = code[0];
std::vector<u32> local, control, evaluation;
// The fused LS addresses one control point using its v3 ordinal.
local.push_back(EncodeVop2(0x0b, local_address, 255u, 3));
local.push_back(ls_stride);
@@ -43710,7 +43762,6 @@ void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) {
local.push_back(EncodeSop1(0x20, 0, 6));
local.push_back(0xffffffffu); // Shader footer is not executable after LS handoff.
auto &control = code[1];
AppendVop3(&control, 0x365, 0, 193u, InlineU32(0));
AppendVop3(&control, 0x366, 0, 193u, Vgpr(0));
control.push_back(EncodeSopp(0x0a));
@@ -43769,7 +43820,6 @@ void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) {
}
AppendEnd(&control);
auto &evaluation = code[2];
AppendVop3(&evaluation, 0x169, 24, 255u, Vgpr(7));
evaluation.push_back(3 * hs_stride);
evaluation.push_back(EncodeMubuf0(0x0d, 28));
@@ -43790,91 +43840,187 @@ void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) {
tess.ls_stride == ls_stride && tess.hs_stride == hs_stride,
"captured LS and HS address arithmetic must produce distinct strides");
constexpr std::array stages{ShaderType::Local, ShaderType::TessellationControl,
ShaderType::TessellationEvaluation};
for (u32 stage = 0; stage < stages.size(); stage++) {
ShaderVertexInputInfo vertex;
vertex.logical_stage = stages[stage];
vertex.tess = tess;
CompileOptions options;
options.stage = stages[stage];
options.input_info.vertex = &vertex;
auto translated = TranslateProgram(code[stage], options);
std::set<u32> local_offsets, evaluation_offsets, factor_offsets, patch_offsets;
for (const auto *block : translated.program.blocks) {
for (const auto &inst : *block) {
if (inst.GetOpcode() != IR::ValueOpcode::GetTessellationAttribute &&
inst.GetOpcode() != IR::ValueOpcode::SetTessellationAttribute) {
continue;
}
if (!inst.Arg(1).IsImmediate()) {
continue;
}
switch (static_cast<IR::TessellationAttribute>(inst.Arg(0).U32())) {
case IR::TessellationAttribute::LocalOutput:
local_offsets.insert(inst.Arg(1).U32());
break;
case IR::TessellationAttribute::EvaluationInput:
evaluation_offsets.insert(inst.Arg(1).U32());
break;
case IR::TessellationAttribute::Factor:
factor_offsets.insert(inst.Arg(1).U32());
break;
case IR::TessellationAttribute::PatchOutput:
patch_offsets.insert(inst.Arg(1).U32());
break;
default:
break;
}
}
}
Require(name, "separate DS offsets", stage != 0 || local_offsets == std::set<u32>{4, 12},
"DS_WRITE2 must preserve both independently encoded offsets");
Require(name, "offchip component addressing",
stage != 2 ||
evaluation_offsets == std::set<u32>{28, 2 * hs_stride + 28},
"TES control point and component offsets must survive lowering");
Require(name, "triangle factor layout",
stage != 1 || factor_offsets == std::set<u32>{0, 4, 8, 12},
"the four packed guest factors must retain their order");
Require(name, "predicated patch addresses",
stage != 1 || patch_offsets == std::set<u32>{0x7fc0, 0x7fd0, 0x7fe0, 0x7ff0},
"CP0 patch stores must use the address assigned on their active predicate");
auto result = CompileProgram(std::move(translated), options, {});
ValidateSpirv(name, result.spirv);
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2);
std::string assembly;
Require(name, "disassembly", tools.Disassemble(result.spirv, &assembly),
"failed to disassemble tessellation shader");
Require(name, "native local inputs",
stage != 0 || (assembly.find("VertexIndex") != std::string::npos &&
assembly.find("InstanceIndex") != std::string::npos),
"LS v2/v5 must read native vertex/instance indices");
Require(name, "control point lane",
stage != 1 || (assembly.find("InvocationId") != std::string::npos &&
assembly.find("SubgroupLocalInvocationId") == std::string::npos),
"HS lane masks must use control point invocation IDs");
Require(name, "control point barrier",
stage != 1 || (assembly.find("OpMemoryModel Logical GLSL450") != std::string::npos &&
assembly.find("OpControlBarrier %uint_2 %uint_4 %uint_0") != std::string::npos),
"HS barriers must match GLSL450 control point synchronization semantics");
Require(name, "factor builtins",
stage != 1 || (assembly.find("TessLevelOuter") != std::string::npos &&
assembly.find("TessLevelInner") != std::string::npos),
"triangle factors require outer[4] and inner[2] Vulkan builtins");
Require(name, "native domain and winding",
stage != 2 || (assembly.find("TessCoord") != std::string::npos &&
assembly.find("Triangles") != std::string::npos &&
assembly.find("SpacingFractionalOdd") != std::string::npos &&
assembly.find("VertexOrderCw") != std::string::npos),
"TES must preserve guest triangle domain, partitioning, and winding");
}
Require(name, "patch constant range", tess.patch_base == 0x7fc0u && tess.patch_size == 52u,
"CP0 patch stores must reflect the HS patch constant range");
using Attribute = IR::TessellationAttribute;
auto ls = CompileTessellationStage(name, local, ShaderType::Local, tess);
auto hs = CompileTessellationStage(name, control, ShaderType::TessellationControl, tess);
auto ds = CompileTessellationStage(name, evaluation, ShaderType::TessellationEvaluation, tess);
Require(name, "separate DS offsets", ls.addresses[Attribute::LocalOutput] == std::set<u32>{4, 12},
"DS_WRITE2 must preserve both independently encoded offsets");
Require(name, "offchip component addressing",
ds.addresses[Attribute::EvaluationInput] == std::set<u32>{28, 2 * hs_stride + 28},
"TES control point and component offsets must survive lowering");
Require(name, "triangle factor layout",
hs.addresses[Attribute::Factor] == std::set<u32>{0, 4, 8, 12},
"the four packed guest factors must retain their order");
Require(name, "predicated patch addresses",
hs.addresses[Attribute::PatchOutput] == std::set<u32>{0x7fc0, 0x7fd0, 0x7fe0, 0x7ff0},
"CP0 patch stores must use the address assigned on their active predicate");
Require(name, "native local inputs",
ls.assembly.find("VertexIndex") != std::string::npos &&
ls.assembly.find("InstanceIndex") != std::string::npos,
"LS v2/v5 must read native vertex/instance indices");
Require(name, "control point lane",
hs.assembly.find("InvocationId") != std::string::npos &&
hs.assembly.find("SubgroupLocalInvocationId") == std::string::npos,
"HS lane masks must use control point invocation IDs");
Require(name, "control point barrier",
hs.assembly.find("OpMemoryModel Logical GLSL450") != std::string::npos &&
hs.assembly.find("OpControlBarrier %uint_2 %uint_4 %uint_0") != std::string::npos,
"HS barriers must match GLSL450 control point synchronization semantics");
Require(name, "factor builtins",
hs.assembly.find("TessLevelOuter") != std::string::npos &&
hs.assembly.find("TessLevelInner") != std::string::npos,
"triangle factors require outer[4] and inner[2] Vulkan builtins");
Require(name, "native domain and winding",
ds.assembly.find("TessCoord") != std::string::npos &&
ds.assembly.find("Triangles") != std::string::npos &&
ds.assembly.find("SpacingFractionalOdd") != std::string::npos &&
ds.assembly.find("VertexOrderCw") != std::string::npos,
"TES must preserve guest triangle domain, partitioning, and winding");
std::printf("[host] %-32s ok\n", name);
}
// PPSA25468: CP0 writes patch constants below the 32 KiB offchip ring end
// through mul24 addresses that reuse data VGPRs under nested EXEC masks, and
// the TES reads them back.
void CheckTessellationPatchConstants() {
using namespace ShaderRecompiler;
using Attribute = IR::TessellationAttribute;
constexpr const char *name = "TessellationPatchConstants";
constexpr u32 ls_stride = 48, hs_stride = 80, patch_stride = 0xf0;
std::vector<u32> local;
// One 128-bit DS store writes the LS record.
local.push_back(EncodeVop2(0x0b, 20, 255u, 3)); // v_mul_u32_u24 v20, ls_stride, v3
local.push_back(ls_stride);
local.push_back(EncodeDs0(0xdf, 16)); // ds_write_b128 v20, v[2:5] offset:16
local.push_back(EncodeDs1(0, 2, 20));
local.push_back(EncodeSop1(0x20, 0, 6));
const auto make_control = [&](bool divergent) {
std::vector<u32> control;
AppendVop3(&control, 0x365, 0, 193u, InlineU32(0));
AppendVop3(&control, 0x366, 0, 193u, Vgpr(0));
control.push_back(EncodeVopc(0xd1, 249u, 3));
control.push_back(EncodeVopcSdwa(0, 0, 0, 6, 1, 0, 0, 0, 0, 0, 0, 0, 1));
const auto skip_control = control.size();
control.push_back(0u); // s_cbranch_execz
AppendVop3(&control, 0x148, 16, Vgpr(1), InlineU32(8), InlineU32(5));
control.push_back(EncodeVop2(0x1b, 50, 255u, 1)); // v_and_b32 v50, 0xff, v1
control.push_back(0xffu);
control.push_back(EncodeVop2(0x0b, 0, 255u, 16)); // v_mul_u32_u24 v0, hs_stride, v16
control.push_back(hs_stride);
AppendVop3(&control, 0x169, 1, Vgpr(16), 255u); // v_mul_lo_u32 v1, v16, ls_stride
control.push_back(ls_stride);
AppendVop3(&control, 0x143, 17, 255u, Vgpr(50), Vgpr(0));
control.push_back(3 * hs_stride);
AppendVop3(&control, 0x143, 14, 255u, Vgpr(50), Vgpr(1));
control.push_back(3 * ls_stride);
// ds_read2_b32 v[10:11], v14 offset0:4 offset1:5
control.push_back(EncodeDs0(0x37, (5u << 8u) | 4u));
control.push_back(EncodeDs1(10, 0, 14));
control.push_back(EncodeMubuf0(0x1d, 16));
control.push_back(EncodeMubuf1(10, 2, 17, 2));
// v17 stays affine at the forward branch's join unless the paths diverge.
control.push_back(EncodeVopc(0x01, InlineU32(0), 10)); // v_cmp_lt_f32 vcc, 0, v10
const auto skip_scale = control.size();
control.push_back(0u); // s_cbranch_vccz
control.push_back(EncodeVop1(0x01, 12, 242u)); // v_mov_b32 v12, 1.0
if (divergent) {
control.push_back(EncodeVop2(0x25, 17, InlineU32(4), 17)); // v_add_nc_u32 v17, 4, v17
}
control[skip_scale] = EncodeSopp(0x06, control.size() - skip_scale - 1u);
control.push_back(EncodeMubuf0(0x1c, 24));
control.push_back(EncodeMubuf1(12, 2, 17, 2));
// CP0 reuses data VGPR v10 for the ring address 0x8000 - (patch + 1) * 0xf0.
control.push_back(EncodeVopc(0xd4, InlineU32(1), 16)); // v_cmpx_gt_u32 1, v16
const auto skip_patch = control.size();
control.push_back(0u); // s_cbranch_execz
control.push_back(EncodeVop2(0x25, 10, InlineU32(1), 50)); // v_add_nc_u32 v10, 1, v50
control.push_back(EncodeVop2(0x0b, 10, 255u, 10)); // v_mul_u32_u24 v10, 0xf0, v10
control.push_back(patch_stride);
control.push_back(EncodeVop2(0x26, 10, 255u, 10)); // v_sub_nc_u32 v10, 0x8000, v10
control.push_back(0x8000u);
control.push_back(EncodeVopc(0x01, InlineU32(0), 11)); // v_cmp_lt_f32 vcc, 0, v11
control.push_back(EncodeSop1(0x24, 0, 106)); // s_and_saveexec_b64 s[0:1], vcc
const auto skip_nested = control.size();
control.push_back(0u); // s_cbranch_execz
control.push_back(EncodeMubuf0(0x1c, 0)); // buffer_store_dword v11, v10, s[8:11], s2 offen
control.push_back(EncodeMubuf1(11, 2, 10, 2));
control[skip_nested] = EncodeSopp(0x08, control.size() - skip_nested - 1u);
control.push_back(EncodeSop1(0x04, 126, 0)); // s_mov_b64 exec, s[0:1]
// buffer_store_dwordx3 v[11:13], v10, s[8:11], s2 offen offset:16
control.push_back(EncodeMubuf0(0x1f, 16));
control.push_back(EncodeMubuf1(11, 2, 10, 2));
control[skip_control] = EncodeSopp(0x08, control.size() - skip_control - 1u);
control[skip_patch] = EncodeSopp(0x08, control.size() - skip_patch - 1u);
AppendEnd(&control);
return control;
};
const auto control = make_control(false);
std::vector<u32> evaluation;
AppendVop3(&evaluation, 0x169, 24, 255u, Vgpr(7));
evaluation.push_back(3 * hs_stride);
evaluation.push_back(EncodeVop2(0x25, 2, InlineU32(1), 7));
evaluation.push_back(EncodeVop2(0x0b, 2, 255u, 2));
evaluation.push_back(patch_stride);
evaluation.push_back(EncodeVop2(0x26, 2, 255u, 2));
evaluation.push_back(0x8000u);
evaluation.push_back(EncodeMubuf0(0x0d, hs_stride + 16)); // CP1 dwords 4-5
evaluation.push_back(EncodeMubuf1(10, 2, 24, 4));
evaluation.push_back(EncodeMubuf0(0x0d, 16)); // patch constant dwords 4-5
evaluation.push_back(EncodeMubuf1(12, 2, 2, 4));
evaluation.push_back(EncodeVop2(0x03, 10, Vgpr(12), 10)); // v_add_f32 v10, v12, v10
evaluation.push_back(EncodeVop2(0x03, 11, Vgpr(13), 11));
evaluation.push_back(EncodeExp0(0x0c, 0xf));
evaluation.push_back(EncodeExp1(5, 6, 10, 11));
AppendEnd(&evaluation);
ShaderTessellationInputInfo tess{.input_control_points = 3,
.output_control_points = 3,
.domain = 1,
.partitioning = 2,
.output_topology = 2};
AnalyzeTessellationPrograms(local, control, tess);
Require(name, "reflected interface",
tess.ls_stride == ls_stride && tess.hs_stride == hs_stride &&
tess.patch_base == 0x7f10u && tess.patch_size == 28u,
"b128 LS records, joined HS paths and CP0 patch stores must reflect the layout");
auto ls = CompileTessellationStage(name, local, ShaderType::Local, tess);
auto hs = CompileTessellationStage(name, control, ShaderType::TessellationControl, tess);
auto ds = CompileTessellationStage(name, evaluation, ShaderType::TessellationEvaluation, tess);
Require(name, "128-bit LS record",
ls.addresses[Attribute::LocalOutput] == std::set<u32>{16, 20, 24, 28},
"DS_WRITE_B128 must lower to four consecutive LS outputs");
Require(name, "predicated patch outputs",
hs.addresses[Attribute::PatchOutput] == std::set<u32>{0x7f10, 0x7f20, 0x7f24, 0x7f28},
"CP0 mul24 addresses under nested EXEC masks must reach patch constants");
Require(name, "patch inputs",
ds.addresses[Attribute::PatchInput] == std::set<u32>{0x7f20, 0x7f24} &&
ds.addresses[Attribute::EvaluationInput] ==
std::set<u32>{hs_stride + 16, hs_stride + 20},
"TES patch constant reads must not alias control point inputs");
Require(name, "matching patch interface",
HasPatchInterface(hs.assembly, "Output", 2, 5) &&
HasPatchInterface(ds.assembly, "Input", 2, 5),
"HS and TES patch variables must share their Location and type");
std::printf("[host] %-32s ok\n", name);
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
ExpectFatal("TessellationDivergentJoin", [&] {
ShaderTessellationInputInfo info{.input_control_points = 3, .output_control_points = 3};
AnalyzeTessellationPrograms(local, make_control(true), info);
});
#endif
}
void CheckTessellationPrograms() {
CheckTessellationProgram("TessellationShiftedStride", 124, 128);
CheckTessellationProgram("TessellationMultipliedStride", 108, 112);
CheckTessellationPatchConstants();
}
void CheckEmbeddedFetchVertexOffset() {
+12 -9
View File
@@ -1596,20 +1596,23 @@ void TestDirectMapQueryOffsetAndPartialMunmap() {
ExpectUnmapped(test, base + SceKernelPageSize);
constexpr uint64_t transaction_sentinel = 0x5452414e53414354ull; // "TRANSACT"
constexpr uint64_t rejected_write = 0x4e4f504152544941ull; // "NOPARTIA"
constexpr uint64_t sparse_write = 0x4e4f504152544941ull; // "NOPARTIA"
constexpr uint64_t backed_prefix_mask = 0xffffffffull;
const auto crossing_address = base + SceKernelPageSize - sizeof(uint32_t);
std::memcpy(reinterpret_cast<void*>(alias_base + SceKernelPageSize - sizeof(uint32_t)),
&transaction_sentinel, sizeof(transaction_sentinel));
Check(test,
!Libs::LibKernel::Memory::TryWriteBacking(crossing_address, &rejected_write,
sizeof(rejected_write)),
"TryWriteBacking should reject a range crossing an unmapped span");
uint64_t backing_after_rejected_write = 0;
std::memcpy(&backing_after_rejected_write,
Libs::LibKernel::Memory::TryWriteBacking(crossing_address, &sparse_write,
sizeof(sparse_write)),
"TryWriteBacking should write a range crossing an unmapped span");
uint64_t backing_after_sparse_write = 0;
std::memcpy(&backing_after_sparse_write,
reinterpret_cast<const void*>(alias_base + SceKernelPageSize - sizeof(uint32_t)),
sizeof(backing_after_rejected_write));
Check(test, backing_after_rejected_write == transaction_sentinel,
"failed backing writes must not modify a validated prefix");
sizeof(backing_after_sparse_write));
Check(test,
backing_after_sparse_write == ((sparse_write & backed_prefix_mask) |
(transaction_sentinel & ~backed_prefix_mask)),
"sparse backing writes must fill the backed prefix and skip the unmapped span");
uint64_t rejected_read = transaction_sentinel;
Check(test,
!Libs::LibKernel::Memory::TryReadBacking(crossing_address, &rejected_read,
+32 -17
View File
@@ -6214,6 +6214,10 @@ void TestNewShaderRecompilerImageGatherVariants() {
EncodeMimg1(76, 0, 1, 20), // image_gather4_c_o
EncodeMimg0(0x5f, 0x1),
EncodeMimg1(80, 0, 1, 24), // image_gather4_c_lz_o
EncodeMimg0(0xc8, 0x1),
EncodeMimg1(84, 0, 1, 28), // image_gather4_c_a
EncodeMimg0(0xd8, 0x1),
EncodeMimg1(88, 0, 1, 32), // image_gather4_c_o_a
0xbf810000u,
};
@@ -6249,6 +6253,10 @@ void TestNewShaderRecompilerImageGatherVariants() {
Check((result.decoded_dump.find("sample_flags=compare|offset|level_zero addr_components=4") != std::string::npos),
"IMAGE_GATHER4_C_LZ_O did not expose compare+offset+level-zero sample "
"metadata");
Check((result.decoded_dump.find("sample_flags=compare|adjust addr_components=3") != std::string::npos),
"PS5 IMAGE_GATHER4_C_A did not expose compare+adjust sample metadata");
Check((result.decoded_dump.find("sample_flags=compare|offset|adjust addr_components=4") != std::string::npos),
"PS5 IMAGE_GATHER4_C_O_A did not expose compare+offset+adjust sample metadata");
Check((result.ir_dump.find("ImageGather4 v60") != std::string::npos),
"IMAGE_GATHER4_LZ did not lower to shared IR ImageGather4");
Check((result.ir_dump.find("ImageGather4 v64") != std::string::npos),
@@ -6261,6 +6269,10 @@ void TestNewShaderRecompilerImageGatherVariants() {
"IMAGE_GATHER4_C_O did not lower to shared IR ImageGather4");
Check((result.ir_dump.find("ImageGather4 v80") != std::string::npos),
"IMAGE_GATHER4_C_LZ_O did not lower to shared IR ImageGather4");
Check((result.ir_dump.find("ImageGather4 v84") != std::string::npos),
"IMAGE_GATHER4_C_A did not lower to shared IR ImageGather4");
Check((result.ir_dump.find("ImageGather4 v88") != std::string::npos),
"IMAGE_GATHER4_C_O_A did not lower to shared IR ImageGather4");
Check((result.ir_dump.find("data_dwords=4") != std::string::npos),
"IMAGE_GATHER4 did not preserve four-component result metadata");
Check((result.ir_dump.find("image_flags=0x30") != std::string::npos),
@@ -6273,6 +6285,10 @@ void TestNewShaderRecompilerImageGatherVariants() {
"IMAGE_GATHER4_C_O flags did not survive into IR memory metadata");
Check((result.ir_dump.find("image_flags=0x38") != std::string::npos),
"IMAGE_GATHER4_C_LZ_O flags did not survive into IR memory metadata");
Check((result.ir_dump.find("image_flags=0x408") != std::string::npos),
"IMAGE_GATHER4_C_A flags did not survive into IR memory metadata");
Check((result.ir_dump.find("image_flags=0x418") != std::string::npos),
"IMAGE_GATHER4_C_O_A flags did not survive into IR memory metadata");
Check(SpirvContainsCapability(result.spirv, 25),
"SPIR-V binary does not request ImageGatherExtended");
Check(SpirvContainsOpcode(result.spirv, 96),
@@ -11740,7 +11756,7 @@ void TestNewShaderRecompilerSetpcJumpTable() {
Check((*jump)->terminator.cases.size() == 2,
"S_SETPC_B64 jump table did not retain selector mapping");
Check(
(result.ir_dump.find("SLoadDword") == std::string::npos),
(result.ir_dump.find("LoadAddressU32") == std::string::npos),
"S_SETPC_B64 jump table load was reflected as a raw scalar buffer load");
Check(SpirvContainsOpcode(result.spirv, 251),
"dispatcher SPIR-V lacks OpSwitch");
@@ -11801,7 +11817,9 @@ void TestNewShaderRecompilerPrunesUnreachableSetpcMetadata() {
"unreachable S_SETPC metadata suppressed a reachable scalar load component");
}
void TestNewShaderRecompilerSetpcDwordJumpTable() {
// offset_sgpr 106 aliases the vcc_lo selector, like PPSA34015's
// s_load_dword s17, s[22:23], s17.
void TestNewShaderRecompilerSetpcDwordJumpTable(uint32_t offset_sgpr) {
const uint32_t shader[] = {
EncodeSopp(0x02, 1), // skip one unreachable block
EncodeSMovB32(100, 129), // unreachable
@@ -11811,20 +11829,20 @@ void TestNewShaderRecompilerSetpcDwordJumpTable() {
EncodeSop2(0x00, 4, 4, 255), // s_add_u32 s4, s4, literal
0x00000034u,
EncodeSop2(0x04, 5, 5, 128), // s_addc_u32 s5, s5, 0
EncodeSmem0(0x00, 6, 2),
(106u << 25u) | 4u, // s_load_dword s6, s[4:5], vcc_lo offset:4
EncodeSopp(0x0c, 0), // s_waitcnt 0
EncodeSop2(0x01, 4, 4, 6), // s_sub_u32 s4, s4, s6
EncodeSop2(0x05, 5, 5, 128), // s_subb_u32 s5, s5, 0
EncodeSop1(0x20, 0, 4), // s_setpc_b64 s[4:5]
EncodeSMovB32(1, 129), // case 0
EncodeSmem0(0x00, offset_sgpr, 2),
(106u << 25u) | 4u, // s_load_dword offset, s[4:5], vcc_lo offset:4
EncodeSopp(0x0c, 0), // s_waitcnt 0
EncodeSop2(0x01, 4, 4, offset_sgpr), // s_sub_u32 s4, s4, offset
EncodeSop2(0x05, 5, 5, 128), // s_subb_u32 s5, s5, 0
EncodeSop1(0x20, 0, 4), // s_setpc_b64 s[4:5]
EncodeSMovB32(1, 129), // case 0
EncodeSopp(0x02, 1),
EncodeSMovB32(2, 129), // case 1
EncodeSopp(0x01, 0),
0u,
0x00000010u,
0x00000008u,
0x00000010u, // table at pc 0x44, targets relative backward from pc 0x40
0x00000010u, // table at pc 0x4c, targets relative backward from pc 0x48
};
auto options = MakeCompileOptions(ShaderType::Compute);
@@ -11850,7 +11868,7 @@ void TestNewShaderRecompilerSetpcDwordJumpTable() {
result.program.blocks,
[](const auto *block) { return block->start_pc == 0x04u; }),
"subtractive S_SETPC_B64 table targets or selector mapping changed");
Check((result.ir_dump.find("SLoadDword") == std::string::npos),
Check((result.ir_dump.find("LoadAddressU32") == std::string::npos),
"subtractive S_SETPC_B64 table load reached normal IR");
Check(SpirvContainsOpcode(result.spirv, 251),
"subtractive S_SETPC_B64 dispatcher lacks OpSwitch");
@@ -14343,7 +14361,7 @@ void BuildTypedPlan(const uint32_t *code, uint32_t words,
ShaderRecompiler::IR::RemoveIdentities(ir);
ShaderRecompiler::IR::EliminateDeadCode(ir.blocks);
}
ShaderRecompiler::IR::TrackResources(ir, decoded, cfg);
ShaderRecompiler::IR::TrackResources(ir, decoded, cfg, options.input_info);
ShaderRecompiler::IR::EliminateDeadCode(ir.blocks);
}
@@ -16182,10 +16200,6 @@ void TestCompilerStageInputOwnership() {
CheckSpirvBinaryValidates(compiled.spirv);
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
options.stage = ShaderType::Vertex;
options.input_info.vertex = nullptr;
ExpectFatal([&] { (void)TranslateProgram(shader, options); },
"missing stage metadata bypassed translation validation");
const uint32_t auxiliary_position[] = {
EncodeExp0(0x0d, 0x1), EncodeExp1(0, 0, 0, 0), EncodeSopp(0x01)};
for (const auto stage : {ShaderType::Pixel, ShaderType::Compute}) {
@@ -16768,7 +16782,8 @@ int main() {
TestEmbeddedVertexFormatSwizzle();
TestNewShaderRecompilerSetpcJumpTable();
TestNewShaderRecompilerPrunesUnreachableSetpcMetadata();
TestNewShaderRecompilerSetpcDwordJumpTable();
TestNewShaderRecompilerSetpcDwordJumpTable(6);
TestNewShaderRecompilerSetpcDwordJumpTable(106);
TestTypedEntryStateIsMinimal();
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
TestFinalSsaRejectsRegisterStatePseudos();