mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-21 19:36:55 +02:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b7f9a70c6 | |||
| 6ad393775f | |||
| f492027be2 | |||
| 8843291506 | |||
| 63ca9c0b80 | |||
| 0caf4a6aa0 | |||
| c715891242 | |||
| f3e01aaf1e | |||
| 0957f088ad | |||
| 4a2961cdf9 | |||
| f0e462a8a2 | |||
| feed075a9f | |||
| 18e6747dbf | |||
| 6b6274fa5a | |||
| 1a053f9e13 | |||
| 12ca1599cf | |||
| f186b35706 | |||
| 992a83003d | |||
| fa8216c4d9 | |||
| 742fe17173 | |||
| 0cad4672e9 |
Vendored
+17
@@ -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
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user