mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-28 16:23:05 +00:00
Compare commits
68
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80932f8da1 | ||
|
|
f0e55803ba | ||
|
|
ca6a7b88ce | ||
|
|
13b291f998 | ||
|
|
2944d126b3 | ||
|
|
7b7294aa03 | ||
|
|
4644c52927 | ||
|
|
4bb4be50b9 | ||
|
|
be9e13915d | ||
|
|
68d0ba5a81 | ||
|
|
70668fbd98 | ||
|
|
ea1b032c59 | ||
|
|
3c4124dafa | ||
|
|
8cc5f5364a | ||
|
|
1ce663b5a9 | ||
|
|
66f488a17f | ||
|
|
7b7f9a70c6 | ||
|
|
6ad393775f | ||
|
|
f492027be2 | ||
|
|
8843291506 | ||
|
|
63ca9c0b80 | ||
|
|
0caf4a6aa0 | ||
|
|
c715891242 | ||
|
|
f3e01aaf1e | ||
|
|
0957f088ad | ||
|
|
4a2961cdf9 | ||
|
|
f0e462a8a2 | ||
|
|
feed075a9f | ||
|
|
18e6747dbf | ||
|
|
6b6274fa5a | ||
|
|
1a053f9e13 | ||
|
|
12ca1599cf | ||
|
|
f186b35706 | ||
|
|
992a83003d | ||
|
|
fa8216c4d9 | ||
|
|
742fe17173 | ||
|
|
0cad4672e9 | ||
|
|
cd4eab9d65 | ||
|
|
fde0b98e69 | ||
|
|
d4465faa79 | ||
|
|
e5f0ee7fc2 | ||
|
|
5f29374d5b | ||
|
|
2c730e58ea | ||
|
|
e87fb8c859 | ||
|
|
440245f616 | ||
|
|
05d25ef28c | ||
|
|
cad6aafdc5 | ||
|
|
defb2f11d3 | ||
|
|
75d7a4311d | ||
|
|
bb505a3ae8 | ||
|
|
ab8566591f | ||
|
|
b2d3ce6dfc | ||
|
|
09c993dfe0 | ||
|
|
de81300b28 | ||
|
|
ac3711ce7c | ||
|
|
8b51370ca1 | ||
|
|
e9bcc77e0b | ||
|
|
8639e0de81 | ||
|
|
7675579e79 | ||
|
|
831862a50b | ||
|
|
099de8dea6 | ||
|
|
f4bdb52922 | ||
|
|
22ef16a044 | ||
|
|
37df7f3278 | ||
|
|
33208a545a | ||
|
|
4059179e9d | ||
|
|
fe3857a171 | ||
|
|
c3e7ec8285 |
No files matched your search
@@ -6,3 +6,4 @@ _Build/vscode-clang/
|
||||
_Build/
|
||||
cmake-build-debug/
|
||||
_PipelineCache/
|
||||
CMakeUserPresets.json
|
||||
Vendored
+1
-1
@@ -27,7 +27,7 @@
|
||||
"args": [
|
||||
"--game", "D:/gamesps5/PPSA07429",
|
||||
"--printf-direction", "File",
|
||||
"--graphics-debug-dump", "True"
|
||||
"--graphics-debug-dump"
|
||||
],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}\\_Build\\vscode-clang",
|
||||
|
||||
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
|
||||
|
||||
@@ -189,10 +189,6 @@ bool SyncRawImageBuffersEnabled() {
|
||||
return g_config->sync_raw_image_buffers;
|
||||
}
|
||||
|
||||
bool SupportZmaskEnabled() {
|
||||
return g_config->support_zmask;
|
||||
}
|
||||
|
||||
bool TessellationEnabled() {
|
||||
return g_config->tessellation_enabled;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,6 @@ struct ConfigOptions {
|
||||
bool renderdoc_enabled = false;
|
||||
bool readback_linear_images = false;
|
||||
bool sync_raw_image_buffers = false;
|
||||
bool support_zmask = false;
|
||||
bool tessellation_enabled = false;
|
||||
bool trophy_enabled = true;
|
||||
uint32_t trophy_duration_seconds = 6;
|
||||
@@ -152,7 +151,6 @@ bool GpuAssistedValidationEnabled();
|
||||
bool RenderDocEnabled();
|
||||
bool ReadbackLinearImagesEnabled();
|
||||
bool SyncRawImageBuffersEnabled();
|
||||
bool SupportZmaskEnabled();
|
||||
bool TessellationEnabled();
|
||||
bool TrophyEnabled();
|
||||
uint32_t GetTrophyDurationSeconds();
|
||||
|
||||
@@ -51,20 +51,26 @@ inline constexpr auto BrightWhite = fmt::fg(fmt::terminal_color::bright_white)
|
||||
|
||||
} // namespace Color
|
||||
|
||||
// Out of line, so callers on small guest stacks (fibers) do not reserve the formatting frame.
|
||||
template <typename... Args>
|
||||
[[gnu::noinline]] void Printf(fmt::text_style style, const char* format, const Args&... args) {
|
||||
Write(style, fmt::sprintf(format, args...));
|
||||
}
|
||||
|
||||
} // namespace Log
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define LOGF(...) \
|
||||
do { \
|
||||
if (!::Log::IsSilent()) { \
|
||||
::Log::Write(::fmt::sprintf(__VA_ARGS__)); \
|
||||
::Log::Printf({}, __VA_ARGS__); \
|
||||
} \
|
||||
} while (false)
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define LOGF_COLOR(style, ...) \
|
||||
do { \
|
||||
if (!::Log::IsSilent()) { \
|
||||
::Log::Write((style), ::fmt::sprintf(__VA_ARGS__)); \
|
||||
::Log::Printf((style), __VA_ARGS__); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
|
||||
@@ -367,7 +367,7 @@ bool SysFileDeleteFile(const std::filesystem::path& name) {
|
||||
|
||||
bool SysFileFlush(sys_file_t& f) {
|
||||
if (f.type == SYS_FILE_FILE && f.f != nullptr) {
|
||||
return (fflush(f.f) == 0);
|
||||
return fflush(f.f) == 0 && fsync(fileno(f.f)) == 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -89,42 +89,6 @@ static void MountSandboxDirs() {
|
||||
MountOrCreateDir("_TempData/" + title_id, "/temp");
|
||||
}
|
||||
|
||||
static bool ClearDirectoryContents(const std::filesystem::path& dir) {
|
||||
bool ok = true;
|
||||
|
||||
for (const auto& entry: Common::File::GetDirEntries(dir)) {
|
||||
if (entry.name == "." || entry.name == "..") {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto path = dir / entry.name;
|
||||
|
||||
if (entry.is_file) {
|
||||
Common::File::RemoveReadonly(path);
|
||||
ok = Common::File::DeleteFile(path) && ok;
|
||||
} else {
|
||||
ok = ClearDirectoryContents(path) && ok;
|
||||
ok = Common::File::DeleteDirectory(path) && ok;
|
||||
}
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void ClearDebugTextureFolder() {
|
||||
const std::string debug_texture_folder = "_Textures";
|
||||
|
||||
if (!Common::File::IsDirectoryExisting(debug_texture_folder)) {
|
||||
Common::File::CreateDirectories(debug_texture_folder);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ClearDirectoryContents(debug_texture_folder)) {
|
||||
LOGF_COLOR(Log::Color::BrightYellow, "TextureDump: failed to completely clear %s\n",
|
||||
debug_texture_folder.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static void Init(const Config::ConfigOptions& cfg, const std::filesystem::path& param_json,
|
||||
Common::Subsystems& subsystems) {
|
||||
EXIT_IF(!Common::Thread::IsMainThread());
|
||||
@@ -198,8 +162,6 @@ void Run(const RunOptions& options) {
|
||||
Common::Subsystems subsystems(true);
|
||||
Init(options.config, param_json, subsystems);
|
||||
|
||||
ClearDebugTextureFolder();
|
||||
|
||||
PrintSystemInfo();
|
||||
std::string title_id;
|
||||
Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id);
|
||||
|
||||
@@ -104,7 +104,6 @@ public:
|
||||
uint32_t value);
|
||||
void PrepareCpuFlip(uint64_t request_id);
|
||||
void SynchronizeGpu();
|
||||
void EmitGlobalBarrier();
|
||||
void TriggerEopEventAtEndOfPipe(uint32_t interrupt_context_id);
|
||||
void DispatchDirect(uint32_t thread_group_x, uint32_t thread_group_y, uint32_t thread_group_z,
|
||||
uint32_t mode);
|
||||
|
||||
@@ -73,7 +73,6 @@ constexpr auto MakeContextDispatchTable() {
|
||||
g_hw_ctx_func[Pm4::PA_SC_BINNER_CNTL_0] = HwCtxSetPaScExtendedControl;
|
||||
g_hw_ctx_func[Pm4::PA_SC_BINNER_CNTL_1] = HwCtxSetPaScExtendedControl;
|
||||
g_hw_ctx_func[Pm4::PA_SC_CONSERVATIVE_RASTERIZATION_CNTL] = HwCtxSetPaScExtendedControl;
|
||||
g_hw_ctx_func[Pm4::DB_ALPHA_TO_MASK] = HwCtxSetAlphaToMask;
|
||||
g_hw_ctx_func[Pm4::VGT_DRAW_PAYLOAD_CNTL] = HwCtxSetDrawPayloadControl;
|
||||
g_hw_ctx_func[Pm4::VGT_PRIMITIVEID_RESET] = HwCtxSetPrimitiveIdReset;
|
||||
g_hw_ctx_func[Pm4::PA_CL_OBJPRIM_ID_CNTL] = HwCtxSetObjprimIdControl;
|
||||
|
||||
@@ -79,7 +79,6 @@ uint32_t HwCtxSetAaMask(CommandProcessor&, uint32_t, uint32_t, const uint32_t*,
|
||||
uint32_t HwCtxSetVtxControl(CommandProcessor&, uint32_t, uint32_t, const uint32_t*, uint32_t);
|
||||
uint32_t HwCtxSetPaScExtendedControl(CommandProcessor&, uint32_t, uint32_t, const uint32_t*,
|
||||
uint32_t);
|
||||
uint32_t HwCtxSetAlphaToMask(CommandProcessor&, uint32_t, uint32_t, const uint32_t*, uint32_t);
|
||||
uint32_t HwCtxSetDrawPayloadControl(CommandProcessor&, uint32_t, uint32_t, const uint32_t*,
|
||||
uint32_t);
|
||||
uint32_t HwCtxSetPrimitiveIdReset(CommandProcessor&, uint32_t, uint32_t, const uint32_t*, uint32_t);
|
||||
|
||||
@@ -71,12 +71,6 @@ constexpr uint32_t NormalizeRegisterOffset(uint32_t raw_offset) {
|
||||
return raw_offset & ~RegisterSelectorMask;
|
||||
}
|
||||
|
||||
bool ReleaseMemGcrNeedsBarrier(uint32_t eop_event_type, uint32_t gcr_cntl) {
|
||||
return eop_event_type != 0x28u ||
|
||||
(gcr_cntl & (GcrGl2MetadataInvalidate | GcrGl0VectorInvalidate | GcrGl1Invalidate |
|
||||
GcrGl2Invalidate | GcrGl2Writeback)) != 0;
|
||||
}
|
||||
|
||||
uint32_t ReleaseMemCacheActionFromGcr(uint32_t gcr_cntl) {
|
||||
return ((gcr_cntl & GcrGl2Writeback) != 0 ? 0x38u : 0x00u);
|
||||
}
|
||||
@@ -289,12 +283,6 @@ KYTY_HW_CTX_PARSER(HwCtxSetCentroidPriority) {
|
||||
static void HwCtxIgnoreAaMaskRegister([[maybe_unused]] uint32_t cmd_offset,
|
||||
[[maybe_unused]] uint32_t value) {}
|
||||
|
||||
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) {}
|
||||
@@ -439,16 +427,6 @@ KYTY_HW_CTX_PARSER(HwCtxSetPointState) {
|
||||
return num_values;
|
||||
}
|
||||
|
||||
KYTY_HW_CTX_PARSER(HwCtxSetAlphaToMask) {
|
||||
auto num_values = KYTY_PM4_LEN(cmd_id) - 2u;
|
||||
|
||||
for (uint32_t i = 0; i < num_values; i++) {
|
||||
HwCtxIgnoreAlphaToMaskRegister(buffer[i]);
|
||||
}
|
||||
|
||||
return num_values;
|
||||
}
|
||||
|
||||
KYTY_HW_CTX_PARSER(HwCtxSetDrawPayloadControl) {
|
||||
cp.GetCtx().SetDrawPayloadControl(buffer[0]);
|
||||
return KYTY_PM4_LEN(cmd_id) - 2u;
|
||||
@@ -2263,30 +2241,14 @@ KYTY_CP_OP_PARSER(CpOpReleaseMem) {
|
||||
}
|
||||
};
|
||||
|
||||
if (data_sel == 0 || interrupt_selector == 4) {
|
||||
if (eop_event_type != 0x28 || gcr_cntl != 0) {
|
||||
cp.EmitGlobalBarrier();
|
||||
}
|
||||
|
||||
// Cache actions only order GPU work, which the renderer's resource barriers already order.
|
||||
if (data_sel == 0 || interrupt_selector == 4 ||
|
||||
(release_dst == ReleaseMemDstMemory && dst_gpu_addr == nullptr)) {
|
||||
trigger_interrupt();
|
||||
|
||||
return 7;
|
||||
}
|
||||
|
||||
if (release_dst == ReleaseMemDstMemory && dst_gpu_addr == nullptr) {
|
||||
if (eop_event_type != 0x28 || gcr_cntl != 0) {
|
||||
cp.EmitGlobalBarrier();
|
||||
}
|
||||
|
||||
trigger_interrupt();
|
||||
|
||||
return 7;
|
||||
}
|
||||
|
||||
if (ReleaseMemGcrNeedsBarrier(eop_event_type, gcr_cntl)) {
|
||||
cp.EmitGlobalBarrier();
|
||||
}
|
||||
|
||||
auto cache_action = ReleaseMemCacheActionFromGcr(gcr_cntl);
|
||||
cache_policy = 0;
|
||||
|
||||
@@ -2899,7 +2861,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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3491,7 +3453,8 @@ void GraphicsInitJmpTablesCxIndirect() {
|
||||
g_hw_ctx_indirect_func[Pm4::PA_SC_CONSERVATIVE_RASTERIZATION_CNTL] =
|
||||
[](KYTY_HW_CTX_INDIRECT_ARGS) { HwCtxIgnorePaScExtendedControl(cmd_offset, value); };
|
||||
g_hw_ctx_indirect_func[Pm4::DB_ALPHA_TO_MASK] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
|
||||
HwCtxIgnoreAlphaToMaskRegister(value);
|
||||
cp.GetCtx().SetAlphaToMaskEnable(
|
||||
KYTY_PM4_GET(value, DB_ALPHA_TO_MASK, ALPHA_TO_MASK_ENABLE) != 0);
|
||||
};
|
||||
g_hw_ctx_indirect_func[Pm4::CB_COLOR_CONTROL] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
|
||||
cp.GetCtx().SetColorControl(DecodeColorControl(value));
|
||||
|
||||
@@ -343,12 +343,11 @@ void CommandProcessor::WriteData(uint32_t* dst, const uint32_t* src, uint32_t dw
|
||||
return;
|
||||
}
|
||||
|
||||
if (write_one_address) {
|
||||
for (uint32_t i = 0; i < dw_num; i++) {
|
||||
dst[0] = src[i];
|
||||
}
|
||||
} else {
|
||||
memcpy(dst, src, static_cast<size_t>(dw_num) * sizeof(uint32_t));
|
||||
// Completion labels can be reused immediately; memcpy may write a destination twice.
|
||||
auto* output = static_cast<volatile uint32_t*>(dst);
|
||||
for (uint32_t i = 0; i < dw_num; i++) {
|
||||
*output = src[i];
|
||||
output += !write_one_address;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,7 +607,6 @@ bool GuestGpu::Process(Submission& submission) {
|
||||
case SubmissionType::SuspendPoint:
|
||||
// Age caches at the frame boundary, not once per command list.
|
||||
m_renderer.RunGarbageCollector();
|
||||
cp.EmitGlobalBarrier();
|
||||
m_renderer.GetCommandScheduler().DeferPriorityOperation(
|
||||
[ready = m_suspend_point_ready] { ready->release(); });
|
||||
cp.BufferFlush();
|
||||
@@ -1223,22 +1221,6 @@ void CommandProcessor::WriteAtEndOfPipe64(uint32_t cache_policy, uint32_t event_
|
||||
interrupt_context_id);
|
||||
}
|
||||
|
||||
void CommandProcessor::EmitGlobalBarrier() {
|
||||
Common::LockGuard lock(m_renderer.GetMutex());
|
||||
|
||||
vk::MemoryBarrier2 barrier {};
|
||||
barrier.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands;
|
||||
barrier.srcAccessMask = vk::AccessFlagBits2::eMemoryWrite;
|
||||
barrier.dstStageMask = vk::PipelineStageFlagBits2::eAllCommands;
|
||||
barrier.dstAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite;
|
||||
|
||||
vk::DependencyInfo dependency {};
|
||||
dependency.memoryBarrierCount = 1;
|
||||
dependency.pMemoryBarriers = &barrier;
|
||||
GetScheduler().EndRendering();
|
||||
CurrentBuffer().Handle().pipelineBarrier2(dependency);
|
||||
}
|
||||
|
||||
void CommandProcessor::TriggerEopEventAtEndOfPipe(uint32_t interrupt_context_id) {
|
||||
(void)CurrentBuffer().Handle();
|
||||
GetScheduler().DeferPriorityOperation(
|
||||
@@ -1258,21 +1240,17 @@ void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
|
||||
}
|
||||
|
||||
const auto valid_cache_event_index = event_index == 0x00000000 || event_index == 0x00000007;
|
||||
// Partial flushes and cache writebacks only order GPU work, which the renderer's resource
|
||||
// barriers already order.
|
||||
switch (event_type) {
|
||||
// CsPartialFlush, GsPartialFlush, PsPartialFlush.
|
||||
case 0x00000007:
|
||||
case 0x0000000f:
|
||||
case 0x00000010: EmitGlobalBarrier(); break;
|
||||
// CbDbDataWritebackInvalidate, CbDataWritebackInvalidate.
|
||||
case 0x00000010: break;
|
||||
// CbDbDataWritebackInvalidate, CbDataWritebackInvalidate, DbDataWritebackInvalidate,
|
||||
// DbMetadataWritebackInvalidate, CbMetadataWritebackInvalidate.
|
||||
case 0x00000016:
|
||||
case 0x00000031:
|
||||
if (!valid_cache_event_index) {
|
||||
EXIT("unknown event type: 0x%08" PRIx32 ", 0x%08" PRIx32 "\n", event_type,
|
||||
event_index);
|
||||
}
|
||||
EmitGlobalBarrier();
|
||||
break;
|
||||
// DbDataWritebackInvalidate, DbMetadataWritebackInvalidate, CbMetadataWritebackInvalidate.
|
||||
case 0x0000002a:
|
||||
case 0x0000002c:
|
||||
case 0x0000002e:
|
||||
@@ -1280,7 +1258,6 @@ void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
|
||||
EXIT("unknown event type: 0x%08" PRIx32 ", 0x%08" PRIx32 "\n", event_type,
|
||||
event_index);
|
||||
}
|
||||
EmitGlobalBarrier();
|
||||
break;
|
||||
case 0x0000000d:
|
||||
case 0x0000000e:
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "common/common.h"
|
||||
#include "graphics/guest_gpu/gpu_defs.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace Libs::Graphics::HW {
|
||||
|
||||
struct ColorBase {
|
||||
@@ -574,6 +576,12 @@ struct ShaderRegisters {
|
||||
uint32_t m_paScShaderControl = 0;
|
||||
|
||||
DepthShaderControl db_shader_control;
|
||||
bool alpha_to_mask_enable = false;
|
||||
|
||||
// DB_SHADER_CONTROL.ALPHA_TO_MASK_DISABLE overrides DB_ALPHA_TO_MASK per shader.
|
||||
[[nodiscard]] bool AlphaToMask() const {
|
||||
return alpha_to_mask_enable && !db_shader_control.alpha_to_mask_disable;
|
||||
}
|
||||
|
||||
[[nodiscard]] uint32_t GetExportCount() const {
|
||||
return 1u + ((m_spiVsOutConfig >> 1u) & 0x1Fu);
|
||||
@@ -824,6 +832,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 {
|
||||
@@ -908,6 +921,7 @@ public:
|
||||
void SetDepthShaderControl(const DepthShaderControl& value) {
|
||||
m_sh_regs.db_shader_control = value;
|
||||
}
|
||||
void SetAlphaToMaskEnable(bool value) { m_sh_regs.alpha_to_mask_enable = value; }
|
||||
|
||||
void SetScShaderControl(uint32_t value) { m_sh_regs.m_paScShaderControl = value; }
|
||||
|
||||
@@ -924,6 +938,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;
|
||||
|
||||
|
||||
@@ -549,6 +549,8 @@ constexpr uint32_t VGT_SHADER_STAGES_EN = 0x2D5;
|
||||
constexpr uint32_t VGT_LS_HS_CONFIG = 0x2D6;
|
||||
constexpr uint32_t VGT_TF_PARAM = 0x2DB;
|
||||
constexpr uint32_t DB_ALPHA_TO_MASK = 0x2DC;
|
||||
constexpr uint32_t DB_ALPHA_TO_MASK_ALPHA_TO_MASK_ENABLE_SHIFT = 0;
|
||||
constexpr uint32_t DB_ALPHA_TO_MASK_ALPHA_TO_MASK_ENABLE_MASK = 0x1;
|
||||
constexpr uint32_t PA_SU_POLY_OFFSET_DB_FMT_CNTL = 0x2DE;
|
||||
constexpr uint32_t PA_SU_POLY_OFFSET_DB_FMT_CNTL_NEG_NUM_DB_BITS_SHIFT = 0;
|
||||
constexpr uint32_t PA_SU_POLY_OFFSET_DB_FMT_CNTL_NEG_NUM_DB_BITS_MASK = 0xFF;
|
||||
|
||||
@@ -256,6 +256,10 @@ BufferCache::BufferCache(GraphicContext& graphics, CommandScheduler& scheduler,
|
||||
m_gds_buffer.Flush(0, m_gds_buffer.Size());
|
||||
SetVulkanObjectNameF(m_graphics.device, m_bda_pagetable_buffer.Handle(),
|
||||
"BDA Page Table Buffer");
|
||||
m_scheduler.BeginCommand();
|
||||
for (auto* buffer: {&m_bda_pagetable_buffer, m_fault_manager.GetFaultBuffer()}) {
|
||||
buffer->Fill(0, buffer->Size(), 0);
|
||||
}
|
||||
const auto null_id =
|
||||
m_slot_buffers.insert(m_graphics, m_scheduler, MemoryUsage::DeviceLocal, 0, AllFlags, 16);
|
||||
EXIT_IF(null_id != NULL_BUFFER_ID);
|
||||
|
||||
+6
-7
@@ -263,6 +263,9 @@ void TextureCache::DeleteImage(ImageId id) {
|
||||
EXIT("TextureCache: deleting a GPU-modified image without resolving its contents\n");
|
||||
}
|
||||
m_download_images.erase(id);
|
||||
if (image->info.metadata.kind == ImageMetadataKind::Htile) {
|
||||
m_surface_metas.erase(image->info.metadata.range.address);
|
||||
}
|
||||
UnregisterImage(id);
|
||||
if (m_scheduler.Active()) {
|
||||
m_scheduler.DeferOperation([this, id] { m_slot_images.erase(id); });
|
||||
@@ -1033,7 +1036,7 @@ void TextureCache::MaterializeColorClear(ImageId id, const ImageDesc& desc,
|
||||
std::scoped_lock lock {m_lock};
|
||||
auto& image = m_slot_images[id];
|
||||
image.info.metadata = desc.info.metadata;
|
||||
// Native color metadata must not retain a reused HTile/CMask/FMask clear flag.
|
||||
// Native color metadata must not retain a reused HTile clear flag.
|
||||
m_surface_metas.erase(range.address);
|
||||
if (range.size == 0 || desc.info.resources.levels != 1) {
|
||||
return;
|
||||
@@ -1411,11 +1414,8 @@ vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) {
|
||||
image.info.metadata = desc.info.metadata;
|
||||
if (desc.info.HasMetadata()) {
|
||||
const auto [metadata, inserted] = m_surface_metas.try_emplace(desc.info.metadata.range.address);
|
||||
if (inserted) {
|
||||
metadata->second.type = MetaDataInfo::Type::HTile;
|
||||
if (image.info.htile_initial_clear) {
|
||||
metadata->second.clear_mask.set();
|
||||
}
|
||||
if (inserted && image.info.htile_initial_clear) {
|
||||
metadata->second.clear_mask.set();
|
||||
}
|
||||
}
|
||||
RefreshImage(id);
|
||||
@@ -1876,7 +1876,6 @@ void TextureCache::UnmapMemory(uint64_t address, uint64_t size) {
|
||||
EXIT("TextureCache: invalid unmap range\n");
|
||||
}
|
||||
std::scoped_lock lock {m_lock};
|
||||
// HTILE belongs to guest memory, independently of native image replacements.
|
||||
for (auto metadata = m_surface_metas.begin(); metadata != m_surface_metas.end();) {
|
||||
const auto base = metadata->first;
|
||||
if (base >= address && base < address + size) {
|
||||
|
||||
@@ -81,9 +81,6 @@ public:
|
||||
|
||||
private:
|
||||
struct MetaDataInfo {
|
||||
enum class Type : uint8_t { CMask, FMask, HTile };
|
||||
|
||||
Type type;
|
||||
// PS5 DB_DEPTH_VIEW encodes 13-bit base and last array-slice indices.
|
||||
std::bitset<8192> clear_mask;
|
||||
};
|
||||
|
||||
@@ -72,7 +72,16 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
|
||||
return;
|
||||
}
|
||||
const auto samples = render_sample_count(rt.attrib.num_fragments);
|
||||
if (samples == 0 || rt.attrib.num_samples != rt.attrib.num_fragments) {
|
||||
// TODO(PPSA24156): Replace this 2x MSAA approximation with proper 4-coverage/2-fragment EQAA.
|
||||
const bool approximate_eqaa = rt.attrib.num_samples == 2 && rt.attrib.num_fragments == 1;
|
||||
if (approximate_eqaa) {
|
||||
static std::atomic_bool logged = false;
|
||||
if (!logged.exchange(true, std::memory_order_relaxed)) {
|
||||
Log::WriteToConsoleAndLog(
|
||||
"Warning: approximating 4-coverage/2-fragment EQAA with 2x MSAA.\n");
|
||||
}
|
||||
}
|
||||
if (samples == 0 || (rt.attrib.num_samples != rt.attrib.num_fragments && !approximate_eqaa)) {
|
||||
EXIT("unsupported render-target sample configuration: samples=%u fragments=%u\n",
|
||||
rt.attrib.num_samples, rt.attrib.num_fragments);
|
||||
}
|
||||
@@ -220,8 +229,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
|
||||
}
|
||||
const bool has_dcc = rt.info.dcc_compression_enable && rt.dcc_addr.addr != 0;
|
||||
const bool has_cmask = !rt.info.dcc_compression_enable && rt.info.cmask_fast_clear_enable &&
|
||||
rt.cmask.addr != 0 && samples == 1 &&
|
||||
!rt.info.fmask_compression_enable &&
|
||||
rt.cmask.addr != 0 &&
|
||||
!rt.attrib3.write_vrs_rate_hint_to_cmask;
|
||||
if (has_dcc || has_cmask) {
|
||||
TileSizeAlign metadata_size {};
|
||||
@@ -233,7 +241,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
|
||||
} else {
|
||||
(void)TileGetCmaskSize(width, height, layers, levels, metadata_size);
|
||||
}
|
||||
// DCC owns the clear when both planes are enabled; single-sample CMASK stays expanded.
|
||||
// DCC owns the clear when both planes are enabled.
|
||||
desc.info.metadata.kind = has_dcc ? ImageMetadataKind::Dcc : ImageMetadataKind::Cmask;
|
||||
desc.info.metadata.range = {has_dcc ? rt.dcc_addr.addr : rt.cmask.addr, metadata_size.size};
|
||||
desc.info.metadata.clear_word =
|
||||
|
||||
@@ -327,7 +327,7 @@ void CommandScheduler::CheckActive() const {
|
||||
}
|
||||
|
||||
CommandBuffer& CommandScheduler::Current() {
|
||||
CheckActive();
|
||||
EXIT_IF(m_command.IsInvalid());
|
||||
return m_command;
|
||||
}
|
||||
|
||||
|
||||
@@ -104,8 +104,14 @@ void CommandBuffer::BeginRendering(const RenderState& state) const {
|
||||
rendering.pDepthAttachment = depth_stencil.has_depth ? &depth : nullptr;
|
||||
rendering.pStencilAttachment = depth_stencil.has_stencil ? &stencil : nullptr;
|
||||
Handle().beginRendering(rendering);
|
||||
// The open pass holds the cleared contents, so a later clear begins a new pass.
|
||||
m_render_state = state;
|
||||
m_rendering = true;
|
||||
for (auto& color: m_render_state.color_attachments) {
|
||||
color.is_clear = false;
|
||||
}
|
||||
m_render_state.depth_stencil_attachment.depth_clear = false;
|
||||
m_render_state.depth_stencil_attachment.stencil_clear = false;
|
||||
m_rendering = true;
|
||||
}
|
||||
|
||||
void CommandBuffer::EndRendering() const {
|
||||
|
||||
@@ -294,16 +294,6 @@ static void RtCheck(const HW::RenderTarget& rt) {
|
||||
EXIT_NOT_IMPLEMENTED(rt.dcc.overwrite_combiner_disable != false);
|
||||
// EXIT_NOT_IMPLEMENTED(rt.dcc.data_write_on_dcc_clear_to_reg != false);
|
||||
EXIT_NOT_IMPLEMENTED(rt.dcc.dcc_clear_key_enable != false);
|
||||
if (rt.cmask.addr != 0x0000000000000000 || rt.fmask.addr != 0x0000000000000000 ||
|
||||
rt.dcc_addr.addr != 0x0000000000000000) {
|
||||
static bool logged = false;
|
||||
if (!logged) {
|
||||
LOGF("RenderTarget: temporary: ignoring PS5 metadata addresses cmask=0x%016" PRIx64
|
||||
" fmask=0x%016" PRIx64 " dcc=0x%016" PRIx64 "\n",
|
||||
rt.cmask.addr, rt.fmask.addr, rt.dcc_addr.addr);
|
||||
logged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,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) {
|
||||
|
||||
@@ -353,8 +353,12 @@ void Image::CopyImage(Image& source) {
|
||||
std::vector<vk::ImageCopy> copies;
|
||||
copies.reserve(levels);
|
||||
for (uint32_t level = 0; level < levels; level++) {
|
||||
const auto width = std::max(source.backing.extent.width >> level, 1u);
|
||||
const auto height = std::max(source.backing.extent.height >> level, 1u);
|
||||
// Overlapping cache images can differ in size; copying the source extent into a smaller
|
||||
// destination writes out of bounds and loses the device.
|
||||
const auto width = std::min(std::max(source.backing.extent.width >> level, 1u),
|
||||
std::max(backing.extent.width >> level, 1u));
|
||||
const auto height = std::min(std::max(source.backing.extent.height >> level, 1u),
|
||||
std::max(backing.extent.height >> level, 1u));
|
||||
const auto depth = std::max(base_depth >> level, 1u);
|
||||
const auto [source_layers, destination_layers] = SanitizeCopyLayers(source, *this, depth);
|
||||
vk::ImageCopy copy {};
|
||||
|
||||
@@ -126,13 +126,9 @@ NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource
|
||||
if (size > graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange) {
|
||||
EXIT("storage buffer range is unsupported\n");
|
||||
}
|
||||
const bool metadata_read = resource.read && Config::SupportZmaskEnabled() &&
|
||||
(address & 0x7fffu) == 0 &&
|
||||
context.GetTextureCache().IsMeta(address);
|
||||
auto [buffer, offset] = context.GetBufferCache().ObtainBuffer(
|
||||
address, size, resource.written || metadata_read,
|
||||
auto [buffer, offset] = context.GetBufferCache().ObtainBuffer(
|
||||
address, size, resource.written,
|
||||
resource.formatted || (resource.read && Config::SyncRawImageBuffersEnabled()), id);
|
||||
if (metadata_read) buffer->Fill(offset, size, 0xf); // HTILE ZMask::kUncompressed.
|
||||
const auto aligned_offset = Common::AlignDown(offset, alignment);
|
||||
const auto adjustment = offset - aligned_offset;
|
||||
const auto max_range = graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange;
|
||||
@@ -147,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() |
|
||||
@@ -158,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;
|
||||
@@ -285,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;
|
||||
@@ -744,6 +725,12 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
|
||||
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
|
||||
const auto address = descriptor.Base48();
|
||||
auto size = descriptor.GetSize();
|
||||
if (size != 0 && descriptor.Stride() == 0 && descriptor.OutOfBounds() == 2 &&
|
||||
!metadata.scalar) {
|
||||
// OOB_SELECT=2 only tests NumRecords != 0; a zero-stride fetch
|
||||
// still needs its full payload when NumRecords is smaller than it.
|
||||
size = std::max(size, uint64_t {metadata.max_byte_extent});
|
||||
}
|
||||
if (address == 0 || size == 0) {
|
||||
source = {};
|
||||
continue;
|
||||
@@ -761,14 +748,18 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const auto& graphics = m_context.GetGraphics();
|
||||
const auto limit = uint64_t {graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange} -
|
||||
(graphics.StorageMinAlignment() - 1);
|
||||
size = std::min({size, uint64_t {256} * 1024 * 1024, limit});
|
||||
}
|
||||
// PPSA15246
|
||||
const auto& graphics = m_context.GetGraphics();
|
||||
const auto limit =
|
||||
uint64_t {graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange} -
|
||||
(graphics.StorageMinAlignment() - 1);
|
||||
size = std::min({size, uint64_t {256} * 1024 * 1024, limit});
|
||||
}
|
||||
// Finite GPU descriptors can span sparse PRT holes; clamp only oversized/sentinel ranges.
|
||||
if (size >= (uint64_t {1} << 30u) || descriptor.NumRecords() == UINT32_MAX) {
|
||||
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
|
||||
}
|
||||
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
|
||||
source = {address, size, cache.FindBuffer(address, size)};
|
||||
}
|
||||
}
|
||||
@@ -950,8 +941,12 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
|
||||
for (const auto& resource: program.info.buffers) {
|
||||
if (!resource.written || resource.descriptor_index == UINT32_MAX) continue;
|
||||
const auto& written = writer->buffer_sources[resource.descriptor_index];
|
||||
if (written.size != 0 && ImageRangeOverlaps(address, size,
|
||||
written.address, written.size)) {
|
||||
// ADD_TID and swizzle move fixed-offset writes past write_extent.
|
||||
const bool linear = (resource.packed_stride & ((1u << 14u) | (1u << 20u))) == 0u;
|
||||
const auto written_size =
|
||||
linear ? std::min<uint64_t>(written.size, resource.write_extent) : written.size;
|
||||
if (written_size != 0 && ImageRangeOverlaps(address, size,
|
||||
written.address, written_size)) {
|
||||
EXIT("scalar resource reads overlap a shader buffer write\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +275,7 @@ struct PipelineCache::ProgramCache {
|
||||
ShaderRecompiler::IR::ResourceSnapshot resources;
|
||||
ShaderRecompiler::IR::ResourceSpecialization specialization;
|
||||
std::vector<Permutation> permutations;
|
||||
bool skip_dispatch = false;
|
||||
};
|
||||
|
||||
struct ProgramKeyHash {
|
||||
@@ -360,6 +361,7 @@ struct PipelineCache::ProgramCache {
|
||||
bool source_changed = false;
|
||||
if (entry != programs.end()) {
|
||||
auto& cached = entry->second;
|
||||
if (cached.skip_dispatch) return {};
|
||||
Permutation* permutation = nullptr;
|
||||
if (cached.call_source) {
|
||||
source_changed = cached.RefreshCallSource(runtime);
|
||||
@@ -419,10 +421,18 @@ struct PipelineCache::ProgramCache {
|
||||
DumpShaderOriginal(stage_name, options.shader_hash, params.code);
|
||||
ShaderRecompiler::ShaderSource source;
|
||||
if (entry == programs.end()) source = ShaderRecompiler::PrepareShaderSource(params.code, options);
|
||||
if (source.unsupported_call_pc != UINT32_MAX) {
|
||||
PipelineCacheLog("!!! TEMPORARY COMPUTE DISPATCH SKIP !!! shader=0x{:016x} pc=0x{:08x} "
|
||||
"reason=unsupported GPU-selected callable target; rendering is incomplete",
|
||||
params.hash, source.unsupported_call_pc);
|
||||
programs.try_emplace(lookup_key, ShaderRecompiler::IR::ResourcePlan{}, nullptr)
|
||||
.first->second.skip_dispatch = true;
|
||||
return {};
|
||||
}
|
||||
auto* current = entry == programs.end() ? &source : entry->second.call_source.get();
|
||||
ShaderRecompiler::TranslateResult translated;
|
||||
if (current != nullptr) {
|
||||
if (current->call) {
|
||||
if (!current->calls.empty()) {
|
||||
if (entry == programs.end()) ShaderRecompiler::RefreshShaderSource(*current, runtime);
|
||||
translated = ShaderRecompiler::TranslateProgram(current->linked->decoded, options);
|
||||
translated.program.source_reads = current->reads;
|
||||
@@ -433,7 +443,7 @@ struct PipelineCache::ProgramCache {
|
||||
translated = ShaderRecompiler::TranslateProgram(params.code, options);
|
||||
}
|
||||
if (entry == programs.end()) {
|
||||
auto retained = source.call ? std::make_unique<ShaderRecompiler::ShaderSource>(std::move(source)) : nullptr;
|
||||
auto retained = !source.calls.empty() ? std::make_unique<ShaderRecompiler::ShaderSource>(std::move(source)) : nullptr;
|
||||
entry = programs.try_emplace(lookup_key,
|
||||
ShaderRecompiler::IR::ExtractResourcePlan(translated.program), std::move(retained)).first;
|
||||
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
|
||||
@@ -580,8 +590,8 @@ void PipelineCache::InitializeDriverCache() {
|
||||
return;
|
||||
}
|
||||
if (!initial_data.empty()) {
|
||||
PipelineCacheLog("Vulkan pipeline cache: loaded {} bytes from {}", initial_data.size(),
|
||||
path);
|
||||
PipelineCacheLog("Vulkan pipeline cache: loaded {:.2f} MB from {}",
|
||||
static_cast<double>(initial_data.size()) / 1'000'000.0, path);
|
||||
} else {
|
||||
PipelineCacheLog("Vulkan pipeline cache: initialized empty");
|
||||
}
|
||||
@@ -621,8 +631,8 @@ void PipelineCache::Save() {
|
||||
}
|
||||
if (result != vk::Result::eSuccess || size == 0 ||
|
||||
size > std::numeric_limits<uint32_t>::max()) {
|
||||
PipelineCacheLog("Vulkan pipeline cache: save failed ({}, {} bytes)",
|
||||
vk::to_string(result), size);
|
||||
PipelineCacheLog("Vulkan pipeline cache: save failed ({}, {:.2f} MB)",
|
||||
vk::to_string(result), static_cast<double>(size) / 1'000'000.0);
|
||||
return;
|
||||
}
|
||||
payload.resize(size);
|
||||
@@ -654,7 +664,8 @@ void PipelineCache::Save() {
|
||||
Common::PathToString(m_driver_cache_path));
|
||||
return;
|
||||
}
|
||||
PipelineCacheLog("Vulkan pipeline cache: saved {} bytes to {}", payload.size(),
|
||||
PipelineCacheLog("Vulkan pipeline cache: saved {:.2f} MB to {}",
|
||||
static_cast<double>(payload.size()) / 1'000'000.0,
|
||||
Common::PathToString(m_driver_cache_path));
|
||||
m_driver_cache_dirty = false;
|
||||
}
|
||||
@@ -769,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;
|
||||
@@ -782,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);
|
||||
@@ -951,6 +971,7 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
|
||||
if (static_params.sample_shading_enable && !m_graphics.sample_rate_shading_enabled) {
|
||||
EXIT("Pipeline: sample-rate shading is required but unsupported by the host\n");
|
||||
}
|
||||
static_params.alpha_to_coverage_enable = ps_active && ctx.GetShaderRegisters().AlphaToMask();
|
||||
const bool rect_list = Prospero::IsRectList(command.GetUserConfig().GetPrimType());
|
||||
static_params.cull_back = !rect_list && mc.cull_back;
|
||||
static_params.cull_front = !rect_list && mc.cull_front;
|
||||
|
||||
@@ -39,6 +39,7 @@ struct PipelineStaticParameters {
|
||||
bool primitive_restart_enable = false;
|
||||
uint32_t samples = 1;
|
||||
bool sample_shading_enable = false;
|
||||
bool alpha_to_coverage_enable = false;
|
||||
uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||
bool cull_front = false;
|
||||
bool cull_back = false;
|
||||
@@ -62,7 +63,7 @@ struct PipelineStaticParameters {
|
||||
static_assert(std::is_trivially_copyable_v<PipelineStaticParameters>);
|
||||
static_assert(std::is_standard_layout_v<PipelineStaticParameters>);
|
||||
static_assert(alignof(PipelineStaticParameters) == 1);
|
||||
static_assert(sizeof(PipelineStaticParameters) == 116);
|
||||
static_assert(sizeof(PipelineStaticParameters) == 117);
|
||||
|
||||
struct PipelineRenderingState {
|
||||
std::array<vk::Format, RENDER_COLOR_ATTACHMENTS_MAX> color_formats {};
|
||||
|
||||
@@ -92,7 +92,8 @@ bool HasShaderBufferWrites(const ShaderStageRuntime& runtime) {
|
||||
const auto& program = *runtime.program;
|
||||
const auto& resources = *runtime.resources;
|
||||
EXIT_IF(resources.buffers.size() != program.info.buffers.size());
|
||||
bool has_writes = false;
|
||||
// Address writes reach memory outside the bound buffer descriptors.
|
||||
bool has_writes = program.has_address_writes;
|
||||
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
|
||||
if (!program.info.buffers[i].written) {
|
||||
continue;
|
||||
|
||||
@@ -196,21 +196,27 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe
|
||||
const auto& limits = graphics.GetPhysicalDeviceProperties().limits;
|
||||
uint64_t descriptor_count = 0;
|
||||
uint64_t storage_buffers = 0;
|
||||
std::array<uint64_t, 32> stage_buffers {}, stage_resources {};
|
||||
uint64_t samplers = 0;
|
||||
std::array<uint64_t, 32> stage_buffers {}, stage_resources {}, stage_samplers {};
|
||||
for (const auto& binding: bindings) {
|
||||
descriptor_count += binding.descriptorCount;
|
||||
const bool storage = binding.descriptorType == vk::DescriptorType::eStorageBuffer;
|
||||
const bool sampler = binding.descriptorType == vk::DescriptorType::eSampler;
|
||||
if (storage) storage_buffers += binding.descriptorCount;
|
||||
if (sampler) samplers += binding.descriptorCount;
|
||||
for (auto stages = static_cast<uint32_t>(binding.stageFlags); stages != 0; stages &= stages - 1u) {
|
||||
const auto stage = std::countr_zero(stages);
|
||||
if (storage) stage_buffers[stage] += binding.descriptorCount;
|
||||
if (binding.descriptorType != vk::DescriptorType::eSampler)
|
||||
if (sampler) stage_samplers[stage] += binding.descriptorCount;
|
||||
else
|
||||
stage_resources[stage] += binding.descriptorCount;
|
||||
if (stage_buffers[stage] > limits.maxPerStageDescriptorStorageBuffers ||
|
||||
stage_resources[stage] > limits.maxPerStageResources) {
|
||||
stage_resources[stage] > limits.maxPerStageResources ||
|
||||
stage_samplers[stage] > limits.maxPerStageDescriptorSamplers) {
|
||||
EXIT("native descriptor stage %u exceeds device limits: buffers=%" PRIu64
|
||||
"/%u resources=%" PRIu64 "/%u\n", stage, stage_buffers[stage],
|
||||
limits.maxPerStageDescriptorStorageBuffers, stage_resources[stage], limits.maxPerStageResources);
|
||||
"/%u resources=%" PRIu64 "/%u samplers=%" PRIu64 "/%u\n", stage, stage_buffers[stage],
|
||||
limits.maxPerStageDescriptorStorageBuffers, stage_resources[stage], limits.maxPerStageResources,
|
||||
stage_samplers[stage], limits.maxPerStageDescriptorSamplers);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,6 +224,10 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe
|
||||
EXIT("native storage-buffer descriptors exceed device limit: %" PRIu64 "/%u\n",
|
||||
storage_buffers, limits.maxDescriptorSetStorageBuffers);
|
||||
}
|
||||
if (samplers > limits.maxDescriptorSetSamplers) {
|
||||
EXIT("native sampler descriptors exceed device limit: %" PRIu64 "/%u\n",
|
||||
samplers, limits.maxDescriptorSetSamplers);
|
||||
}
|
||||
pipeline.uses_push_descriptors = descriptor_count <= graphics.max_push_descriptors;
|
||||
|
||||
vk::DescriptorSetLayoutCreateInfo create {};
|
||||
@@ -412,6 +422,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
multisampling.sampleShadingEnable = static_params.sample_shading_enable ? VK_TRUE : VK_FALSE;
|
||||
multisampling.rasterizationSamples = vulkan_sample_count(static_params.samples);
|
||||
multisampling.minSampleShading = 1.0f;
|
||||
multisampling.alphaToCoverageEnable =
|
||||
static_params.alpha_to_coverage_enable ? VK_TRUE : VK_FALSE;
|
||||
|
||||
vk::PipelineColorBlendAttachmentState color_blend_attachment[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||
for (uint32_t i = 0; i < rendering.color_count; i++) {
|
||||
@@ -626,6 +638,9 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
|
||||
|
||||
vk::ComputePipelineCreateInfo info {};
|
||||
if (input_info.stage.program->info.uses_gds) {
|
||||
info.flags = vk::PipelineCreateFlagBits::eDispatchBase;
|
||||
}
|
||||
info.stage = comp_shader_stage_info;
|
||||
info.layout = pipeline.pipeline_layout;
|
||||
info.basePipelineIndex = -1;
|
||||
|
||||
@@ -201,11 +201,11 @@ private:
|
||||
void BindImage(ImageId id, bool storage);
|
||||
void BindRenderTarget(ImageId id);
|
||||
void ResetBindings();
|
||||
void UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer);
|
||||
[[nodiscard]] bool TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
|
||||
CommandBuffer& command, uint32_t group_x,
|
||||
uint32_t group_y, uint32_t group_z, uint32_t mode);
|
||||
[[nodiscard]] bool TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer);
|
||||
[[nodiscard]] bool TryConsumeComputeFill(const ShaderComputeInputInfo& input,
|
||||
CommandBuffer& command, uint32_t group_x,
|
||||
uint32_t group_y, uint32_t group_z, uint32_t mode);
|
||||
|
||||
RenderContext& m_context;
|
||||
GraphicsBindings m_graphics_bindings;
|
||||
|
||||
@@ -51,8 +51,8 @@ static bool FillSourcesDisjoint(std::span<const ShaderRecompiler::IR::Descriptor
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer) {
|
||||
bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer) {
|
||||
const auto& program = *input.stage.program;
|
||||
const auto& resources = *input.stage.resources;
|
||||
if (resources.buffers.size() != program.info.buffers.size()) {
|
||||
@@ -62,9 +62,10 @@ void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input
|
||||
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
|
||||
const auto& resource = program.info.buffers[i];
|
||||
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
|
||||
// Reads and transforms of metadata do not establish a clear state.
|
||||
// A metadata resource that is also read is not proven to be a full overwrite. Execute it
|
||||
// conservatively instead of replacing the dispatch with a coarse full-surface clear.
|
||||
if ((!resource.written || resource.read) && cache.IsMeta(descriptor.Base48())) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +75,13 @@ void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input
|
||||
if (resource.written) {
|
||||
const auto descriptor =
|
||||
DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
|
||||
(void)cache.ClearMeta(descriptor.Base48());
|
||||
if (cache.ClearMeta(descriptor.Base48())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t group_x,
|
||||
@@ -128,9 +132,9 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
|
||||
CommandBuffer& command, uint32_t group_x,
|
||||
uint32_t group_y, uint32_t group_z, uint32_t mode) {
|
||||
bool RenderExecutor::TryConsumeComputeFill(const ShaderComputeInputInfo& input,
|
||||
CommandBuffer& command, uint32_t group_x,
|
||||
uint32_t group_y, uint32_t group_z, uint32_t mode) {
|
||||
const auto& program = *input.stage.program;
|
||||
const auto& resources = *input.stage.resources;
|
||||
const auto& fill = resources.uniform_fill;
|
||||
@@ -186,14 +190,26 @@ bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& i
|
||||
size)) {
|
||||
return false;
|
||||
}
|
||||
if (!cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear)) {
|
||||
return false;
|
||||
const bool image_clear =
|
||||
cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear);
|
||||
if (!image_clear) {
|
||||
// CPU fills invalidate overlapping cached images. Preserve the native dispatch for
|
||||
// partial or ambiguous image backings, including pooled metadata aliases.
|
||||
if ((descriptor.Base48() & 3u) != 0 ||
|
||||
!m_context.IsMapped(descriptor.Base48(), size) ||
|
||||
cache.IsRegionRegistered(descriptor.Base48(), size)) {
|
||||
return false;
|
||||
}
|
||||
// Metadata need not have an image backing yet. Keep proven fills in guest memory
|
||||
// when CPU-owned, so color-clear discovery can read them without a GPU download.
|
||||
m_context.GetBufferCache().FillBuffer(descriptor.Base48(), size, packed_clear, false);
|
||||
}
|
||||
static std::atomic<uint32_t> logged_clears {0};
|
||||
if (logged_clears.fetch_add(1, std::memory_order_relaxed) < 32) {
|
||||
LOGF("GraphicsRenderDispatchDirect: compute image clear shader=0x%016" PRIx64
|
||||
LOGF("GraphicsRenderDispatchDirect: compute %s shader=0x%016" PRIx64
|
||||
" addr=0x%016" PRIx64 " size=0x%016" PRIx64 " value=0x%08" PRIx32 "\n",
|
||||
input.stage.program->shader_hash, descriptor.Base48(), size, packed_clear);
|
||||
image_clear ? "image clear" : "buffer fill", input.stage.program->shader_hash,
|
||||
descriptor.Base48(), size, packed_clear);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -297,6 +313,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
}
|
||||
const auto compute_program =
|
||||
m_context.GetPipelineCache().GetComputeProgram(cs_regs, sh_regs, input_info);
|
||||
if (!compute_program) return;
|
||||
if (use_thread_dimensions) {
|
||||
input_info.dispatch_threads_num[0] = thread_group_x;
|
||||
input_info.dispatch_threads_num[1] = thread_group_y;
|
||||
@@ -305,14 +322,12 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
|
||||
const auto& program = *input_info.stage.program;
|
||||
const auto& resources = *input_info.stage.resources;
|
||||
if (resources.specialization_reads.empty()) {
|
||||
// Cached metadata addresses can be reused as ordinary buffers; preserve their guest writes.
|
||||
UpdateComputeMetaClears(input_info, buffer);
|
||||
if (TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
|
||||
thread_group_z, mode)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
if (resources.specialization_reads.empty() &&
|
||||
(TryConsumeComputeMetaClear(input_info, buffer) ||
|
||||
TryConsumeComputeFill(input_info, buffer, thread_group_x, thread_group_y,
|
||||
thread_group_z, mode))) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
|
||||
if (use_thread_dimensions) {
|
||||
@@ -432,7 +447,27 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
}
|
||||
|
||||
vk_buffer.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline.pipeline);
|
||||
vk_buffer.dispatch(thread_group_x, thread_group_y, thread_group_z);
|
||||
constexpr uint32_t DISPATCH_INITIATOR_ORDERED_APPEND_ENABLE = 1u << 3u;
|
||||
const uint64_t group_threads = uint64_t {std::max(input_info.threads_num[0], 1u)} *
|
||||
std::max(input_info.threads_num[1], 1u) *
|
||||
std::max(input_info.threads_num[2], 1u);
|
||||
if ((mode & DISPATCH_INITIATOR_ORDERED_APPEND_ENABLE) != 0 && program.info.uses_gds &&
|
||||
group_threads <= input_info.wave_size && !input_info.lds_storage) {
|
||||
// A single guest wave per group can run in launch order without a device-wide
|
||||
// spinlock. Keep the guest workgroup IDs when splitting the dispatch.
|
||||
for (uint32_t z = 0; z < thread_group_z; ++z) {
|
||||
for (uint32_t y = 0; y < thread_group_y; ++y) {
|
||||
for (uint32_t x = 0; x < thread_group_x; ++x) {
|
||||
if ((x | y | z) != 0u) {
|
||||
ShaderAccessBarrier(vk_buffer, vk::PipelineStageFlagBits::eComputeShader);
|
||||
}
|
||||
vk_buffer.dispatchBase(x, y, z, 1, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vk_buffer.dispatch(thread_group_x, thread_group_y, thread_group_z);
|
||||
}
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::Dispatches);
|
||||
|
||||
// The removed host fence also ordered read-only dispatches before later writers.
|
||||
@@ -464,6 +499,7 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
input_info.indirect_args_address = args_addr;
|
||||
const auto compute_program = m_context.GetPipelineCache().GetComputeProgram(
|
||||
cs_regs, buffer.GetRegisters().GetShaderRegisters(), input_info);
|
||||
if (!compute_program) return;
|
||||
buffer.EndRendering();
|
||||
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
|
||||
auto& bindings = m_compute_bindings;
|
||||
|
||||
@@ -436,7 +436,7 @@ static bool DrawHasValidVertexShader(const HW::Shader& sh_ctx) {
|
||||
static bool PixelShaderHasDepthOrCoverageSideEffects(const HW::ShaderRegisters& sh_regs) {
|
||||
const auto& db = sh_regs.db_shader_control;
|
||||
return db.shader_kill_enable || db.shader_z_export_enable || db.shader_mask_export_enable ||
|
||||
db.shader_dual_export_enable || db.shader_execute_on_noop;
|
||||
db.shader_dual_export_enable || db.shader_execute_on_noop || sh_regs.AlphaToMask();
|
||||
}
|
||||
|
||||
struct DrawRenderState {
|
||||
@@ -935,6 +935,29 @@ bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCal
|
||||
LogDrawPhase(draw.Name(), "ResolveRenderDepthTarget");
|
||||
}
|
||||
ResolveRenderDepthTarget(buffer, state.depth_info);
|
||||
// TODO(PPSA09243): Vulkan passes take one sample count. Where PS5 attachments mix counts,
|
||||
// keep those matching the raster count instead of resolving multisampled shadows.
|
||||
const auto colors = std::span {state.color_info, state.color_count};
|
||||
const auto pass_samples = state.depth_info.image_id ? state.depth_info.desc.info.samples
|
||||
: state.color_info[0].desc.info.samples;
|
||||
if (std::ranges::any_of(colors, [&](const RenderColorInfo& color) {
|
||||
return color.desc.info.samples != pass_samples;
|
||||
})) {
|
||||
const auto raster =
|
||||
render_sample_count(buffer.GetRegisters().GetAaConfig().msaa_num_samples);
|
||||
const auto dropped = std::ranges::remove_if(colors, [&](const RenderColorInfo& color) {
|
||||
return color.desc.info.samples != raster;
|
||||
});
|
||||
state.color_count -= static_cast<uint32_t>(dropped.size());
|
||||
if (state.depth_info.image_id && state.depth_info.desc.info.samples != raster) {
|
||||
state.depth_info = {};
|
||||
}
|
||||
static std::atomic_bool logged = false;
|
||||
if (!logged.exchange(true, std::memory_order_relaxed)) {
|
||||
Log::WriteToConsoleAndLog(
|
||||
"Warning: dropping attachments whose sample count differs from the raster.\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (state.color_count == 0 && !state.depth_info.image_id && !state.ps_active) {
|
||||
LogFramebufferSkip(draw.Name(), state.color_info[0], state.depth_info, buffer,
|
||||
@@ -1193,13 +1216,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 +1237,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,
|
||||
@@ -1220,6 +1250,20 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
dynamic_state.SetAttachmentFeedbackLoopAspects(feedback_aspects);
|
||||
dynamic_state.Commit(m_context.GetGraphics(), vk_buffer);
|
||||
|
||||
vk::PipelineStageFlags shader_write_stages = {};
|
||||
for (const auto& stage: vertex_stages) {
|
||||
if (HasShaderBufferWrites(stage.stage)) {
|
||||
shader_write_stages |= ShaderPipelineStages(NativeShaderStage(stage.logical_stage));
|
||||
}
|
||||
}
|
||||
if (state.ps_active && HasShaderBufferWrites(state.ps_input_info.stage)) {
|
||||
shader_write_stages |= vk::PipelineStageFlagBits::eFragmentShader;
|
||||
}
|
||||
if (shader_write_stages) {
|
||||
// As for dispatches, buffer writes wait for earlier accesses to the memory.
|
||||
m_context.GetCommandScheduler().EndRendering();
|
||||
ShaderWriteHazardBarrier(vk_buffer, shader_write_stages);
|
||||
}
|
||||
LogDrawPhase(draw.Name(), "BeginRendering");
|
||||
if (!draw.IsIndexed()) {
|
||||
SetDrawDebugPhase(buffer, submit_id, draw, 0x400u);
|
||||
@@ -1240,15 +1284,6 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
if (!draw.IsIndexed()) {
|
||||
SetDrawDebugPhase(buffer, submit_id, draw, 0x600u);
|
||||
}
|
||||
vk::PipelineStageFlags shader_write_stages = {};
|
||||
for (const auto& stage: vertex_stages) {
|
||||
if (HasShaderBufferWrites(stage.stage)) {
|
||||
shader_write_stages |= ShaderPipelineStages(NativeShaderStage(stage.logical_stage));
|
||||
}
|
||||
}
|
||||
if (state.ps_active && HasShaderBufferWrites(state.ps_input_info.stage)) {
|
||||
shader_write_stages |= vk::PipelineStageFlagBits::eFragmentShader;
|
||||
}
|
||||
if (shader_write_stages) {
|
||||
m_context.GetCommandScheduler().EndRendering();
|
||||
ShaderWriteBarrier(vk_buffer, shader_write_stages);
|
||||
|
||||
@@ -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 {};
|
||||
@@ -481,18 +481,13 @@ Decoder::Program DecodeFusedProgram(std::span<const uint32_t> front, std::span<c
|
||||
return result;
|
||||
}
|
||||
|
||||
void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
|
||||
void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
|
||||
using namespace IR;
|
||||
EXIT_NOT_IMPLEMENTED(options.stage != ShaderType::Compute);
|
||||
for (uint32_t i = 0; i < source.decoded.instructions.size(); ++i) {
|
||||
if (source.decoded.instructions[i].opcode != Decoder::Opcode::S_SWAPPC_B64) continue;
|
||||
EXIT_NOT_IMPLEMENTED(source.call.has_value() || options.stage != ShaderType::Compute);
|
||||
source.call = ShaderSource::Call {.instruction = i};
|
||||
source.calls.push_back({.instruction = i});
|
||||
}
|
||||
EXIT_IF(options.input_info.compute == nullptr);
|
||||
const auto& call = source.decoded.instructions[source.call->instruction];
|
||||
EXIT_NOT_IMPLEMENTED(call.src0.kind != Decoder::OperandKind::Sgpr ||
|
||||
call.dst.kind != Decoder::OperandKind::Sgpr ||
|
||||
call.src0.reg != call.dst.reg || call.dst.reg >= 105u);
|
||||
const auto graph = CFG::BuildGraph(source.decoded);
|
||||
Program query;
|
||||
query.stage = options.stage;
|
||||
@@ -524,7 +519,11 @@ void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
|
||||
return value;
|
||||
};
|
||||
Register = [&](uint32_t before, uint32_t code) -> Value {
|
||||
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code);
|
||||
std::vector<uint32_t> crossed_calls;
|
||||
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code, crossed_calls);
|
||||
for (const auto crossed: crossed_calls) {
|
||||
std::ranges::find(source.calls, crossed, &ShaderSource::Call::instruction)->preserved_registers.set(code);
|
||||
}
|
||||
if (index == UINT32_MAX) {
|
||||
EXIT_IF(code < query.user_data_base || code - query.user_data_base >= query.user_data_count);
|
||||
return IREmitter(&block).GetUserData(static_cast<ScalarReg>(code));
|
||||
@@ -557,18 +556,30 @@ void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
|
||||
EXIT_IF(!producer.ready || !producer.values.contains(code));
|
||||
return producer.values.at(code).Resolve();
|
||||
};
|
||||
for (uint32_t i = 0; i < 2u; ++i) {
|
||||
const auto value = Register(source.call->instruction, call.src0.reg + i);
|
||||
EXIT_IF(!ValidateRuntimeValue(query, value, RuntimeValueType::Integer));
|
||||
auto& root = query.value_storage.emplace_back(ValueOpcode::ReferenceU32);
|
||||
root.SetArg(0, value);
|
||||
source.call->target[i] = Value(&root);
|
||||
for (auto& site: source.calls) {
|
||||
const auto& call = source.decoded.instructions[site.instruction];
|
||||
EXIT_NOT_IMPLEMENTED(call.src0.kind != Decoder::OperandKind::Sgpr ||
|
||||
call.dst.kind != Decoder::OperandKind::Sgpr ||
|
||||
call.src0.reg != call.dst.reg || call.dst.reg >= 105u);
|
||||
for (uint32_t i = 0; i < 2u; ++i) {
|
||||
const auto value = Register(site.instruction, call.src0.reg + i);
|
||||
if (!ValidateRuntimeValue(query, value, RuntimeValueType::Integer)) {
|
||||
EXIT_IF(value.Instruction()->GetOpcode() != ValueOpcode::ReadConstBuffer);
|
||||
source.unsupported_call_pc = call.pc;
|
||||
source.calls.clear();
|
||||
return;
|
||||
}
|
||||
auto& root = query.value_storage.emplace_back(ValueOpcode::ReferenceU32);
|
||||
root.SetArg(0, value);
|
||||
site.target[i] = Value(&root);
|
||||
}
|
||||
}
|
||||
RewriteToSsa(query.blocks);
|
||||
ConstantPropagationPass(query.blocks, query.wave_size);
|
||||
RemoveIdentities(query);
|
||||
EliminateDeadCode(query.blocks);
|
||||
for (auto& value: source.call->target) value = value.Instruction()->Arg(0).Resolve();
|
||||
for (auto& site: source.calls)
|
||||
for (auto& value: site.target) value = value.Instruction()->Arg(0).Resolve();
|
||||
query.value_storage.clear();
|
||||
for (auto& inst: block) {
|
||||
inst.SetParent(nullptr);
|
||||
@@ -595,7 +606,7 @@ ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOp
|
||||
Decoder::DecodeProgram(code, source.decoded);
|
||||
}
|
||||
if (source.decoded.has_swap_pc) {
|
||||
PrepareCallTarget(source, options);
|
||||
PrepareCallTargets(source, options);
|
||||
source.code.assign(code.begin(), code.end());
|
||||
source.decoded.code = source.code;
|
||||
}
|
||||
@@ -603,88 +614,122 @@ ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOp
|
||||
}
|
||||
|
||||
const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtRuntime& runtime) {
|
||||
EXIT_IF(source.unsupported_call_pc != UINT32_MAX);
|
||||
auto& reads = source.reads;
|
||||
reads.clear();
|
||||
IR::SrtReadCapture capture(runtime, reads);
|
||||
const auto clean = IR::CleanRuntime(capture.ObservedRuntime());
|
||||
IR::SrtWalker walker(source.call_targets, clean);
|
||||
uint32_t low = 0, high = 0;
|
||||
const auto& call = source.decoded.instructions[source.call->instruction];
|
||||
if (!walker.Evaluate(source.call->target[0], low) || !walker.Evaluate(source.call->target[1], high))
|
||||
EXIT("shader call at pc 0x%08x has an unavailable scalar target", call.pc);
|
||||
// SWAPPC reads the old aliased pair and ignores the target's low two bits.
|
||||
const auto address = ((uint64_t {high} << 32u) | low) & ~uint64_t {3};
|
||||
EXIT_IF(address == 0u);
|
||||
for (auto& site: source.calls) {
|
||||
uint32_t low = 0, high = 0;
|
||||
if (!walker.Evaluate(site.target[0], low) || !walker.Evaluate(site.target[1], high))
|
||||
EXIT("shader call at pc 0x%08x has an unavailable scalar target",
|
||||
source.decoded.instructions[site.instruction].pc);
|
||||
// SWAPPC reads the old aliased pair and ignores the target's low two bits.
|
||||
const auto address = ((uint64_t {high} << 32u) | low) & ~uint64_t {3};
|
||||
EXIT_IF(address == 0u);
|
||||
site.address = address;
|
||||
}
|
||||
const auto query_reads = reads.size();
|
||||
if (source.linked) {
|
||||
auto& linked = *source.linked;
|
||||
const auto function = std::span(linked.code).subspan(source.code.size());
|
||||
if (clean.read_memory(clean.userdata, address, linked.observed_function) &&
|
||||
std::ranges::equal(function, linked.observed_function)) {
|
||||
return linked.decoded;
|
||||
bool unchanged = true;
|
||||
for (size_t i = 0; i < source.calls.size(); ++i) {
|
||||
const auto& function = linked.functions[i];
|
||||
const auto expected = std::span(linked.code).subspan(function.offset, function.size);
|
||||
const auto preceding = std::span(source.calls).first(i);
|
||||
const auto previous = std::ranges::find(preceding, source.calls[i].address, &ShaderSource::Call::address);
|
||||
if (previous != preceding.end()) {
|
||||
const auto& observed = linked.functions[previous - preceding.begin()];
|
||||
if (std::ranges::equal(expected, std::span(linked.code).subspan(observed.offset, observed.size))) continue;
|
||||
unchanged = false;
|
||||
break;
|
||||
}
|
||||
linked.observed_function.resize(function.size);
|
||||
if (!clean.read_memory(clean.userdata, source.calls[i].address, linked.observed_function) ||
|
||||
!std::ranges::equal(expected, linked.observed_function)) {
|
||||
unchanged = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (unchanged) return linked.decoded;
|
||||
reads.resize(query_reads);
|
||||
}
|
||||
ShaderSource::Linked linked {.code = source.code,
|
||||
.decoded = source.decoded};
|
||||
ShaderSource::Linked linked {.code = source.code, .decoded = source.decoded};
|
||||
const auto& last = source.decoded.instructions.back();
|
||||
EXIT_NOT_IMPLEMENTED(last.opcode != Decoder::Opcode::S_ENDPGM);
|
||||
// Keep the caller's data/footer bytes at their native offsets. The leaf's virtual CFG
|
||||
// PCs follow decoded instructions; its physical code is independently read and owned.
|
||||
const auto function_pc = last.pc + last.word_count * sizeof(uint32_t);
|
||||
for (uint32_t index = 0;;) {
|
||||
// The observed queue helper is a scalar leaf. Scalar encodings need at most one literal.
|
||||
EXIT_IF(index >= 64u || address > UINT64_MAX - (uint64_t {index} + 2u) * sizeof(uint32_t));
|
||||
std::array<uint32_t, 2> words {};
|
||||
if (!clean.read_memory(clean.userdata, address + uint64_t {index} * sizeof(uint32_t),
|
||||
std::span(words).first(1)))
|
||||
EXIT("shader call at pc 0x%08x cannot read leaf code", call.pc);
|
||||
const auto family = Decoder::GetInstructionFamily(words[0]);
|
||||
EXIT_NOT_IMPLEMENTED(family != Decoder::Family::SOP1 && family != Decoder::Family::SOP2 &&
|
||||
family != Decoder::Family::SOPC && family != Decoder::Family::SOPK &&
|
||||
family != Decoder::Family::SOPP);
|
||||
Decoder::Instruction inst;
|
||||
Decoder::DecodeInstruction(words, 0u, inst);
|
||||
if (inst.word_count == 2u) {
|
||||
if (!clean.read_memory(clean.userdata, address + uint64_t {index + 1u} * sizeof(uint32_t),
|
||||
std::span(words).last(1)))
|
||||
EXIT("shader call at pc 0x%08x cannot read leaf literal", call.pc);
|
||||
inst = {};
|
||||
Decoder::DecodeInstruction(words, 0u, inst);
|
||||
// Keep the caller's data/footer at native offsets. Expand leaf CFGs per call site
|
||||
// before SSA, so return edges neither require a dispatcher nor merge resource roots.
|
||||
uint32_t function_pc = last.pc + last.word_count * sizeof(uint32_t);
|
||||
std::map<uint64_t, std::vector<Decoder::Instruction>> functions;
|
||||
for (size_t i = 0; i < source.calls.size(); ++i) {
|
||||
const auto& site = source.calls[i];
|
||||
const auto& call = source.decoded.instructions[site.instruction];
|
||||
const auto address = source.calls[i].address;
|
||||
auto [entry, inserted] = functions.try_emplace(address);
|
||||
auto& function = entry->second;
|
||||
if (inserted) {
|
||||
for (uint32_t index = 0;;) {
|
||||
std::array<uint32_t, Decoder::MaxInstructionRawWords> words {};
|
||||
EXIT_IF(index > UINT32_MAX / sizeof(uint32_t) - words.size() ||
|
||||
address > UINT64_MAX - (uint64_t {index} + words.size()) * sizeof(uint32_t));
|
||||
uint32_t count = 1;
|
||||
Decoder::Instruction inst;
|
||||
for (;;) {
|
||||
if (!clean.read_memory(clean.userdata, address + uint64_t {index} * sizeof(uint32_t),
|
||||
std::span(words).first(count)))
|
||||
EXIT("shader call at pc 0x%08x cannot read leaf code", call.pc);
|
||||
inst = {};
|
||||
Decoder::DecodeInstruction(words, 0u, inst);
|
||||
if (inst.word_count <= count) break;
|
||||
count = inst.word_count;
|
||||
}
|
||||
inst.pc = index * sizeof(uint32_t);
|
||||
if (Decoder::IsDirectBranch(inst.opcode)) inst.branch_target += inst.pc;
|
||||
index += inst.word_count;
|
||||
EXIT_NOT_IMPLEMENTED(inst.opcode == Decoder::Opcode::S_SWAPPC_B64 ||
|
||||
inst.opcode == Decoder::Opcode::S_GETPC_B64 ||
|
||||
inst.opcode == Decoder::Opcode::S_ENDPGM);
|
||||
function.push_back(inst);
|
||||
if (inst.opcode == Decoder::Opcode::S_SETPC_B64) break;
|
||||
}
|
||||
}
|
||||
inst.pc = index * sizeof(uint32_t);
|
||||
if (Decoder::IsDirectBranch(inst.opcode)) inst.branch_target += inst.pc;
|
||||
linked.code.insert(linked.code.end(), inst.raw, inst.raw + inst.word_count);
|
||||
index += inst.word_count;
|
||||
EXIT_NOT_IMPLEMENTED(inst.opcode == Decoder::Opcode::S_SWAPPC_B64 ||
|
||||
inst.opcode == Decoder::Opcode::S_GETPC_B64 ||
|
||||
inst.opcode == Decoder::Opcode::S_ENDPGM);
|
||||
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg) || CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u));
|
||||
const bool returns = inst.opcode == Decoder::Opcode::S_SETPC_B64;
|
||||
if (returns) {
|
||||
EXIT_NOT_IMPLEMENTED(inst.src0.kind != Decoder::OperandKind::Sgpr || inst.src0.reg != call.dst.reg);
|
||||
inst.opcode = Decoder::Opcode::S_BRANCH;
|
||||
inst.branch_target = call.pc + call.word_count * sizeof(uint32_t);
|
||||
} else if (Decoder::IsDirectBranch(inst.opcode)) {
|
||||
EXIT_IF(inst.branch_target > UINT32_MAX - function_pc);
|
||||
inst.branch_target += function_pc;
|
||||
const uint32_t function_size = function.back().pc + function.back().word_count * sizeof(uint32_t);
|
||||
EXIT_IF(function_pc > UINT32_MAX - function_size);
|
||||
const auto function_offset = static_cast<uint32_t>(linked.code.size());
|
||||
for (auto inst: function) {
|
||||
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg) ||
|
||||
CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u));
|
||||
for (uint32_t code = 0; code < site.preserved_registers.size(); ++code) {
|
||||
if (site.preserved_registers[code] && CFG::MayWriteScalarRegister(inst, code))
|
||||
EXIT("shader call at pc 0x%08x clobbers scalar target dependency s%u", call.pc, code);
|
||||
}
|
||||
linked.code.insert(linked.code.end(), inst.raw, inst.raw + inst.word_count);
|
||||
if (inst.opcode == Decoder::Opcode::S_SETPC_B64) {
|
||||
EXIT_NOT_IMPLEMENTED(inst.src0.kind != Decoder::OperandKind::Sgpr || inst.src0.reg != call.dst.reg);
|
||||
inst.opcode = Decoder::Opcode::S_BRANCH;
|
||||
inst.branch_target = call.pc + call.word_count * sizeof(uint32_t);
|
||||
} else if (Decoder::IsDirectBranch(inst.opcode)) {
|
||||
EXIT_IF(inst.branch_target >= function_size);
|
||||
inst.branch_target += function_pc;
|
||||
}
|
||||
inst.pc += function_pc;
|
||||
linked.decoded.instructions.push_back(std::move(inst));
|
||||
}
|
||||
inst.pc += function_pc;
|
||||
linked.decoded.instructions.push_back(std::move(inst));
|
||||
if (returns) break;
|
||||
}
|
||||
const auto function_end = linked.decoded.instructions.back().pc +
|
||||
linked.decoded.instructions.back().word_count * sizeof(uint32_t);
|
||||
for (size_t i = source.decoded.instructions.size(); i + 1u < linked.decoded.instructions.size(); ++i) {
|
||||
const auto& inst = linked.decoded.instructions[i];
|
||||
if (Decoder::IsDirectBranch(inst.opcode))
|
||||
EXIT_IF(inst.branch_target < function_pc || inst.branch_target >= function_end);
|
||||
linked.functions.push_back({function_offset, function_size / 4u});
|
||||
linked.decoded.instructions[site.instruction].branch_target = function_pc;
|
||||
function_pc += function_size;
|
||||
}
|
||||
reads.resize(query_reads);
|
||||
reads.emplace_back(address, (linked.code.size() - source.code.size()) * sizeof(uint32_t));
|
||||
linked.decoded.instructions[source.call->instruction].branch_target = function_pc;
|
||||
for (size_t i = 0; i < source.calls.size(); ++i) {
|
||||
const auto preceding = std::span(source.calls).first(i);
|
||||
if (std::ranges::find(preceding, source.calls[i].address, &ShaderSource::Call::address) == preceding.end())
|
||||
reads.emplace_back(source.calls[i].address, linked.functions[i].size * sizeof(uint32_t));
|
||||
}
|
||||
linked.decoded.code = linked.code;
|
||||
linked.observed_function.resize(linked.code.size() - source.code.size());
|
||||
linked.observed_function.resize(std::ranges::max_element(linked.functions, {},
|
||||
&ShaderSource::Linked::Function::size)->size);
|
||||
|
||||
source.linked = std::move(linked);
|
||||
++source.revision;
|
||||
return source.linked->decoded;
|
||||
@@ -763,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),
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "graphics/shader/shader.h"
|
||||
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
@@ -47,16 +48,21 @@ struct ShaderSource {
|
||||
struct Call {
|
||||
uint32_t instruction;
|
||||
std::array<IR::Value, 2> target;
|
||||
std::bitset<128> preserved_registers;
|
||||
uint64_t address = 0;
|
||||
};
|
||||
struct Linked {
|
||||
struct Function { uint32_t offset; uint32_t size; };
|
||||
std::vector<uint32_t> code;
|
||||
Decoder::Program decoded;
|
||||
std::vector<Function> functions;
|
||||
std::vector<uint32_t> observed_function;
|
||||
};
|
||||
std::vector<uint32_t> code;
|
||||
Decoder::Program decoded;
|
||||
IR::ResourcePlan call_targets;
|
||||
std::optional<Call> call;
|
||||
std::vector<Call> calls;
|
||||
uint32_t unsupported_call_pc = UINT32_MAX;
|
||||
std::optional<Linked> linked;
|
||||
uint64_t revision = 0;
|
||||
std::vector<std::pair<uint64_t, uint64_t>> reads;
|
||||
|
||||
@@ -21,15 +21,21 @@ 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 {};
|
||||
registers[local ? 3u : 1u] =
|
||||
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 entry {}, registers;
|
||||
entry[local ? 3u : 1u] =
|
||||
local ? Address {Kind::Affine, 1u, 0u} : Address {Kind::PackedControlPoint};
|
||||
const auto constant = [](uint32_t value) { return Address {Kind::Affine, 0u, value}; };
|
||||
const auto read = [&](const Operand& operand) -> Address {
|
||||
@@ -79,78 +85,122 @@ uint32_t ReflectTessellationStride(const Decoder::Program& program, bool local,
|
||||
? value
|
||||
: Address {};
|
||||
};
|
||||
uint32_t stride = 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;
|
||||
const bool buffer_store = inst.opcode == Opcode::BUFFER_STORE_DWORD ||
|
||||
inst.opcode == Opcode::BUFFER_STORE_DWORDX2 ||
|
||||
inst.opcode == Opcode::BUFFER_STORE_DWORDX3 ||
|
||||
inst.opcode == Opcode::BUFFER_STORE_DWORDX4;
|
||||
const bool control_store =
|
||||
!local && buffer_store && inst.src2.kind == OperandKind::Sgpr && inst.src2.reg == 2u;
|
||||
const bool control_read =
|
||||
!local && (inst.opcode == Opcode::DS_READ_B32 || inst.opcode == Opcode::DS_READ2_B32);
|
||||
if ((local && local_store) || control_store || control_read) {
|
||||
const auto address = read(inst.src0);
|
||||
if (address.kind != Kind::Affine) {
|
||||
EXIT("%s tessellation address is not affine at pc 0x%08x\n", local ? "LS" : "HS",
|
||||
inst.pc);
|
||||
}
|
||||
if (address.coefficient != 0u) {
|
||||
const auto expected = control_read ? input_stride : stride;
|
||||
EXIT_NOT_IMPLEMENTED((address.coefficient & 3u) != 0u ||
|
||||
(expected != 0u && expected != address.coefficient));
|
||||
if (!control_read) stride = address.coefficient;
|
||||
}
|
||||
// A branch joins its target with the fallthrough path. Definitions that differ between
|
||||
// the paths are unknown there. Loop headers repeat the walk until back edges are stable.
|
||||
std::unordered_map<uint32_t, Registers> joins, loops;
|
||||
const auto merge = [](Registers& into, const Registers& from) {
|
||||
bool changed = false;
|
||||
for (uint32_t index = 0; index < into.size(); index++) {
|
||||
if (into[index] == from[index]) continue;
|
||||
changed |= into[index] != Address {};
|
||||
into[index] = {};
|
||||
}
|
||||
// Store vdata is a source, despite occupying the decoder's dst field.
|
||||
if (local_store || buffer_store || inst.dst.kind != OperandKind::Vgpr) continue;
|
||||
const auto lhs = read(inst.src0);
|
||||
const auto rhs = read(inst.src1);
|
||||
const auto third = read(inst.src2);
|
||||
Address value;
|
||||
switch (inst.opcode) {
|
||||
case Opcode::V_BFE_U32:
|
||||
if (lhs.kind == Kind::PackedControlPoint && rhs.kind == Kind::Affine &&
|
||||
rhs.coefficient == 0u && third.kind == Kind::Affine &&
|
||||
third.coefficient == 0u) {
|
||||
if (rhs.constant == 8u && third.constant == 5u) value = {Kind::Affine, 1u, 0u};
|
||||
if (rhs.constant == 0u && third.constant == 8u) value = constant(0u);
|
||||
return changed;
|
||||
};
|
||||
|
||||
uint32_t stride = 0, patch_begin = UINT32_MAX, patch_end = 0;
|
||||
for (bool repeat = true; repeat;) {
|
||||
repeat = false;
|
||||
registers = entry;
|
||||
for (const auto& inst: program.instructions) {
|
||||
if (const auto join = joins.find(inst.pc); join != joins.end()) {
|
||||
merge(registers, join->second);
|
||||
joins.erase(join);
|
||||
}
|
||||
if (const auto loop = loops.find(inst.pc); loop != loops.end())
|
||||
merge(registers, loop->second);
|
||||
if (IsDirectBranch(inst.opcode)) {
|
||||
const bool back = inst.branch_target <= inst.pc;
|
||||
const auto [join, inserted] =
|
||||
(back ? loops : joins).try_emplace(inst.branch_target, registers);
|
||||
const bool changed = !inserted && merge(join->second, registers);
|
||||
repeat |= back && (inserted || changed);
|
||||
}
|
||||
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 ||
|
||||
inst.opcode == Opcode::BUFFER_STORE_DWORDX4;
|
||||
const bool control_store = !local && buffer_store &&
|
||||
inst.src2.kind == OperandKind::Sgpr && inst.src2.reg == 2u;
|
||||
if ((local && local_store) || control_store) {
|
||||
const auto address = read(inst.src0);
|
||||
if (address.kind != Kind::Affine) {
|
||||
EXIT("%s tessellation address is not affine at pc 0x%08x\n",
|
||||
local ? "LS" : "HS", inst.pc);
|
||||
}
|
||||
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:
|
||||
if (rhs.kind == Kind::Affine && rhs.coefficient == 0u && rhs.constant < 32u)
|
||||
value = add(multiply(lhs, constant(1u << rhs.constant)), third);
|
||||
break;
|
||||
case Opcode::V_LSHLREV_B32:
|
||||
if (lhs.kind == Kind::Affine && lhs.coefficient == 0u && lhs.constant < 32u)
|
||||
value = multiply(rhs, constant(1u << lhs.constant));
|
||||
break;
|
||||
case Opcode::V_SUB_NC_U32:
|
||||
if (lhs.kind == Kind::Affine && rhs.kind == Kind::Affine &&
|
||||
lhs.coefficient >= rhs.coefficient && lhs.constant >= rhs.constant)
|
||||
value = affine(lhs.coefficient - rhs.coefficient, lhs.constant - rhs.constant);
|
||||
break;
|
||||
default: break;
|
||||
if (address.coefficient != 0u) {
|
||||
EXIT_NOT_IMPLEMENTED((address.coefficient & 3u) != 0u ||
|
||||
(stride != 0u && stride != address.coefficient));
|
||||
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.
|
||||
if (local_store || buffer_store || inst.dst.kind != OperandKind::Vgpr) continue;
|
||||
const auto lhs = read(inst.src0);
|
||||
const auto rhs = read(inst.src1);
|
||||
const auto third = read(inst.src2);
|
||||
Address value;
|
||||
switch (inst.opcode) {
|
||||
case Opcode::V_BFE_U32:
|
||||
if (lhs.kind == Kind::PackedControlPoint && rhs.kind == Kind::Affine &&
|
||||
rhs.coefficient == 0u && third.kind == Kind::Affine &&
|
||||
third.coefficient == 0u) {
|
||||
if (rhs.constant == 8u && third.constant == 5u)
|
||||
value = {Kind::Affine, 1u, 0u};
|
||||
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:
|
||||
if (rhs.kind == Kind::Affine && rhs.coefficient == 0u && rhs.constant < 32u)
|
||||
value = add(multiply(lhs, constant(1u << rhs.constant)), third);
|
||||
break;
|
||||
case Opcode::V_LSHLREV_B32:
|
||||
if (lhs.kind == Kind::Affine && lhs.coefficient == 0u && lhs.constant < 32u)
|
||||
value = multiply(rhs, constant(1u << lhs.constant));
|
||||
break;
|
||||
case Opcode::V_SUB_NC_U32:
|
||||
if (lhs.kind == Kind::Affine && rhs.kind == Kind::Affine &&
|
||||
lhs.coefficient >= rhs.coefficient && lhs.constant >= rhs.constant)
|
||||
value =
|
||||
affine(lhs.coefficient - rhs.coefficient, lhs.constant - rhs.constant);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
if (inst.dst.sdwa_sel != 6u || inst.dst.op_sel || inst.dst.omod || inst.dst.clamp ||
|
||||
inst.dst.dpp)
|
||||
value = {};
|
||||
for (uint32_t index = 0;
|
||||
index < std::max(inst.data_dwords, 1u) && inst.dst.reg + index < registers.size();
|
||||
index++) {
|
||||
registers[inst.dst.reg + index] = {};
|
||||
}
|
||||
registers.at(inst.dst.reg) = value;
|
||||
}
|
||||
if (inst.dst.sdwa_sel != 6u || inst.dst.op_sel || inst.dst.omod || inst.dst.clamp ||
|
||||
inst.dst.dpp)
|
||||
value = {};
|
||||
for (uint32_t index = 0;
|
||||
index < std::max(inst.data_dwords, 1u) && inst.dst.reg + index < registers.size(); index++) {
|
||||
registers[inst.dst.reg + index] = {};
|
||||
}
|
||||
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 +211,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 +228,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 +262,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 +288,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;
|
||||
@@ -261,15 +331,22 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
|
||||
kind = TessellationAttribute::Factor;
|
||||
factors += components;
|
||||
} else {
|
||||
kind = write ? TessellationAttribute::ControlOutput
|
||||
: TessellationAttribute::EvaluationInput;
|
||||
EXIT_NOT_IMPLEMENTED(write
|
||||
? options.stage != ShaderType::TessellationControl
|
||||
: options.stage != ShaderType::TessellationEvaluation);
|
||||
// After its barrier, the HS may read back outputs of other control points.
|
||||
const bool output = options.stage == ShaderType::TessellationControl;
|
||||
EXIT_NOT_IMPLEMENTED(
|
||||
!output && (write || options.stage != ShaderType::TessellationEvaluation));
|
||||
kind = output ? TessellationAttribute::ControlOutput
|
||||
: TessellationAttribute::EvaluationInput;
|
||||
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 = output ? TessellationAttribute::PatchOutput
|
||||
: TessellationAttribute::PatchInput;
|
||||
// HS read-backs must not hide misplaced patch stores from the check below.
|
||||
if (write || !output) patches += components;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -320,11 +397,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);
|
||||
|
||||
@@ -115,7 +115,7 @@ uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
|
||||
} else {
|
||||
state.builder.AddFunction(spv::OpConvertFToU, TypeU32(state), converted_raw, trunc);
|
||||
}
|
||||
const auto nan = EmitClassifyF32(state, src).nan;
|
||||
const auto nan = Unary(state, spv::OpIsNan, TypeBool(state), src);
|
||||
if (signed_value) {
|
||||
const auto below = Binary(state, spv::OpFOrdLessThanEqual, TypeBool(state), src,
|
||||
ConstantF32(state, 0xcf000000u));
|
||||
@@ -160,19 +160,6 @@ uint32_t EmitFPMad32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
|
||||
return sum;
|
||||
}
|
||||
|
||||
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
|
||||
const auto min_ab = EmitMinMaxF32Value(state, a, b, false);
|
||||
const auto min3 = EmitMinMaxF32Value(state, min_ab, c, false);
|
||||
const auto max_ab = EmitMinMaxF32Value(state, a, b, true);
|
||||
const auto high_min = EmitMinMaxF32Value(state, max_ab, c, false);
|
||||
const auto median = EmitMinMaxF32Value(state, min_ab, high_min, true);
|
||||
const auto nan_ab = Binary(state, spv::OpLogicalOr, TypeBool(state),
|
||||
EmitClassifyF32(state, a).nan, EmitClassifyF32(state, b).nan);
|
||||
const auto any_nan =
|
||||
Binary(state, spv::OpLogicalOr, TypeBool(state), nan_ab, EmitClassifyF32(state, c).nan);
|
||||
return Select(state, TypeF32(state), any_nan, min3, median);
|
||||
}
|
||||
|
||||
uint32_t EmitFindUMsb64(EmitterState& state, uint32_t value) {
|
||||
// GLSL FindUMsb is restricted to 32-bit components.
|
||||
const auto unpacked = Unary(state, spv::OpBitcast, TypeU32Vector(state, 2), value);
|
||||
@@ -255,11 +242,9 @@ uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1)
|
||||
}
|
||||
|
||||
uint32_t EmitFPSaturate32(EmitterState& state, uint32_t arg0) {
|
||||
// The clamp modifier runs in DX10 clamp mode: NaN becomes 0. FClamp leaves NaN undefined.
|
||||
const auto clamped = EmitExt(state, TypeF32(state), GLSLstd450FClamp,
|
||||
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
|
||||
return Select(state, TypeF32(state), EmitClassifyF32(state, arg0).nan, ConstantF32(state, 0),
|
||||
clamped);
|
||||
// The clamp modifier runs in DX10 clamp mode: NaN becomes 0, as NClamp's NMax step does.
|
||||
return EmitExt(state, TypeF32(state), GLSLstd450NClamp,
|
||||
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
|
||||
}
|
||||
|
||||
uint32_t EmitSMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
|
||||
@@ -17,16 +17,9 @@ uint32_t EmitCompareU32Constant(EmitterState& state, spv::Op opcode, uint32_t va
|
||||
}
|
||||
|
||||
F32Class EmitClassifyF32Bits(EmitterState& state, uint32_t bits) {
|
||||
F32Class cls;
|
||||
cls.bits = bits;
|
||||
const auto abs_bits = EmitAndConstant(state, cls.bits, 0x7fffffffu);
|
||||
cls.nan = EmitCompareU32Constant(state, spv::OpUGreaterThan, abs_bits, 0x7f800000u);
|
||||
cls.zero = EmitCompareU32Constant(state, spv::OpIEqual, abs_bits, 0);
|
||||
return cls;
|
||||
}
|
||||
|
||||
F32Class EmitClassifyF32(EmitterState& state, uint32_t value) {
|
||||
return EmitClassifyF32Bits(state, EmitBitcastF32ToU32(state, value));
|
||||
const auto abs_bits = EmitAndConstant(state, bits, 0x7fffffffu);
|
||||
return {EmitCompareU32Constant(state, spv::OpUGreaterThan, abs_bits, 0x7f800000u),
|
||||
EmitCompareU32Constant(state, spv::OpIEqual, abs_bits, 0)};
|
||||
}
|
||||
|
||||
uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit,
|
||||
@@ -100,25 +93,54 @@ uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask) {
|
||||
return EmitClassMaskBits(state, bits, mask, true);
|
||||
}
|
||||
|
||||
static uint32_t EmitMinMaxF32(EmitterState& state, uint32_t lhs, uint32_t rhs, uint32_t lhs_bits,
|
||||
uint32_t rhs_bits, bool max_value) {
|
||||
// RDNA2 V_MIN/V_MAX_F32 return the other source for a NaN source (S1 when both are NaN)
|
||||
// and order -0 below +0. An unordered compare keeps S0 for a NaN S1 and, unlike NMin/NMax,
|
||||
// always returns the exact bits of one source.
|
||||
const auto either = EmitOrU32(state, lhs_bits, rhs_bits);
|
||||
const auto both_zero = EmitCompareU32Constant(state, spv::OpIEqual,
|
||||
EmitAndConstant(state, either, 0x7fffffffu), 0);
|
||||
const auto zero =
|
||||
EmitBitcastU32ToF32(state, max_value ? EmitAndU32(state, lhs_bits, rhs_bits) : either);
|
||||
const auto keep_lhs =
|
||||
Binary(state, max_value ? spv::OpFUnordGreaterThanEqual : spv::OpFUnordLessThan,
|
||||
TypeBool(state), lhs, rhs);
|
||||
const auto ordered = Select(state, TypeF32(state), keep_lhs, lhs, rhs);
|
||||
const auto numeric = Select(state, TypeF32(state), both_zero, zero, ordered);
|
||||
return Select(state, TypeF32(state), Unary(state, spv::OpIsNan, TypeBool(state), lhs), rhs,
|
||||
numeric);
|
||||
}
|
||||
|
||||
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
|
||||
const auto lhs_class = EmitClassifyF32(state, lhs);
|
||||
const auto rhs_class = EmitClassifyF32(state, rhs);
|
||||
return EmitMinMaxF32(state, lhs, rhs, EmitBitcastF32ToU32(state, lhs),
|
||||
EmitBitcastF32ToU32(state, rhs), max_value);
|
||||
}
|
||||
|
||||
const auto numeric_cond = state.builder.AllocateId();
|
||||
state.builder.AddFunction(max_value ? spv::OpFOrdGreaterThanEqual : spv::OpFOrdLessThan,
|
||||
TypeBool(state), numeric_cond, lhs, rhs);
|
||||
const auto ordered_bits =
|
||||
EmitSelectValueU32(state, numeric_cond, lhs_class.bits, rhs_class.bits);
|
||||
|
||||
const auto both_zero = EmitLogicalAndBool(state, lhs_class.zero, rhs_class.zero);
|
||||
const auto zero_bits = max_value ? EmitAndU32(state, lhs_class.bits, rhs_class.bits)
|
||||
: EmitOrU32(state, lhs_class.bits, rhs_class.bits);
|
||||
const auto numeric_bits = EmitSelectValueU32(state, both_zero, zero_bits, ordered_bits);
|
||||
|
||||
const auto rhs_nan_bits =
|
||||
EmitSelectValueU32(state, rhs_class.nan, lhs_class.bits, numeric_bits);
|
||||
const auto result_bits = EmitSelectValueU32(state, lhs_class.nan, rhs_class.bits, rhs_nan_bits);
|
||||
return EmitBitcastU32ToF32(state, result_bits);
|
||||
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
|
||||
const auto a_bits = EmitBitcastF32ToU32(state, a);
|
||||
const auto b_bits = EmitBitcastF32ToU32(state, b);
|
||||
const auto c_bits = EmitBitcastF32ToU32(state, c);
|
||||
const auto min_ab = EmitMinMaxF32(state, a, b, a_bits, b_bits, false);
|
||||
const auto min3 =
|
||||
EmitMinMaxF32(state, min_ab, c, EmitBitcastF32ToU32(state, min_ab), c_bits, false);
|
||||
const auto max_ab = EmitMinMaxF32(state, a, b, a_bits, b_bits, true);
|
||||
const auto max3 =
|
||||
EmitMinMaxF32(state, max_ab, c, EmitBitcastF32ToU32(state, max_ab), c_bits, true);
|
||||
// RDNA2 compares numerically against MAX3, including ties between signed zeros, then takes
|
||||
// V_MAX_F32 of the other two sources in their original order.
|
||||
const auto a_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, a);
|
||||
const auto b_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, b);
|
||||
const auto x = Select(state, TypeF32(state), a_is_max, b, a);
|
||||
const auto y =
|
||||
Select(state, TypeF32(state), EmitLogicalOrBool(state, a_is_max, b_is_max), c, b);
|
||||
const auto median = EmitMinMaxF32Value(state, x, y, true);
|
||||
const auto is_nan = [&](uint32_t value) {
|
||||
return Unary(state, spv::OpIsNan, TypeBool(state), value);
|
||||
};
|
||||
const auto any_nan =
|
||||
EmitLogicalOrBool(state, EmitLogicalOrBool(state, is_nan(a), is_nan(b)), is_nan(c));
|
||||
return Select(state, TypeF32(state), any_nan, min3, median);
|
||||
}
|
||||
|
||||
uint32_t EmitFlushF32DenormToSignedZero(EmitterState& state, uint32_t value) {
|
||||
|
||||
@@ -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(
|
||||
@@ -655,6 +677,14 @@ void EmitStoreCompletion(EmitterState& state) {
|
||||
spv::MemorySemanticsImageMemoryMask));
|
||||
}
|
||||
|
||||
void EmitDataCacheInvalidate(EmitterState& state) {
|
||||
// RDNA2 GL0/GL1 invalidation makes vector memory visible without a workgroup rendezvous.
|
||||
state.builder.AddFunction(
|
||||
spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeDevice),
|
||||
ConstantU32(state, spv::MemorySemanticsAcquireMask | spv::MemorySemanticsUniformMemoryMask |
|
||||
spv::MemorySemanticsImageMemoryMask));
|
||||
}
|
||||
|
||||
void EmitBarrier(EmitterState& state) {
|
||||
const auto tessellation = state.program.stage == ShaderType::TessellationControl;
|
||||
if (!tessellation && ShaderWorkgroupInput(state.program.stage, state.input_info) == nullptr) {
|
||||
|
||||
@@ -97,10 +97,14 @@ uint32_t GatherMip(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memory
|
||||
ConstantF32Value(state, 1.0f / 256.0f));
|
||||
};
|
||||
const auto sampler_control = ctx.Arg(inst, 6);
|
||||
const auto mip_filter = Extract(sampler_control, 26, 2);
|
||||
const auto preclamp = Binary(state, spv::OpINotEqual, TypeBool(state),
|
||||
Extract(sampler_control, 28, 1), ConstantU32(state, 0));
|
||||
const auto half = ConstantF32Value(state, 0.5f);
|
||||
const auto zero = ZeroF32(state);
|
||||
// Point selects the nearest mip; linear gathers return the lower, primary mip only.
|
||||
const auto rounding = Select(state, TypeF32(state),
|
||||
Binary(state, spv::OpIEqual, TypeBool(state), mip_filter, ConstantU32(state, 1)),
|
||||
ConstantF32Value(state, 0.5f), zero);
|
||||
const auto base = Unary(state, spv::OpConvertUToF, TypeF32(state),
|
||||
Extract(ctx.Arg(inst, 4), 12, 4));
|
||||
const auto minimum = Binary(state, spv::OpFSub, TypeF32(state),
|
||||
@@ -108,19 +112,19 @@ uint32_t GatherMip(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memory
|
||||
const auto last = ConstantF32Value(state, static_cast<float>(mip_count - 1u));
|
||||
auto lod = Binary(state, spv::OpFAdd, TypeF32(state),
|
||||
AddressF32(ctx, mem, address, layout.lod),
|
||||
Select(state, TypeF32(state), preclamp, half, zero));
|
||||
Select(state, TypeF32(state), preclamp, rounding, zero));
|
||||
lod = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(
|
||||
state, lod, LodFixed(ctx.Arg(inst, 5), 0), LodFixed(ctx.Arg(inst, 5), 12));
|
||||
// T# minimum LOD is in physical mip space and applies after the S# view-relative clamp.
|
||||
lod = EmitGlsl<GLSLstd450FMax, IR::Type::F32>(state, lod, minimum);
|
||||
lod = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(state, lod, zero, last);
|
||||
lod = Binary(state, spv::OpFAdd, TypeF32(state), lod,
|
||||
Select(state, TypeF32(state), preclamp, zero, half));
|
||||
Select(state, TypeF32(state), preclamp, zero, rounding));
|
||||
// The clamped value is nonnegative, so conversion performs the integer mip rounding.
|
||||
const auto mip = EmitUMin32(state, Unary(state, spv::OpConvertFToU, TypeU32(state), lod),
|
||||
ConstantU32(state, mip_count - 1u));
|
||||
const auto mip_none = Binary(state, spv::OpIEqual, TypeBool(state),
|
||||
Extract(sampler_control, 26, 2), ConstantU32(state, 0));
|
||||
mip_filter, ConstantU32(state, 0));
|
||||
return Select(state, TypeU32(state), mip_none, ConstantU32(state, 0), mip);
|
||||
}
|
||||
|
||||
@@ -284,10 +288,10 @@ uint32_t ResultVector(ValueEmitContext& ctx, uint32_t value,
|
||||
}
|
||||
|
||||
uint32_t QueryDimensions(ValueEmitContext& ctx, const IR::MemoryInfo& mem,
|
||||
const IR::Inst& address) {
|
||||
const auto dimension = ctx.state.program.info.images.at(mem.resource).dimension;
|
||||
const IR::Inst& address, uint32_t resource, uint32_t array_index) {
|
||||
const auto dimension = ctx.state.program.info.images.at(resource).dimension;
|
||||
const auto& info = ImageDimensionInfoFor(dimension);
|
||||
const auto image = LoadImageDescriptor(ctx.state, mem.resource);
|
||||
const auto image = LoadImageDescriptor(ctx.state, resource, 0u, array_index);
|
||||
const auto size = ctx.state.builder.AllocateId();
|
||||
if (info.multisampled != 0u) {
|
||||
ctx.state.builder.AddFunction(spv::OpImageQuerySize,
|
||||
@@ -689,68 +693,58 @@ auto EmitImageAccess(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t resul
|
||||
}
|
||||
const auto selected = EmitIndirectResourceIndex(
|
||||
state, key, image.indirect_mapping_offset, image.indirect_search_iterations, 0u);
|
||||
struct ImageRun {
|
||||
uint32_t first;
|
||||
uint32_t count;
|
||||
uint32_t resource;
|
||||
uint32_t slot_bias;
|
||||
const auto SameType = [&](uint32_t lhs, uint32_t rhs) {
|
||||
const auto& first = state.program.info.images[lhs];
|
||||
const auto& second = state.program.info.images[rhs];
|
||||
return first.dimension == second.dimension && first.cube == second.cube &&
|
||||
IR::DescriptorBindingForImage(first) == IR::DescriptorBindingForImage(second) &&
|
||||
(inst.GetOpcode() != IR::ValueOpcode::ImageWrite ||
|
||||
first.shader_swizzle == second.shader_swizzle) &&
|
||||
first.mip_count == 1u && second.mip_count == 1u;
|
||||
};
|
||||
std::vector<ImageRun> runs;
|
||||
for (uint32_t ordinal = 0; ordinal < image.indirect_resources.size(); ++ordinal) {
|
||||
const auto resource = image.indirect_resources[ordinal];
|
||||
const auto& candidate = state.program.info.images[resource];
|
||||
const auto kind = *IR::DescriptorBindingForImage(candidate);
|
||||
if (!runs.empty()) {
|
||||
auto& run = runs.back();
|
||||
const auto& first = state.program.info.images[run.resource];
|
||||
const auto next_slot = run.slot_bias + ordinal;
|
||||
if (candidate.dimension == first.dimension && candidate.cube == first.cube &&
|
||||
IR::DescriptorBindingForImage(first) == kind &&
|
||||
(inst.GetOpcode() != IR::ValueOpcode::ImageWrite ||
|
||||
candidate.shader_swizzle == first.shader_swizzle) &&
|
||||
candidate.mip_count == 1u && first.mip_count == 1u &&
|
||||
(ordinal == 1u || candidate.descriptor_index == next_slot)) {
|
||||
// Native roots precede appended children; only the root may have a slot gap.
|
||||
if (ordinal == 1u)
|
||||
run.slot_bias = candidate.descriptor_index - ordinal;
|
||||
++run.count;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
runs.push_back({ordinal, 1u, resource,
|
||||
candidate.descriptor_index - ordinal});
|
||||
// Descriptor order is unrelated to image type. Share each typed access and
|
||||
// select its descriptor slot independently, including gaps between native roots.
|
||||
struct ImageGroup { uint32_t resource; uint32_t count; };
|
||||
std::vector<ImageGroup> groups;
|
||||
std::vector<uint32_t> ordinal_groups;
|
||||
for (const auto resource: image.indirect_resources) {
|
||||
const auto found = std::ranges::find_if(groups, [&](const ImageGroup& group) {
|
||||
return SameType(resource, group.resource);
|
||||
});
|
||||
ordinal_groups.push_back(static_cast<uint32_t>(found - groups.begin()));
|
||||
if (found == groups.end()) groups.push_back({resource, 1u});
|
||||
else ++found->count;
|
||||
}
|
||||
const auto EmitRun = [&](uint32_t index) {
|
||||
const auto& run = runs[index];
|
||||
if (run.count == 1u) return emit(run.resource, 0u);
|
||||
auto slot = Binary(state, spv::OpIAdd, TypeU32(state), selected,
|
||||
ConstantU32(state, run.slot_bias));
|
||||
if (run.first == 0u) {
|
||||
const auto root_slot = image.descriptor_index;
|
||||
if (root_slot != run.slot_bias) {
|
||||
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
|
||||
ConstantU32(state, 0u));
|
||||
const auto root_or_child = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), root_or_child,
|
||||
is_root, ConstantU32(state, root_slot), slot);
|
||||
slot = root_or_child;
|
||||
}
|
||||
}
|
||||
return emit(run.resource, slot);
|
||||
};
|
||||
auto run_index = ConstantU32(state, 0u);
|
||||
for (uint32_t index = 1; index < runs.size(); ++index) {
|
||||
const bool dynamic_slot = std::ranges::any_of(groups, [](const ImageGroup& group) {
|
||||
return group.count > 1u;
|
||||
});
|
||||
auto group_index = ConstantU32(state, 0u);
|
||||
auto slot_bias = ConstantU32(state, image.descriptor_index);
|
||||
auto previous_bias = image.descriptor_index;
|
||||
for (uint32_t ordinal = 1; ordinal < image.indirect_resources.size(); ++ordinal) {
|
||||
const auto& candidate = state.program.info.images[image.indirect_resources[ordinal]];
|
||||
const auto bias = candidate.descriptor_index - ordinal;
|
||||
const bool select_group = ordinal_groups[ordinal] != ordinal_groups[ordinal - 1u];
|
||||
const bool select_slot = dynamic_slot && bias != previous_bias;
|
||||
previous_bias = bias;
|
||||
if (!select_group && !select_slot) continue;
|
||||
const auto at_or_after = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state),
|
||||
selected, ConstantU32(state, runs[index].first));
|
||||
const auto next = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), next, at_or_after,
|
||||
ConstantU32(state, index), run_index);
|
||||
run_index = next;
|
||||
selected, ConstantU32(state, ordinal));
|
||||
if (select_group) group_index = Select(state, TypeU32(state), at_or_after,
|
||||
ConstantU32(state, ordinal_groups[ordinal]), group_index);
|
||||
if (select_slot) slot_bias = Select(state, TypeU32(state), at_or_after,
|
||||
ConstantU32(state, bias), slot_bias);
|
||||
}
|
||||
return runs.size() == 1u
|
||||
? EmitRun(0u)
|
||||
: EmitIndexSwitch(state, run_index, static_cast<uint32_t>(runs.size()),
|
||||
result_type, EmitRun);
|
||||
const auto slot = dynamic_slot
|
||||
? Binary(state, spv::OpIAdd, TypeU32(state), selected, slot_bias) : 0u;
|
||||
const auto EmitGroup = [&](uint32_t index) {
|
||||
const auto& group = groups[index];
|
||||
return emit(group.resource, group.count == 1u ? 0u : slot);
|
||||
};
|
||||
return groups.size() == 1u
|
||||
? EmitGroup(0u)
|
||||
: EmitIndexSwitch(state, group_index, static_cast<uint32_t>(groups.size()),
|
||||
result_type, EmitGroup);
|
||||
}
|
||||
|
||||
template <typename Emit>
|
||||
@@ -852,7 +846,10 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
const auto* address = ctx.ImageAddress(inst.Arg(image_info.needs_sampler ? 2 : 1));
|
||||
if (op == IR::ValueOpcode::ImageQueryDimensions) {
|
||||
state.builder.RequireCapability(spv::CapabilityImageQuery);
|
||||
ctx.Define(inst, QueryDimensions(ctx, mem, *address));
|
||||
ctx.Define(inst, EmitImageAccess(ctx, inst, TypeU32Vector(state, 4),
|
||||
[&](uint32_t resource, uint32_t array_index) {
|
||||
return QueryDimensions(ctx, mem, *address, resource, array_index);
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (image_info.needs_sampler &&
|
||||
|
||||
@@ -218,6 +218,7 @@ inline constexpr auto EmitTtraceData = EmitVoid;
|
||||
inline constexpr auto EmitInstPrefetch = EmitVoid;
|
||||
void EmitBarrier(EmitterState& state);
|
||||
void EmitStoreCompletion(EmitterState& state);
|
||||
void EmitDataCacheInvalidate(EmitterState& state);
|
||||
void EmitMeshAllocate(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
uint32_t EmitMeshDrawParameter(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
uint32_t EmitGetTessellationAttribute(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
|
||||
@@ -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;
|
||||
@@ -285,7 +285,6 @@ struct ImageSampleLayout {
|
||||
};
|
||||
|
||||
struct F32Class {
|
||||
uint32_t bits = 0;
|
||||
uint32_t nan = 0;
|
||||
uint32_t zero = 0;
|
||||
};
|
||||
@@ -510,8 +509,6 @@ inline constexpr auto EmitLogicalNotBool = EmitNative<spv::OpLogicalNot, IR::Typ
|
||||
|
||||
F32Class EmitClassifyF32Bits(EmitterState& state, uint32_t bits);
|
||||
|
||||
F32Class EmitClassifyF32(EmitterState& state, uint32_t value);
|
||||
|
||||
uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit,
|
||||
uint32_t class_match);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -233,14 +233,17 @@ void Invoke(Return (*emit)(Context&, Args...), ValueEmitContext& ctx, const IR::
|
||||
}
|
||||
|
||||
void EmitDirectInstruction(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
if (ctx.half != 0 && (inst.GetOpcode() == IR::ValueOpcode::Ballot ||
|
||||
inst.GetOpcode() == IR::ValueOpcode::ReadFirstLane ||
|
||||
inst.GetOpcode() == IR::ValueOpcode::SubgroupUMinU32)) {
|
||||
// These operations combine both emulated halves into one whole-wave result.
|
||||
using Op = IR::ValueOpcode;
|
||||
const auto op = inst.GetOpcode();
|
||||
if (ctx.half != 0 &&
|
||||
(op == Op::Ballot || op == Op::ReadFirstLane || op == Op::SubgroupUMinU32 ||
|
||||
op == Op::ReadConstBuffer ||
|
||||
(op == Op::LoadAddressU32 && ctx.Memory(inst).kind == IR::ResourceKind::ScalarAddress))) {
|
||||
// Whole-wave results and SMEM loads (SGPR operands, no EXEC) are identical in both halves.
|
||||
ctx.Define(inst, ctx.other_half->Result(inst));
|
||||
return;
|
||||
}
|
||||
switch (inst.GetOpcode()) {
|
||||
switch (op) {
|
||||
#define VALUE_OPCODE(name, ...) \
|
||||
case IR::ValueOpcode::name: return Invoke(Emit##name, ctx, inst);
|
||||
#include "graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc"
|
||||
@@ -307,6 +310,7 @@ void EmitBlock(ValueEmitContext& ctx, const IR::Block* block, EmitInstruction&&
|
||||
ctx.state.lane_half = half;
|
||||
if (half == 0 || (inst.GetOpcode() != IR::ValueOpcode::Barrier &&
|
||||
inst.GetOpcode() != IR::ValueOpcode::StoreCompletion &&
|
||||
inst.GetOpcode() != IR::ValueOpcode::DataCacheInvalidate &&
|
||||
inst.GetOpcode() != IR::ValueOpcode::MeshAllocate)) {
|
||||
emit_instruction(lane, inst);
|
||||
}
|
||||
@@ -622,6 +626,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) {
|
||||
@@ -631,7 +641,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) {
|
||||
@@ -646,7 +656,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) {
|
||||
@@ -668,12 +678,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,12 +52,14 @@ 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 {
|
||||
// An HS writes only its own control point but may read any other.
|
||||
const auto vertex =
|
||||
kind == Attribute::ControlOutput
|
||||
kind == Attribute::ControlOutput &&
|
||||
inst.GetOpcode() == IR::ValueOpcode::SetTessellationAttribute
|
||||
? EmitLaneId(state)
|
||||
: EmitBinaryU32(state, spv::OpUDiv, address, ConstantU32(state, stride));
|
||||
state.builder.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, attribute,
|
||||
@@ -60,20 +72,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 +87,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 +117,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);
|
||||
}
|
||||
}
|
||||
@@ -124,7 +128,9 @@ void DefineTessellationInterfaces(EmitterState& state) {
|
||||
|
||||
void DefineTessellationExecutionModes(EmitterState& state) {
|
||||
const auto& tess = state.input_info.vertex->tess;
|
||||
EXIT_NOT_IMPLEMENTED(tess.domain != 1u || tess.partitioning != 2u ||
|
||||
// CxVgtTfParam: triangles, integer or fractional-odd partitioning, clockwise output.
|
||||
EXIT_NOT_IMPLEMENTED(tess.domain != 1u ||
|
||||
(tess.partitioning != 0u && tess.partitioning != 2u) ||
|
||||
tess.output_topology != 2u);
|
||||
state.builder.RequireCapability(spv::CapabilityTessellation);
|
||||
if (state.program.stage == ShaderType::TessellationControl) {
|
||||
@@ -132,7 +138,9 @@ void DefineTessellationExecutionModes(EmitterState& state) {
|
||||
tess.output_control_points);
|
||||
} else {
|
||||
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeTriangles);
|
||||
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeSpacingFractionalOdd);
|
||||
state.builder.AddExecutionMode(
|
||||
state.main_func, tess.partitioning == 0u ? spv::ExecutionModeSpacingEqual
|
||||
: spv::ExecutionModeSpacingFractionalOdd);
|
||||
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeVertexOrderCw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <deque>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <span>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::CFG {
|
||||
@@ -468,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;
|
||||
}
|
||||
@@ -1650,6 +1654,32 @@ Graph BuildGraph(const Decoder::Program& program) {
|
||||
}
|
||||
SortUnique(graph.code_table_load_pcs);
|
||||
|
||||
if (!indirect_setpc && std::ranges::any_of(program.instructions, [](const auto& inst) {
|
||||
return inst.opcode == Opcode::S_SWAPPC_B64 && inst.branch_target != UINT32_MAX;
|
||||
})) {
|
||||
// Linked leaf bodies follow the caller in native PC order. Visit them in
|
||||
// execution order so returning to the caller does not manufacture goto loops.
|
||||
std::vector<bool> visited(graph.blocks.size(), false);
|
||||
std::vector<std::pair<uint32_t, bool>> pending {{graph.entry_block, false}};
|
||||
std::vector<BasicBlock> ordered;
|
||||
ordered.reserve(graph.blocks.size());
|
||||
while (!pending.empty()) {
|
||||
const auto [id, finished] = pending.back();
|
||||
pending.pop_back();
|
||||
if (finished) {
|
||||
ordered.push_back(std::move(graph.blocks[id]));
|
||||
continue;
|
||||
}
|
||||
if (visited[id]) continue;
|
||||
visited[id] = true;
|
||||
pending.emplace_back(id, true);
|
||||
for (const auto successor: graph.blocks[id].successors)
|
||||
pending.emplace_back(successor, false);
|
||||
}
|
||||
std::ranges::reverse(ordered);
|
||||
ApplyBlockOrder(graph, std::move(ordered));
|
||||
}
|
||||
|
||||
RecomputeAnalyses(graph);
|
||||
|
||||
if (indirect_setpc) {
|
||||
@@ -1681,33 +1711,56 @@ Graph Structurize(const Graph& graph) {
|
||||
}
|
||||
|
||||
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code) {
|
||||
uint32_t destination = UINT32_MAX;
|
||||
// Scalar ALU pair widths are not all decoded. The typed producer must confirm this word.
|
||||
return InstructionWritesScalarCode(inst, code) ||
|
||||
(ScalarOperandCode(inst.dst, destination) && code == destination + 1u);
|
||||
// Vector comparisons and carry results can write a scalar pair in wave64.
|
||||
if (inst.family == Decoder::Family::VOP1 || inst.family == Decoder::Family::VOP2 ||
|
||||
inst.family == Decoder::Family::VOPC || inst.family == Decoder::Family::VOP3 ||
|
||||
inst.family == Decoder::Family::VOP3P) {
|
||||
uint32_t destination = 0;
|
||||
for (const auto* operand: {&inst.dst, &inst.dst2})
|
||||
if (ScalarOperandCode(*operand, destination) && code >= destination && code <= destination + 1u)
|
||||
return true;
|
||||
}
|
||||
return InstructionWritesScalarCode(inst, code);
|
||||
}
|
||||
|
||||
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
|
||||
uint32_t before, uint32_t code) {
|
||||
uint32_t before, uint32_t code, std::vector<uint32_t>& crossed_calls) {
|
||||
EXIT_IF(before >= program.instructions.size());
|
||||
const auto found = std::ranges::find_if(graph.blocks, [before](const auto& block) {
|
||||
return block.inst_begin <= before && before < block.inst_end;
|
||||
});
|
||||
EXIT_IF(found == graph.blocks.end());
|
||||
const auto* block = &*found;
|
||||
for (size_t depth = 0; depth < graph.blocks.size(); ++depth) {
|
||||
for (uint32_t i = before; i > block->inst_begin;) {
|
||||
const auto& inst = program.instructions[--i];
|
||||
EXIT_IF(inst.opcode == Opcode::S_SWAPPC_B64);
|
||||
if (MayWriteScalarRegister(inst, code)) return i;
|
||||
}
|
||||
if (block->id == graph.entry_block) return UINT32_MAX;
|
||||
EXIT_IF(block->predecessors.size() != 1u);
|
||||
block = graph.FindBlock(block->predecessors[0]);
|
||||
std::vector<std::pair<uint32_t, uint32_t>> pending {{found->id, before}};
|
||||
std::set<std::pair<uint32_t, uint32_t>> visited;
|
||||
std::optional<uint32_t> definition;
|
||||
while (!pending.empty()) {
|
||||
const auto point = pending.back();
|
||||
pending.pop_back();
|
||||
if (!visited.insert(point).second) continue;
|
||||
const auto* block = graph.FindBlock(point.first);
|
||||
EXIT_IF(block == nullptr);
|
||||
before = block->inst_end;
|
||||
uint32_t index = UINT32_MAX;
|
||||
for (uint32_t i = point.second; i > block->inst_begin;) {
|
||||
const auto& inst = program.instructions[--i];
|
||||
if (inst.opcode == Opcode::S_SWAPPC_B64) {
|
||||
EXIT_NOT_IMPLEMENTED(MayWriteScalarRegister(inst, code));
|
||||
crossed_calls.push_back(i);
|
||||
continue;
|
||||
}
|
||||
if (MayWriteScalarRegister(inst, code)) { index = i; break; }
|
||||
}
|
||||
if (index != UINT32_MAX || block->id == graph.entry_block) {
|
||||
EXIT_IF(definition && *definition != index);
|
||||
definition = index;
|
||||
continue;
|
||||
}
|
||||
EXIT_IF(block->predecessors.empty());
|
||||
for (const auto predecessor: block->predecessors) {
|
||||
pending.emplace_back(predecessor, graph.blocks[predecessor].inst_end);
|
||||
}
|
||||
}
|
||||
EXIT("scalar shader call source has cyclic reaching definitions");
|
||||
EXIT_IF(!definition);
|
||||
return *definition;
|
||||
}
|
||||
|
||||
std::string BranchConditionToString(BranchCondition condition) {
|
||||
|
||||
@@ -115,7 +115,7 @@ struct Graph {
|
||||
Graph BuildGraph(const Decoder::Program& program);
|
||||
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code);
|
||||
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
|
||||
uint32_t before, uint32_t code);
|
||||
uint32_t before, uint32_t code, std::vector<uint32_t>& crossed_calls);
|
||||
// Returns structured control flow or failure diagnostics without changing the native graph.
|
||||
// On failure, failure_block is an original block ID or UINT32_MAX.
|
||||
Graph Structurize(const Graph& graph);
|
||||
|
||||
@@ -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[] = {
|
||||
|
||||
@@ -70,6 +70,8 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
|
||||
{0x52u, Opcode::BUFFER_ATOMIC_ADD_X2, 2, 32},
|
||||
{0x59u, Opcode::BUFFER_ATOMIC_AND_X2, 2, 32},
|
||||
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
|
||||
{0x71u, Opcode::BUFFER_GL0_INV, 0, 0},
|
||||
{0x72u, Opcode::BUFFER_GL1_INV, 0, 0},
|
||||
{0x80u, Opcode::BUFFER_LOAD_FORMAT_D16_X, 1, 16, false, false, true},
|
||||
{0x83u, Opcode::BUFFER_LOAD_FORMAT_D16_XYZW, 2, 16, false, false, true},
|
||||
{0x84u, Opcode::BUFFER_STORE_FORMAT_D16_X, 1, 16, false, false, true},
|
||||
@@ -164,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:
|
||||
@@ -182,6 +186,8 @@ bool IsDsWriteOpcode(Opcode opcode) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsDsAtomicOpcode(Opcode opcode) {
|
||||
switch (opcode) {
|
||||
case Opcode::DS_ADD_U32:
|
||||
@@ -312,6 +318,9 @@ void DecodeMubuf(uint32_t pc, std::span<const uint32_t> code, uint32_t word_inde
|
||||
if (inst.opcode == Opcode::UNSUPPORTED) {
|
||||
SetUnsupported(inst, Family::MUBUF, opcode, "MUBUF opcode is not implemented");
|
||||
}
|
||||
if (inst.opcode == Opcode::BUFFER_GL0_INV || inst.opcode == Opcode::BUFFER_GL1_INV) {
|
||||
return;
|
||||
}
|
||||
|
||||
DecodeVectorGpr(vdata, inst.dst);
|
||||
if (inst.opcode == Opcode::BUFFER_LOAD_FORMAT_D16_XYZW ||
|
||||
|
||||
@@ -164,9 +164,23 @@ void DecodeSop1(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
switch (inst.opcode) {
|
||||
case Opcode::S_SWAPPC_B64:
|
||||
inst.branch_target = UINT32_MAX;
|
||||
inst.data_dwords = 2;
|
||||
[[fallthrough]];
|
||||
case Opcode::S_MOV_B64:
|
||||
case Opcode::S_CMOV_B64:
|
||||
case Opcode::S_NOT_B64:
|
||||
case Opcode::S_WQM_B64:
|
||||
case Opcode::S_BREV_B64:
|
||||
case Opcode::S_BITSET0_B64:
|
||||
case Opcode::S_BITSET1_B64:
|
||||
case Opcode::S_AND_SAVEEXEC_B64:
|
||||
case Opcode::S_ORN2_SAVEEXEC_B64:
|
||||
case Opcode::S_QUADMASK_B64:
|
||||
case Opcode::S_ANDN1_SAVEEXEC_B64:
|
||||
case Opcode::S_BITREPLICATE_B64_B32:
|
||||
inst.data_dwords = 2u;
|
||||
break;
|
||||
case Opcode::S_GETPC_B64:
|
||||
inst.data_dwords = 2u;
|
||||
inst.src_count = 0;
|
||||
DecodeScalarDestination(sdst, pc, inst.dst);
|
||||
return;
|
||||
@@ -206,6 +220,8 @@ void DecodeSop2(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
|
||||
DecodeScalarDestination(sdst, pc, inst.dst);
|
||||
DecodeBinarySources(pc, code, word_index, inst, ssrc0, ssrc1);
|
||||
if (opcode == 0x0bu || (opcode >= 0x0fu && opcode <= 0x25u && (opcode & 1u)) || opcode == 0x29u)
|
||||
inst.data_dwords = 2u;
|
||||
}
|
||||
|
||||
void DecodeSopk(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
|
||||
|
||||
@@ -566,6 +566,9 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
OperandToString(inst.src0).c_str()));
|
||||
case Opcode::S_BARRIER:
|
||||
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: s_barrier", inst.pc));
|
||||
case Opcode::BUFFER_GL0_INV:
|
||||
case Opcode::BUFFER_GL1_INV:
|
||||
return fmt::format("0x{:08x}: {}", inst.pc, magic_enum::enum_name(inst.opcode));
|
||||
case Opcode::V_NOP:
|
||||
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: v_nop", inst.pc));
|
||||
case Opcode::S_ENDPGM:
|
||||
|
||||
@@ -599,6 +599,8 @@ enum class Opcode {
|
||||
BUFFER_ATOMIC_XOR,
|
||||
BUFFER_ATOMIC_FMIN,
|
||||
BUFFER_ATOMIC_FMAX,
|
||||
BUFFER_GL0_INV,
|
||||
BUFFER_GL1_INV,
|
||||
FLAT_LOAD_UBYTE,
|
||||
FLAT_LOAD_SBYTE,
|
||||
FLAT_LOAD_USHORT,
|
||||
@@ -880,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);
|
||||
|
||||
@@ -940,6 +940,10 @@ void Translator::DS_PERMUTE(const Decoder::Instruction& inst, bool backward) {
|
||||
|
||||
void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
switch (inst.opcode) {
|
||||
case Decoder::Opcode::BUFFER_GL0_INV:
|
||||
case Decoder::Opcode::BUFFER_GL1_INV:
|
||||
ir.Emit(IR::ValueOpcode::DataCacheInvalidate);
|
||||
return;
|
||||
case Decoder::Opcode::IMAGE_BVH_INTERSECT_RAY: {
|
||||
const auto result = ir.Emit(IR::ValueOpcode::BvhIntersect,
|
||||
{ConstructU32x4(inst.src1, 4), MakeImageAddress(inst, inst.src0), ir.GetExec()},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -125,6 +125,7 @@ struct BufferResource {
|
||||
uint32_t descriptor_index = UINT32_MAX;
|
||||
uint32_t first_use_pc = 0;
|
||||
uint32_t max_byte_extent = 0;
|
||||
uint32_t write_extent = 0; // UINT32_MAX once a write is runtime-addressed or formatted
|
||||
uint32_t packed_stride = 0;
|
||||
Prospero::BufferFormat descriptor_format = Prospero::BufferFormat::kInvalid;
|
||||
uint32_t descriptor_swizzle = DstSel(4, 5, 6, 7);
|
||||
@@ -220,8 +221,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 +360,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);
|
||||
}
|
||||
|
||||
@@ -495,7 +506,6 @@ struct ShaderInfo {
|
||||
static constexpr uint32_t MaxBufferRoots = 64;
|
||||
static constexpr uint32_t MaxImages = 64;
|
||||
static constexpr uint32_t MaxSamplers = 32;
|
||||
static constexpr uint32_t MaxSampledPairs = 64;
|
||||
|
||||
std::vector<BufferResource> buffers;
|
||||
std::vector<ImageResource> images;
|
||||
|
||||
@@ -94,6 +94,7 @@ bool HasSideEffects(ValueOpcode opcode) {
|
||||
case ValueOpcode::SetTessellationAttribute:
|
||||
case ValueOpcode::MeshAllocate:
|
||||
case ValueOpcode::StoreCompletion:
|
||||
case ValueOpcode::DataCacheInvalidate:
|
||||
case ValueOpcode::Barrier: return true;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
@@ -350,6 +350,7 @@ VALUE_OPCODE(SetAttribute, Void, U32x4, U1)
|
||||
|
||||
VALUE_OPCODE(ControlNop, Void)
|
||||
VALUE_OPCODE(StoreCompletion, Void)
|
||||
VALUE_OPCODE(DataCacheInvalidate, Void)
|
||||
VALUE_OPCODE(Barrier, Void)
|
||||
VALUE_OPCODE(Sendmsg, Void)
|
||||
VALUE_OPCODE(TtraceData, Void)
|
||||
|
||||
@@ -1029,6 +1029,19 @@ void ConstantPropagationPass(const BlockList& blocks, uint32_t wave_size) {
|
||||
}
|
||||
drain();
|
||||
} while (normalized);
|
||||
// Repeated register reads share a bitcast only within the same SSA block.
|
||||
std::unordered_map<const Inst*, Inst*> bitcasts;
|
||||
for (auto* block: blocks) {
|
||||
bitcasts.clear();
|
||||
for (auto& inst: *block) {
|
||||
if (inst.GetOpcode() != ValueOpcode::BitCastF32U32 &&
|
||||
inst.GetOpcode() != ValueOpcode::BitCastU32F32) continue;
|
||||
const auto* source = Arg(inst, 0).TryInstruction();
|
||||
if (source == nullptr) continue;
|
||||
const auto [entry, inserted] = bitcasts.try_emplace(source, &inst);
|
||||
if (!inserted) Replace(inst, Value(entry->second));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
@@ -30,32 +30,62 @@ void RemoveIdentities(Program& program) {
|
||||
}
|
||||
|
||||
void EliminateDeadCode(const BlockList& blocks) {
|
||||
std::vector<Inst*> pending;
|
||||
for (auto* block: blocks) {
|
||||
for (auto& inst: *block) {
|
||||
// Retained resource-planning instructions live outside the block list.
|
||||
inst.live = inst.MayHaveSideEffects() || std::ranges::any_of(inst.Uses(),
|
||||
[](const Use& use) { return use.user->Parent() == nullptr; });
|
||||
if (inst.live) pending.push_back(&inst);
|
||||
}
|
||||
}
|
||||
while (!pending.empty()) {
|
||||
const auto* inst = pending.back();
|
||||
pending.pop_back();
|
||||
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
|
||||
auto* input = inst->Arg(arg).TryInstruction();
|
||||
if (input != nullptr && input->Parent() != nullptr && !input->live) {
|
||||
input->live = true;
|
||||
pending.push_back(input);
|
||||
bool changed;
|
||||
do {
|
||||
std::vector<Inst*> pending;
|
||||
for (auto* block: blocks) {
|
||||
for (auto& inst: *block) {
|
||||
// Retained resource-planning instructions live outside the block list.
|
||||
inst.live = inst.MayHaveSideEffects() || std::ranges::any_of(inst.Uses(),
|
||||
[](const Use& use) { return use.user->Parent() == nullptr; });
|
||||
if (inst.live) pending.push_back(&inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Unlink every dead operand before freeing nodes in a dead Phi cycle.
|
||||
for (auto* block: blocks)
|
||||
for (auto& inst: *block)
|
||||
if (!inst.live) inst.Invalidate();
|
||||
for (auto* block: blocks)
|
||||
std::erase_if(block->Instructions(), [](const Inst& inst) { return !inst.live; });
|
||||
while (!pending.empty()) {
|
||||
const auto* inst = pending.back();
|
||||
pending.pop_back();
|
||||
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
|
||||
auto* input = inst->Arg(arg).TryInstruction();
|
||||
if (input != nullptr && input->Parent() != nullptr && !input->live) {
|
||||
input->live = true;
|
||||
pending.push_back(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Unlink every dead operand before freeing nodes in a dead Phi cycle.
|
||||
for (auto* block: blocks)
|
||||
for (auto& inst: *block)
|
||||
if (!inst.live) inst.Invalidate();
|
||||
for (auto* block: blocks)
|
||||
std::erase_if(block->Instructions(), [](const Inst& inst) { return !inst.live; });
|
||||
|
||||
changed = false;
|
||||
for (auto* block: blocks) {
|
||||
const auto& term = block->terminator;
|
||||
if (term.kind != CFG::TerminatorKind::ConditionalBranch || term.loop_header ||
|
||||
block->condition.IsImmediate()) continue;
|
||||
for (const bool take_true: {false, true}) {
|
||||
const auto* empty = take_true ? term.false_block : term.true_block;
|
||||
const auto* merge = take_true ? term.true_block : term.false_block;
|
||||
if (empty == nullptr || merge == nullptr || !empty->empty() ||
|
||||
empty->terminator.loop_header || merge->terminator.loop_header ||
|
||||
empty->terminator.kind != CFG::TerminatorKind::Branch ||
|
||||
empty->terminator.true_block != merge ||
|
||||
std::ranges::any_of(*merge, [](const Inst& inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::Phi;
|
||||
})) continue;
|
||||
// Keep the CFG and SSA edges intact; only the empty detour's predicate is dead.
|
||||
for (auto& inst: *block) {
|
||||
if (inst.GetOpcode() == ValueOpcode::Reference &&
|
||||
inst.Arg(0).Resolve() == block->condition.Resolve())
|
||||
inst.SetArg(0, Value(take_true));
|
||||
}
|
||||
block->condition = Value(take_true);
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (changed);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
@@ -292,11 +292,23 @@ bool NormalizeIndirectBuffer(DescriptorValue& value, bool indexed, const BufferR
|
||||
bool IsBoundedDescriptorTable(const ResourcePlan& program,
|
||||
const DescriptorSource::IndirectDescriptor& indirect) {
|
||||
const auto* table = Source(program, indirect.table_source);
|
||||
return indirect.sources.empty() && !indirect.selector &&
|
||||
return indirect.sources.empty() && (!indirect.selector || indirect.table_scalar) &&
|
||||
(indirect.key_count.IsEmpty() || indirect.table_maximum) && indirect.workgroup_axis == UINT32_MAX &&
|
||||
table != nullptr && table->dword_count == 4u;
|
||||
}
|
||||
|
||||
bool CanReadIndexedSelector(const ShaderBufferResource& material, uint32_t offset) {
|
||||
if (material.Type() != 0u || material.SwizzleEnabled() || material.AddTid() ||
|
||||
material.OutOfBounds() != 0u) return false;
|
||||
if (material.Format() == Prospero::BufferFormat::kInvalid || material.NumRecords() == 0u ||
|
||||
offset >= material.Stride()) return true;
|
||||
return material.Base48() != 0u &&
|
||||
((material.Base48() | material.Stride() | offset) & 3u) == 0u &&
|
||||
material.NumRecords() <= MaxIndirectDescriptorProbes &&
|
||||
uint64_t {material.NumRecords() - 1u} * material.Stride() + offset + 4u <=
|
||||
uint64_t {UINT32_MAX} + 1u;
|
||||
}
|
||||
|
||||
bool EvaluateIndirectCount(const DescriptorSource::IndirectDescriptor& indirect,
|
||||
const SrtRuntime& runtime, SrtWalker& clean, uint32_t& count) {
|
||||
if (indirect.workgroup_axis != UINT32_MAX) {
|
||||
@@ -490,6 +502,15 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
|
||||
keys.push_back(key);
|
||||
mask &= mask - 1u;
|
||||
}
|
||||
} else if (selector->stride == 0u) {
|
||||
ShaderBufferResource material;
|
||||
if (!DecodeBufferDescriptor(material_value, material) ||
|
||||
!CanReadIndexedSelector(material, selector->offset)) return false;
|
||||
if (material.Format() != Prospero::BufferFormat::kInvalid && material.NumRecords() != 0u &&
|
||||
selector->offset < material.Stride() &&
|
||||
!read_keys(material.Base48(), material.GetSize(), selector->offset,
|
||||
material.Stride(), material.NumRecords())) return false;
|
||||
keys.push_back(0u); // Every out-of-bounds indexed load returns zero.
|
||||
} else {
|
||||
ShaderBufferResource material;
|
||||
if (!DecodeBufferDescriptor(material_value, material) || selector->stride < 4u ||
|
||||
@@ -524,29 +545,29 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
ResourceSnapshot& snapshot,
|
||||
std::vector<DescriptorValue>& descriptors,
|
||||
std::vector<Specialization>& specializations,
|
||||
uint32_t maximum_resources, Normalize&& normalize) {
|
||||
Normalize&& normalize) {
|
||||
const auto& indirect = *program.descriptor_sources[source_index].indirect_descriptor;
|
||||
const auto descriptor_bytes = read_dwords * sizeof(uint32_t);
|
||||
const auto& sources = indirect.sources;
|
||||
const auto* selector = indirect.selector ? &*indirect.selector : nullptr;
|
||||
const bool byte_keys = IsBoundedDescriptorTable(program, indirect);
|
||||
auto* selector_keys = selector != nullptr
|
||||
? &program.selector_keys[program.selector_groups[source_index]] : nullptr;
|
||||
auto& keys = selector_keys != nullptr ? selector_keys->values : program.material_keys;
|
||||
auto& byte_offsets = program.material_keys;
|
||||
const auto children_begin = descriptors.size();
|
||||
const auto root_resource = specializations[resource_index];
|
||||
const auto intern_candidate = [&](const DescriptorValue& candidate, uint32_t& ordinal) {
|
||||
ordinal = 0u;
|
||||
if (candidate == descriptors[resource_index]) return true;
|
||||
const auto intern_candidate = [&](const DescriptorValue& candidate) {
|
||||
if (candidate == descriptors[resource_index]) return 0u;
|
||||
const auto found = std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
|
||||
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
|
||||
const auto ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
|
||||
if (found == descriptors.end()) {
|
||||
if (descriptors.size() >= maximum_resources) return false;
|
||||
descriptors.push_back(candidate);
|
||||
auto child = root_resource;
|
||||
child.indirect_root = resource_index;
|
||||
specializations.push_back(child);
|
||||
}
|
||||
return true;
|
||||
return ordinal;
|
||||
};
|
||||
uint64_t table_base = 0;
|
||||
uint64_t table_size = UINT64_MAX; // Scalar addresses have no buffer descriptor bounds.
|
||||
@@ -558,7 +579,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
"indirect {} resource={} source={} table_source={} words={:08x},{:08x},{:08x},{:08x} "
|
||||
"base={:#x} size={} type={} runtime_stride={} records={} swizzle={} addtid={} oob={} "
|
||||
"maximum={} offset={} immediate={} record_bytes={} child_count={} child_stride={} "
|
||||
"child_offset={} indexed_pointer={} candidates={} capacity={} {}",
|
||||
"child_offset={} indexed_pointer={} candidates={} {}",
|
||||
reason, resource_index, source_index, indirect.table_source,
|
||||
table_value.dwords[0], table_value.dwords[1], table_value.dwords[2], table_value.dwords[3],
|
||||
table_base, table_size, table.Type(), table.Stride(), table.NumRecords(),
|
||||
@@ -567,7 +588,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
indirect.table_offset, indirect.table_immediate, indirect.table_record_bytes,
|
||||
pointer != nullptr ? pointer->count : 1u, pointer != nullptr ? pointer->stride : 0u,
|
||||
pointer != nullptr ? pointer->offset : 0u, pointer != nullptr && pointer->indexed,
|
||||
descriptors.size(), maximum_resources, details));
|
||||
descriptors.size(), details));
|
||||
};
|
||||
const auto read_failed = [&](std::string_view reason, uint32_t row, uint32_t child,
|
||||
uint64_t base, uint64_t offset, size_t bytes) {
|
||||
@@ -583,7 +604,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
if (sources.empty()) {
|
||||
if (selector == nullptr) keys.clear();
|
||||
DescriptorValue material_value;
|
||||
if ((selector != nullptr && !selector_keys->resolved &&
|
||||
if ((selector != nullptr && (byte_keys || !selector_keys->resolved) &&
|
||||
!clean.EvaluateDescriptor(selector->source, material_value)) ||
|
||||
!clean.EvaluateDescriptor(indirect.table_source, table_value)) {
|
||||
return false;
|
||||
@@ -596,16 +617,40 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (IsBoundedDescriptorTable(program, indirect)) {
|
||||
if (byte_keys && table_size > UINT32_MAX) return failed("table geometry");
|
||||
bool use_selector = selector != nullptr;
|
||||
if (byte_keys && selector != nullptr) {
|
||||
ShaderBufferResource material;
|
||||
if (!DecodeBufferDescriptor(material_value, material)) return false;
|
||||
use_selector = CanReadIndexedSelector(material, selector->offset);
|
||||
if (use_selector && material.Format() != Prospero::BufferFormat::kInvalid &&
|
||||
material.NumRecords() != 0u && selector->offset < material.Stride()) {
|
||||
const auto begin = material.Base48() + selector->offset;
|
||||
const auto end = begin + uint64_t {material.NumRecords() - 1u} * material.Stride() + 4u;
|
||||
for (uint32_t i = 0; i < program.info.buffers.size(); ++i) {
|
||||
const auto& buffer = program.info.buffers[i];
|
||||
if (!buffer.written) continue;
|
||||
ShaderBufferResource written;
|
||||
if (program.descriptor_sources[buffer.source].indirect_descriptor ||
|
||||
!DecodeBufferDescriptor(snapshot.buffers[i], written) ||
|
||||
(written.Base48() != 0u && written.GetSize() != 0u &&
|
||||
begin < written.Base48() + written.GetSize() &&
|
||||
written.Base48() < end)) {
|
||||
use_selector = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (byte_keys && !use_selector) {
|
||||
if (!indirect.table_scalar && table.Format() == Prospero::BufferFormat::kInvalid) {
|
||||
descriptors[resource_index] = {.dword_count = dword_count};
|
||||
return true;
|
||||
}
|
||||
if (table_size > UINT32_MAX ||
|
||||
(indirect.table_record_bytes != 0u &&
|
||||
if (indirect.table_record_bytes != 0u &&
|
||||
(table.Type() != 0u || table.SwizzleEnabled() || table.AddTid() ||
|
||||
table.OutOfBounds() != 0u || (table_base & 3u) != 0u ||
|
||||
(table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes)))
|
||||
(table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes))
|
||||
return failed("table geometry");
|
||||
const auto* pointer = indirect.pointer_table ? &*indirect.pointer_table : nullptr;
|
||||
const bool indexed_pointer = pointer != nullptr && pointer->indexed;
|
||||
@@ -640,10 +685,11 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
return failed("candidate count", fmt::format("rows={} limit={}", count, MaxIndirectDescriptorProbes));
|
||||
// Affine SMEM offsets wrap in U32 before adding the aligned immediate.
|
||||
// Image heaps retain one bulk snapshot and their absolute byte-offset mapping.
|
||||
auto& heap_words = program.material_keys;
|
||||
if (image_heap) {
|
||||
keys.resize(table_size / 4u);
|
||||
if (!ReadScalarTable(table_base, table_size, 0u, runtime, keys))
|
||||
return read_failed("image heap read", 0u, 0u, table_base, 0u, keys.size() * sizeof(uint32_t));
|
||||
heap_words.resize(table_size / 4u);
|
||||
if (!ReadScalarTable(table_base, table_size, 0u, runtime, heap_words))
|
||||
return read_failed("image heap read", 0u, 0u, table_base, 0u, heap_words.size() * sizeof(uint32_t));
|
||||
}
|
||||
const auto mapping_offset = snapshot.flattened_srt.size();
|
||||
descriptors[resource_index] = {.dword_count = dword_count};
|
||||
@@ -670,15 +716,14 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
std::span(candidate.dwords).first(read_dwords))) return false;
|
||||
} else {
|
||||
const auto table_word = table_offset / 4u;
|
||||
std::copy_n(keys.begin() + table_word,
|
||||
std::min<size_t>(read_dwords, keys.size() - table_word),
|
||||
std::copy_n(heap_words.begin() + table_word,
|
||||
std::min<size_t>(read_dwords, heap_words.size() - table_word),
|
||||
candidate.dwords.begin());
|
||||
}
|
||||
if (!normalize(candidate))
|
||||
return normalization_failed(table_offset, word, step,
|
||||
indirect.table_maximum ? "key" : "probe");
|
||||
uint32_t ordinal;
|
||||
if (!intern_candidate(candidate, ordinal)) return false;
|
||||
const auto ordinal = intern_candidate(candidate);
|
||||
if (ordinal == 0u) continue;
|
||||
const auto mapping_word = image_heap ? table_offset / 4u : word * child_count + child;
|
||||
snapshot.flattened_srt.resize(mapping_offset + 2u + mapping_word, 0u);
|
||||
@@ -722,21 +767,38 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
selector_keys->resolved = true;
|
||||
}
|
||||
}
|
||||
if (byte_keys) {
|
||||
// Keep cached selector values independent of each destination table's
|
||||
// U32 arithmetic, and use the same byte keys as the full-heap GPU path.
|
||||
byte_offsets.clear();
|
||||
byte_offsets.reserve(keys.size());
|
||||
for (const auto key: keys) {
|
||||
const auto offset = uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
|
||||
(indirect.table_immediate & ~3u);
|
||||
byte_offsets.push_back(offset <= UINT32_MAX ? static_cast<uint32_t>(offset) : UINT32_MAX);
|
||||
}
|
||||
std::ranges::sort(byte_offsets);
|
||||
byte_offsets.erase(std::unique(byte_offsets.begin(), byte_offsets.end()), byte_offsets.end());
|
||||
}
|
||||
}
|
||||
|
||||
const auto mapping_offset = snapshot.flattened_srt.size();
|
||||
const auto key_count = sources.empty() ? keys.size() : sources.size();
|
||||
const auto selected_keys = byte_keys ? std::span<const uint32_t>(byte_offsets)
|
||||
: std::span<const uint32_t>(keys);
|
||||
const auto key_count = sources.empty() ? selected_keys.size() : sources.size();
|
||||
snapshot.flattened_srt.resize(mapping_offset + 1u + key_count * 2u);
|
||||
snapshot.flattened_srt[mapping_offset] = static_cast<uint32_t>(key_count);
|
||||
for (uint32_t entry = 0; entry < key_count; ++entry) {
|
||||
const auto key = sources.empty() ? keys[entry] : entry;
|
||||
const auto key = sources.empty() ? selected_keys[entry] : entry;
|
||||
uint64_t table_offset = 0;
|
||||
DescriptorValue candidate;
|
||||
candidate.dword_count = dword_count;
|
||||
if (sources.empty()) {
|
||||
table_offset = uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
|
||||
(indirect.table_immediate & ~3u);
|
||||
if (!ReadScalarTable(table_base, table_size, table_offset, runtime,
|
||||
table_offset = byte_keys ? key
|
||||
: uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
|
||||
(indirect.table_immediate & ~3u);
|
||||
if (!(byte_keys && key == UINT32_MAX) &&
|
||||
!ReadScalarTable(table_base, table_size, table_offset, runtime,
|
||||
std::span(candidate.dwords).first(read_dwords))) {
|
||||
return read_failed("table descriptor read", key, 0u, table_base, table_offset, descriptor_bytes);
|
||||
}
|
||||
@@ -746,10 +808,10 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
if (!normalize(candidate))
|
||||
return normalization_failed(table_offset, key, indirect.table_stride, "key");
|
||||
uint32_t ordinal = 0;
|
||||
if (entry == 0) {
|
||||
if (entry == 0 && !byte_keys) {
|
||||
descriptors[resource_index] = candidate;
|
||||
} else if (!intern_candidate(candidate, ordinal)) {
|
||||
return false;
|
||||
} else {
|
||||
ordinal = intern_candidate(candidate);
|
||||
}
|
||||
snapshot.flattened_srt[mapping_offset + 1u + entry * 2u] = key;
|
||||
snapshot.flattened_srt[mapping_offset + 2u + entry * 2u] = ordinal;
|
||||
@@ -772,9 +834,8 @@ struct SamplerPlan {
|
||||
uint32_t source;
|
||||
SamplerClass type;
|
||||
};
|
||||
std::array<std::array<uint32_t, 2>, ShaderInfo::MaxSamplers> mapping;
|
||||
std::array<Binding, ShaderInfo::MaxSamplers> bindings;
|
||||
uint32_t sampler_count = 0;
|
||||
std::vector<std::array<uint32_t, 2>> mapping;
|
||||
std::vector<Binding> bindings;
|
||||
};
|
||||
|
||||
template <typename T, typename Keep>
|
||||
@@ -913,6 +974,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
||||
if (i >= program.info.images.size() && source != nullptr &&
|
||||
source->indirect_descriptor &&
|
||||
IsBoundedDescriptorTable(program, *source->indirect_descriptor) &&
|
||||
specialization.images[base_index].indirect_search_iterations == 0u &&
|
||||
!source->indirect_descriptor->pointer_table) {
|
||||
const auto& indirect = *source->indirect_descriptor;
|
||||
const size_t mapping = specialization.images[base_index].indirect_mapping_offset;
|
||||
@@ -997,7 +1059,6 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
||||
is_1d_or_2d(image_class.dimension);
|
||||
if ((!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension)) &&
|
||||
!unfiltered_coordinates) ||
|
||||
image.mip_count != image_class.mip_count ||
|
||||
image.conversion_format != image_class.conversion_format ||
|
||||
(!storage_swizzle && image.shader_swizzle != image_class.shader_swizzle)) {
|
||||
return SpecializationFail(
|
||||
@@ -1011,11 +1072,9 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
||||
}
|
||||
|
||||
bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
|
||||
if (base.samplers.size() > plan.mapping.size()) {
|
||||
return false;
|
||||
}
|
||||
std::array<uint8_t, ShaderInfo::MaxSamplers> usage {};
|
||||
plan.sampler_count = static_cast<uint32_t>(base.samplers.size());
|
||||
plan.mapping.resize(base.samplers.size());
|
||||
plan.bindings.resize(base.samplers.size());
|
||||
std::vector<uint8_t> usage(base.samplers.size());
|
||||
for (const auto& pair: base.sampled_pairs) {
|
||||
if (pair.image >= base.images.size() || pair.sampler >= base.samplers.size()) {
|
||||
return false;
|
||||
@@ -1037,8 +1096,8 @@ bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
|
||||
bool first = true;
|
||||
for (uint32_t type = 0; type < mapping.size(); type++) {
|
||||
if ((classes & (1u << type)) == 0u) continue;
|
||||
const auto target = first ? index : plan.sampler_count++;
|
||||
if (target >= ShaderInfo::MaxSamplers) return false;
|
||||
const auto target = first ? index : static_cast<uint32_t>(plan.bindings.size());
|
||||
if (!first) plan.bindings.emplace_back();
|
||||
mapping[type] = target;
|
||||
plan.bindings[target] = {index, static_cast<SamplerClass>(type)};
|
||||
first = false;
|
||||
@@ -1544,9 +1603,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
return true;
|
||||
};
|
||||
const auto resource_failed = [&](std::string_view kind, uint32_t resource, uint32_t source,
|
||||
size_t candidates, uint32_t capacity) {
|
||||
return SpecializationFail(fmt::format("indirect {} resource={} source={} candidates={} capacity={}",
|
||||
kind, resource, source, candidates, capacity));
|
||||
size_t candidates) {
|
||||
return SpecializationFail(fmt::format("indirect {} resource={} source={} candidates={}",
|
||||
kind, resource, source, candidates));
|
||||
};
|
||||
snapshot.buffers.resize(program.info.buffers.size());
|
||||
specialization.buffers.assign(program.info.buffers.size(), {});
|
||||
@@ -1563,12 +1622,12 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
if (active.empty() || active[base.source]) {
|
||||
if (!MaterializeIndirectDescriptor(
|
||||
program, base.source, i, 4u, 4u, observed, clean, snapshot,
|
||||
snapshot.buffers, specialization.buffers, UINT32_MAX,
|
||||
snapshot.buffers, specialization.buffers,
|
||||
[&](DescriptorValue& value) {
|
||||
return NormalizeIndirectBuffer(
|
||||
value, source->indirect_descriptor->indexed, base);
|
||||
}))
|
||||
return resource_failed("buffer", i, base.source, snapshot.buffers.size(), UINT32_MAX);
|
||||
return resource_failed("buffer", i, base.source, snapshot.buffers.size());
|
||||
}
|
||||
} else if (!evaluate(base.source, snapshot.buffers[i], base.written)) {
|
||||
return false;
|
||||
@@ -1627,14 +1686,14 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
const bool bounded_table = IsBoundedDescriptorTable(program, indirect);
|
||||
if (!MaterializeIndirectDescriptor(
|
||||
program, image.source, i, 8u, image.r128 ? 4u : 8u, observed, clean, snapshot, snapshot.images,
|
||||
specialization.images, UINT32_MAX, [&](DescriptorValue& value) {
|
||||
specialization.images, [&](DescriptorValue& value) {
|
||||
// A broad heap also contains resources for other typed image operations.
|
||||
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128) ||
|
||||
(bounded_table && DescriptorDimension(value, image.dimension) != image.dimension))
|
||||
value.dwords.fill(0u);
|
||||
return true;
|
||||
})) {
|
||||
return resource_failed("image", i, image.source, snapshot.images.size(), UINT32_MAX);
|
||||
return resource_failed("image", i, image.source, snapshot.images.size());
|
||||
}
|
||||
} else {
|
||||
if (!evaluate(image.source, snapshot.images[i], image.written)) {
|
||||
@@ -1660,9 +1719,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
snapshot.samplers[i] = {.dword_count = 4u};
|
||||
if ((active.empty() || active[source]) && !MaterializeIndirectDescriptor(
|
||||
program, source, i, 4u, 4u, observed, clean, snapshot, snapshot.samplers,
|
||||
specialization.samplers, ShaderInfo::MaxSamplers,
|
||||
specialization.samplers,
|
||||
[](DescriptorValue&) { return true; }))
|
||||
return resource_failed("sampler", i, source, snapshot.samplers.size(), ShaderInfo::MaxSamplers);
|
||||
return resource_failed("sampler", i, source, snapshot.samplers.size());
|
||||
} else {
|
||||
if (indirect != nullptr) source = indirect->sources[0];
|
||||
if (!evaluate(source, snapshot.samplers[i])) return false;
|
||||
@@ -1671,11 +1730,13 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
if (program.info.samplers[base_index].gather_lod) {
|
||||
const auto control = snapshot.samplers[i].dwords[2];
|
||||
const auto filter = (control >> 26u) & 3u;
|
||||
// MipNone always selects the base level. Explicit point gathers currently require
|
||||
// encoded-zero primary and secondary bias; linear primary-mip selection is unsupported.
|
||||
if (filter > 1u || (filter == 1u && (control & 0xfffffu) != 0u)) {
|
||||
const auto perf_mip = (snapshot.samplers[i].dwords[1] >> 24u) & 15u;
|
||||
// MipNone always selects the base level. Point/linear selection requires zero
|
||||
// primary/secondary bias; linear mip snapping is not implemented.
|
||||
if (filter > 2u || (filter != 0u && (control & 0xfffffu) != 0u) ||
|
||||
(filter == 2u && perf_mip != 0u)) {
|
||||
return SpecializationFail(
|
||||
"explicit-LOD gather requires mip filtering None or Point with zero LOD biases");
|
||||
"explicit-LOD gather requires None, unbiased Point, or unbiased Linear without mip snapping");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1768,8 +1829,8 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
SamplerPlan sampler_plan;
|
||||
EXIT_IF(!BuildSamplerPlan(program.info, sampler_plan));
|
||||
const auto original_sampler_count = samplers.size();
|
||||
samplers.reserve(sampler_plan.sampler_count);
|
||||
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
|
||||
samplers.reserve(sampler_plan.bindings.size());
|
||||
for (uint32_t index = 0; index < sampler_plan.bindings.size(); index++) {
|
||||
const auto& binding = sampler_plan.bindings[index];
|
||||
if (index >= original_sampler_count) {
|
||||
samplers.push_back(samplers[binding.source]);
|
||||
@@ -1778,7 +1839,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
samplers[index].force_point_filtering = binding.type == SamplerClass::Integer;
|
||||
samplers[index].integer_border = binding.type != SamplerClass::Float;
|
||||
}
|
||||
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
|
||||
for (uint32_t index = 0; index < sampler_plan.bindings.size(); index++) {
|
||||
const auto& binding = sampler_plan.bindings[index];
|
||||
for (auto& candidate: samplers[index].indirect_resources) {
|
||||
candidate = sampler_plan.mapping[candidate][static_cast<uint32_t>(binding.type)];
|
||||
@@ -1905,6 +1966,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
EXIT_IF(image.indirect_root == memory.resource &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageSampleRaw &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageGatherRaw &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageQueryDimensions &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageRead &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageWrite);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -1558,14 +1557,55 @@ private:
|
||||
return read;
|
||||
}
|
||||
|
||||
const Inst* IndexedMaterialRead(const Inst* read, Value active,
|
||||
DescriptorSource::IndirectDescriptor::SelectorRead& selector) const {
|
||||
if (read == nullptr) return nullptr;
|
||||
uint32_t lane = 0;
|
||||
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() != (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];
|
||||
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
|
||||
!memory.idxen || memory.offen || memory.offset > UINT32_MAX - lane * 4u ||
|
||||
read->Arg(2).Resolve() != Value(0u) || read->Arg(3).Resolve() != Value(0u) ||
|
||||
!EquivalentValue(m_program, active, read->Arg(4))) return nullptr;
|
||||
selector.offset = memory.offset + lane * 4u;
|
||||
return read;
|
||||
}
|
||||
|
||||
bool MatchUniformizedMaterialKey(Value key, const Inst& image,
|
||||
DescriptorSource::IndirectDescriptor& indirect,
|
||||
DescriptorSource& material_source) {
|
||||
DescriptorSource& material_source, bool indexed) {
|
||||
const auto* selected = UniformizedMaterialValue(key, image);
|
||||
if (selected == nullptr) return false;
|
||||
const auto active = selected->Arg(0).Resolve();
|
||||
DescriptorSource::IndirectDescriptor::SelectorRead selector;
|
||||
const auto* read = SelectorReadValue(selected->Arg(1), active, selector);
|
||||
if (indexed) {
|
||||
read = IndexedMaterialRead(read, active, selector);
|
||||
// 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) {
|
||||
const auto op = inst.GetOpcode();
|
||||
return ImageOpcodeInfoOf(op).resource_class == ImageResourceClass::Storage ||
|
||||
AddressOpcodeInfoOf(op).access == AddressAccess::Write;
|
||||
});
|
||||
}) || !MakeRuntimeTableSource(*read, material_source)) return false;
|
||||
selector.source = InternSource(material_source);
|
||||
indirect.selector = std::move(selector);
|
||||
return true;
|
||||
}
|
||||
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadAddressU32 ||
|
||||
read->NumArgs() != 4u || !Implies(active, read->Arg(3)))
|
||||
return false;
|
||||
@@ -1818,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;
|
||||
@@ -1829,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;
|
||||
@@ -1858,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;
|
||||
@@ -2004,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;
|
||||
@@ -2066,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});
|
||||
@@ -2815,7 +2855,7 @@ private:
|
||||
} else result.maximum = std::max(result.maximum, SelectorMaximum(key, {}, 0u, active));
|
||||
}
|
||||
}
|
||||
if (lane != nullptr) result = SelectedFiniteDomain(*lane, handle != nullptr ? lane->Arg(1) : Value {});
|
||||
if (lane != nullptr) result = SelectedSelectorDomain(*lane, handle != nullptr ? lane->Arg(1) : Value {});
|
||||
else if (handle == nullptr) result.maximum = SelectorMaximum(key);
|
||||
if (result.maximum > UINT32_MAX / 4u ||
|
||||
uint64_t {result.maximum} * indirect.table_stride + indirect.table_offset > UINT32_MAX)
|
||||
@@ -2971,6 +3011,13 @@ private:
|
||||
// Keep live descriptor payloads, but project descriptor-only SMEM onto
|
||||
// the GPU key using the same plan as other indirect image reads.
|
||||
plan.retain_reads |= table_source.dword_count == 4u;
|
||||
} else if (!sampler && !plan.table_indexed && table_source.dword_count == 4u &&
|
||||
MatchUniformizedMaterialKey(key, handle, indirect, material_source, true)) {
|
||||
// Preserve the heap's byte-offset key so runtime geometry can choose the
|
||||
// tighter selector domain without changing the GPU's descriptor reads.
|
||||
indirect.table_scalar = true;
|
||||
plan.table_read = plan.reads[0];
|
||||
plan.retain_reads = true;
|
||||
} else if (sampler && table_source.dword_count == 4u && !plan.table_indexed &&
|
||||
SetExactTableMaximum(key, indirect, &handle)) {
|
||||
indirect.table_scalar = true;
|
||||
@@ -3020,7 +3067,8 @@ private:
|
||||
}
|
||||
}
|
||||
if (indirect.key_count.IsEmpty() &&
|
||||
!MatchUniformizedMaterialKey(key, handle, indirect, material_source) &&
|
||||
!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;
|
||||
@@ -3064,6 +3112,10 @@ private:
|
||||
indirect.selector_mask = selected.selector_mask;
|
||||
indirect.signed_count = selected.signed_count;
|
||||
indirect.workgroup_axis = selected.workgroup_axis;
|
||||
if (selected.table_scalar) {
|
||||
indirect.table_scalar = true;
|
||||
plan.table_read = plan.reads[0];
|
||||
}
|
||||
material_source = m_sources[selected.selector->source];
|
||||
shared_selector = true;
|
||||
break;
|
||||
@@ -3264,26 +3316,13 @@ private:
|
||||
if (value == nullptr || (selected != nullptr && selected != value)) return false;
|
||||
selected = value;
|
||||
}
|
||||
const auto* read = selected->Arg(1).Resolve().TryInstruction();
|
||||
uint32_t lane = 0;
|
||||
if (read != nullptr && read->GetOpcode() == ValueOpcode::CompositeExtractU32x2) {
|
||||
if (!ImmediateU32(read->Arg(1), lane) || lane >= 2u) return false;
|
||||
read = read->Arg(0).Resolve().TryInstruction();
|
||||
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32x2) return false;
|
||||
} else if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32) {
|
||||
return false;
|
||||
}
|
||||
const auto& memory = m_program.memory_info[read->Flags<MemoryFlags>().index];
|
||||
uint32_t offset = 1u, scalar = 1u;
|
||||
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
|
||||
!memory.idxen || memory.offen || memory.offset > UINT32_MAX - lane * 4u ||
|
||||
!ImmediateU32(read->Arg(2), offset) || offset != 0u ||
|
||||
!ImmediateU32(read->Arg(3), scalar) || scalar != 0u ||
|
||||
!EquivalentValue(m_program, selected->Arg(0), read->Arg(4))) return false;
|
||||
if (!MakeRuntimeTableSource(*read, material_source) ||
|
||||
DescriptorSource::IndirectDescriptor::SelectorRead selector;
|
||||
const auto* read = IndexedMaterialRead(selected->Arg(1).Resolve().TryInstruction(),
|
||||
selected->Arg(0), selector);
|
||||
if (read == nullptr || !MakeRuntimeTableSource(*read, material_source) ||
|
||||
!BoundedSelectedIndex(read->Arg(1), read->Arg(4), indirect)) return false;
|
||||
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead{.source = InternSource(material_source),
|
||||
.offset = memory.offset + lane * 4u});
|
||||
selector.source = InternSource(material_source);
|
||||
indirect.selector = std::move(selector);
|
||||
return finish();
|
||||
}
|
||||
|
||||
@@ -3372,10 +3411,12 @@ private:
|
||||
return found ? SelectorDomain {maximum, counted ? count : Value {}} : SelectorDomain {};
|
||||
}
|
||||
|
||||
SelectorDomain SelectedFiniteDomain(const Inst& lane, Value guard) const {
|
||||
SelectorDomain SelectedSelectorDomain(const Inst& lane, Value guard) const {
|
||||
if (guard.IsEmpty() ? !HasActiveLane(lane.Arg(1), lane.Parent())
|
||||
: !Implies(guard, lane.Arg(1))) return {};
|
||||
return FiniteSelectorDomain(lane.Arg(0), lane.Arg(1));
|
||||
auto result = FiniteSelectorDomain(lane.Arg(0), lane.Arg(1));
|
||||
if (result.maximum == UINT32_MAX) result.maximum = PossibleU32Bits(lane.Arg(0));
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t SelectorMaximum(Value value, SelectorBounds bounds = {}, uint32_t depth = 0,
|
||||
@@ -3392,7 +3433,7 @@ private:
|
||||
};
|
||||
switch (inst->GetOpcode()) {
|
||||
case ValueOpcode::ReadFirstLane:
|
||||
return SelectedFiniteDomain(*inst, guard).maximum;
|
||||
return SelectedSelectorDomain(*inst, guard).maximum;
|
||||
case ValueOpcode::ReadLane:
|
||||
// ReadLane ignores EXEC; only context-free bounds on every source lane apply.
|
||||
return PossibleU32Bits(inst->Arg(0));
|
||||
@@ -3708,10 +3749,10 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t AddBuffer(uint32_t source, const MemoryInfo& memory, ValueOpcode op, uint32_t pc) {
|
||||
uint32_t AddBuffer(uint32_t source, const MemoryInfo& memory, const Inst& inst, uint32_t pc) {
|
||||
for (uint32_t i = 0; i < m_info.buffers.size(); i++) {
|
||||
if (m_info.buffers[i].source == source) {
|
||||
Merge(m_info.buffers[i], memory, op, pc);
|
||||
Merge(m_info.buffers[i], memory, inst, pc);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -3721,13 +3762,14 @@ private:
|
||||
BufferResource resource;
|
||||
resource.source = source;
|
||||
resource.first_use_pc = pc;
|
||||
Merge(resource, memory, op, pc);
|
||||
Merge(resource, memory, inst, pc);
|
||||
m_info.buffers.push_back(resource);
|
||||
return static_cast<uint32_t>(m_info.buffers.size() - 1);
|
||||
}
|
||||
|
||||
static void Merge(BufferResource& resource, const MemoryInfo& memory, ValueOpcode op,
|
||||
static void Merge(BufferResource& resource, const MemoryInfo& memory, const Inst& inst,
|
||||
uint32_t pc) {
|
||||
const auto op = inst.GetOpcode();
|
||||
const auto access = BufferAccessOf(op);
|
||||
const bool atomic = access == BufferAccess::Atomic;
|
||||
const bool write = access == BufferAccess::Write || atomic;
|
||||
@@ -3735,6 +3777,13 @@ private:
|
||||
resource.max_byte_extent = std::max(resource.max_byte_extent, ByteExtent(memory));
|
||||
resource.read = resource.read || !write || atomic;
|
||||
resource.written = resource.written || write;
|
||||
if (write) {
|
||||
// Formatted stores write the whole format; otherwise zero index, VGPR and SGPR offsets
|
||||
// leave only the immediate offset.
|
||||
const bool fixed = !memory.formatted && inst.Arg(1).Resolve() == Value(0u) &&
|
||||
inst.Arg(2).Resolve() == Value(0u) && inst.Arg(3).Resolve() == Value(0u);
|
||||
resource.write_extent = std::max(resource.write_extent, fixed ? ByteExtent(memory) : UINT32_MAX);
|
||||
}
|
||||
resource.atomic = resource.atomic || atomic;
|
||||
resource.formatted = resource.formatted || memory.formatted;
|
||||
resource.scalar = resource.scalar || op == ValueOpcode::ReadConstBuffer ||
|
||||
@@ -3823,9 +3872,6 @@ private:
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (m_info.sampled_pairs.size() >= ShaderInfo::MaxSampledPairs) {
|
||||
Fail(pc, "sampled image/sampler pair limit exceeded");
|
||||
}
|
||||
m_info.sampled_pairs.push_back({image, sampler, pc});
|
||||
}
|
||||
|
||||
@@ -3907,7 +3953,7 @@ private:
|
||||
m_info.uses_dma = true;
|
||||
return;
|
||||
}
|
||||
resource = AddBuffer(source, memory, op, flags.pc);
|
||||
resource = AddBuffer(source, memory, inst, flags.pc);
|
||||
if (resource == UINT32_MAX) {
|
||||
Fail(flags.pc, "buffer resource limit exceeded");
|
||||
}
|
||||
@@ -4027,7 +4073,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;
|
||||
@@ -4060,8 +4106,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) {
|
||||
@@ -509,6 +500,7 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
|
||||
case ValueOpcode::ReadConst: info.uses_flattened_srt = true; break;
|
||||
case ValueOpcode::BitwiseXor32: info.has_bitwise_xor = true; break;
|
||||
case ValueOpcode::StoreCompletion: info.subgroup_barrier = true; break;
|
||||
case ValueOpcode::DataCacheInvalidate: info.coherent_buffers = true; break;
|
||||
case ValueOpcode::BvhIntersect: info.bvh = true; break;
|
||||
case ValueOpcode::ConditionRef:
|
||||
info.subgroup_vote |=
|
||||
|
||||
@@ -159,9 +159,11 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
|
||||
case ValueOpcode::ULessThan32:
|
||||
case ValueOpcode::ULessThanEqual32:
|
||||
case ValueOpcode::IEqual32:
|
||||
case ValueOpcode::IEqual64:
|
||||
case ValueOpcode::UGreaterThan32:
|
||||
case ValueOpcode::SGreaterThanEqual32:
|
||||
case ValueOpcode::INotEqual32:
|
||||
case ValueOpcode::INotEqual64:
|
||||
case ValueOpcode::LogicalOr:
|
||||
case ValueOpcode::LogicalAnd:
|
||||
case ValueOpcode::LogicalXor:
|
||||
@@ -1053,6 +1055,11 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case ValueOpcode::IEqual64:
|
||||
case ValueOpcode::INotEqual64:
|
||||
if (!binary()) return false;
|
||||
result = inst.GetOpcode() == ValueOpcode::IEqual64 ? a == b : a != b;
|
||||
return true;
|
||||
case ValueOpcode::ULessThan32:
|
||||
// No U32 is below zero, including an unknown loop-carried value.
|
||||
if (!Arg(inst, 1, b)) return false;
|
||||
|
||||
@@ -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;
|
||||
@@ -352,10 +355,11 @@ static void ShaderGetStaticInputInfoPS(
|
||||
ps_info.ps_pixel_kill_enable = sh.db_shader_control.shader_kill_enable;
|
||||
ps_info.ps_depth_export_enable = sh.db_shader_control.shader_z_export_enable;
|
||||
ps_info.ps_sample_mask_export_enable = sh.db_shader_control.shader_mask_export_enable;
|
||||
// Alpha-to-mask coverage must gate depth writes, so it rules out early fragment tests.
|
||||
ps_info.ps_early_z =
|
||||
(sh.db_shader_control.shader_z_behavior == 1 && !sh.db_shader_control.shader_kill_enable &&
|
||||
!sh.db_shader_control.shader_z_export_enable &&
|
||||
!sh.db_shader_control.shader_mask_export_enable);
|
||||
!sh.db_shader_control.shader_mask_export_enable && !sh.AlphaToMask());
|
||||
ps_info.ps_execute_on_noop = sh.db_shader_control.shader_execute_on_noop;
|
||||
|
||||
for (uint32_t i = 0; i < ps_info.input_num; i++) {
|
||||
@@ -411,7 +415,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 +444,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 +461,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 +536,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 +655,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 +766,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 +776,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;
|
||||
|
||||
@@ -1326,6 +1326,27 @@ int KYTY_SYSV_ABI KernelFtruncate(int d, int64_t length) {
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI KernelFsync(int d) {
|
||||
PRINT_NAME();
|
||||
|
||||
if (d < DESCRIPTOR_MIN) {
|
||||
return KERNEL_ERROR_EBADF;
|
||||
}
|
||||
|
||||
auto* file = g_files->GetFile(d);
|
||||
if (file == nullptr || !file->opened) {
|
||||
return KERNEL_ERROR_EBADF;
|
||||
}
|
||||
|
||||
Common::LockGuard lock(file->mutex);
|
||||
if (file->f.IsInvalid() || !file->f.Flush()) {
|
||||
return KERNEL_ERROR_EIO;
|
||||
}
|
||||
|
||||
LOGF("\tFsync: %s\n", Common::PathToString(file->real_name).c_str());
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI KernelUnlink(const char* path) {
|
||||
PRINT_NAME();
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ int KYTY_SYSV_ABI KernelStat(const char* path, FileStat* sb);
|
||||
int KYTY_SYSV_ABI KernelFstat(int d, FileStat* sb);
|
||||
int KYTY_SYSV_ABI KernelTruncate(const char* path, int64_t length);
|
||||
int KYTY_SYSV_ABI KernelFtruncate(int d, int64_t length);
|
||||
int KYTY_SYSV_ABI KernelFsync(int d);
|
||||
int KYTY_SYSV_ABI KernelUnlink(const char* path);
|
||||
int KYTY_SYSV_ABI KernelRename(const char* from, const char* to);
|
||||
int KYTY_SYSV_ABI KernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+86
-52
@@ -343,14 +343,21 @@ static bool KernelClockGettimeSpecial(KernelClockid clock_id, KernelTimespec* tp
|
||||
#endif
|
||||
|
||||
struct PthreadMutexPrivate {
|
||||
struct Waiter {
|
||||
Pthread thread = nullptr;
|
||||
std::condition_variable cv;
|
||||
Waiter* previous = nullptr;
|
||||
Waiter* next = nullptr;
|
||||
};
|
||||
uint8_t reserved[256];
|
||||
std::string name;
|
||||
std::mutex m;
|
||||
std::condition_variable cv;
|
||||
Pthread owner = nullptr;
|
||||
uint32_t count = 0;
|
||||
int type = 1;
|
||||
int pprotocol = PTHREAD_PRIO_NONE;
|
||||
Waiter* first_waiter = nullptr;
|
||||
Waiter* last_waiter = nullptr;
|
||||
};
|
||||
|
||||
struct PthreadMutexattrPrivate {
|
||||
@@ -1242,6 +1249,20 @@ static int PthreadRwlockInitNamed(PthreadRwlock* rwlock, const PthreadRwlockattr
|
||||
const char* name);
|
||||
static int PthreadCondInitNamed(PthreadCond* cond, const PthreadCondattr* attr, const char* name);
|
||||
|
||||
static void NativeMutexRemoveWaiter(PthreadMutexPrivate* mutex,
|
||||
PthreadMutexPrivate::Waiter* waiter) {
|
||||
if (waiter->previous != nullptr) {
|
||||
waiter->previous->next = waiter->next;
|
||||
} else {
|
||||
mutex->first_waiter = waiter->next;
|
||||
}
|
||||
if (waiter->next != nullptr) {
|
||||
waiter->next->previous = waiter->previous;
|
||||
} else {
|
||||
mutex->last_waiter = waiter->previous;
|
||||
}
|
||||
}
|
||||
|
||||
static int NativeMutexLock(PthreadMutexPrivate* mutex, KernelUseconds* timeout_us) {
|
||||
EXIT_IF(mutex == nullptr);
|
||||
|
||||
@@ -1258,54 +1279,53 @@ static int NativeMutexLock(PthreadMutexPrivate* mutex, KernelUseconds* timeout_u
|
||||
mutex->count++;
|
||||
return OK;
|
||||
}
|
||||
if (timeout_us != nullptr) {
|
||||
if (*timeout_us > 0) {
|
||||
mutex->cv.wait_for(lock, std::chrono::microseconds(*timeout_us));
|
||||
}
|
||||
return ETIMEDOUT;
|
||||
}
|
||||
return EDEADLK;
|
||||
}
|
||||
|
||||
if (timeout_us == nullptr) {
|
||||
while (mutex->owner != nullptr) {
|
||||
mutex->cv.wait_for(lock, std::chrono::microseconds(SIGNAL_APC_POLL_MICROS));
|
||||
if (mutex->owner != nullptr) {
|
||||
lock.unlock();
|
||||
KernelDispatchPendingSignalForCurrentThread();
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
} else if (*timeout_us == 0) {
|
||||
if (mutex->owner != nullptr) {
|
||||
return ETIMEDOUT;
|
||||
}
|
||||
} else {
|
||||
const auto deadline =
|
||||
std::chrono::steady_clock::now() + std::chrono::microseconds(*timeout_us);
|
||||
while (mutex->owner != nullptr) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= deadline) {
|
||||
return ETIMEDOUT;
|
||||
}
|
||||
|
||||
const auto remaining = deadline - now;
|
||||
const auto poll = (remaining < std::chrono::microseconds(SIGNAL_APC_POLL_MICROS)
|
||||
? remaining
|
||||
: std::chrono::steady_clock::duration(
|
||||
std::chrono::microseconds(SIGNAL_APC_POLL_MICROS)));
|
||||
mutex->cv.wait_for(lock, poll);
|
||||
if (mutex->owner != nullptr) {
|
||||
lock.unlock();
|
||||
KernelDispatchPendingSignalForCurrentThread();
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
if (mutex->owner == nullptr) {
|
||||
mutex->owner = self;
|
||||
mutex->count = 1;
|
||||
return OK;
|
||||
}
|
||||
if (timeout_us != nullptr && *timeout_us == 0) {
|
||||
return ETIMEDOUT;
|
||||
}
|
||||
|
||||
mutex->owner = self;
|
||||
mutex->count = 1;
|
||||
return OK;
|
||||
// Guest mutex waits are not cancellation points, unlike the host condition wait.
|
||||
int previous_cancel_state = 0;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &previous_cancel_state);
|
||||
PthreadMutexPrivate::Waiter waiter;
|
||||
waiter.thread = self;
|
||||
waiter.previous = mutex->last_waiter;
|
||||
if (mutex->last_waiter != nullptr) {
|
||||
mutex->last_waiter->next = &waiter;
|
||||
} else {
|
||||
mutex->first_waiter = &waiter;
|
||||
}
|
||||
mutex->last_waiter = &waiter;
|
||||
const auto deadline = timeout_us != nullptr
|
||||
? std::chrono::steady_clock::now() +
|
||||
std::chrono::microseconds(*timeout_us)
|
||||
: std::chrono::steady_clock::time_point::max();
|
||||
int result = OK;
|
||||
while (mutex->owner != self) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= deadline) {
|
||||
NativeMutexRemoveWaiter(mutex, &waiter);
|
||||
result = ETIMEDOUT;
|
||||
break;
|
||||
}
|
||||
waiter.cv.wait_until(lock, std::min(deadline, now +
|
||||
std::chrono::microseconds(SIGNAL_APC_POLL_MICROS)));
|
||||
if (mutex->owner != self) {
|
||||
lock.unlock();
|
||||
KernelDispatchPendingSignalForCurrentThread();
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
lock.unlock();
|
||||
pthread_setcancelstate(previous_cancel_state, nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
static int NativeMutexTrylock(PthreadMutexPrivate* mutex) {
|
||||
@@ -1358,10 +1378,16 @@ static int NativeMutexUnlock(PthreadMutexPrivate* mutex, uint32_t* recurse = nul
|
||||
return OK;
|
||||
}
|
||||
|
||||
mutex->owner = nullptr;
|
||||
mutex->count = 0;
|
||||
lock.unlock();
|
||||
mutex->cv.notify_one();
|
||||
if (auto* waiter = mutex->first_waiter) {
|
||||
NativeMutexRemoveWaiter(mutex, waiter);
|
||||
// Transfer ownership in FIFO order before waking the selected waiter.
|
||||
mutex->owner = waiter->thread;
|
||||
mutex->count = 1;
|
||||
waiter->cv.notify_one();
|
||||
} else {
|
||||
mutex->owner = nullptr;
|
||||
mutex->count = 0;
|
||||
}
|
||||
|
||||
return OK;
|
||||
}
|
||||
@@ -3363,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);
|
||||
@@ -3466,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;
|
||||
@@ -3484,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)
|
||||
|
||||
@@ -482,15 +482,15 @@
|
||||
<property name="title"><string>Rendering compatibility</string></property>
|
||||
<layout class="QVBoxLayout" name="renderingCompatibilityLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_sync_raw_image_buffers">
|
||||
<property name="toolTip"><string>Can fix missing textures and incorrect transparency (Resident Evil 2).</string></property>
|
||||
<property name="text"><string>Accurate texture synchronization</string></property>
|
||||
<widget class="QLabel" name="label_rendering_compatibility">
|
||||
<property name="text"><string>These settings may help with rendering bugs at a slight performance cost.</string></property>
|
||||
<property name="wordWrap"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_support_zmask">
|
||||
<property name="toolTip"><string>Can restore missing geometry (High on Life 2).</string></property>
|
||||
<property name="text"><string>Support ZMask</string></property>
|
||||
<widget class="QCheckBox" name="checkBox_sync_raw_image_buffers">
|
||||
<property name="toolTip"><string>Can fix missing textures and incorrect transparency (Resident Evil 2).</string></property>
|
||||
<property name="text"><string>Accurate texture synchronization</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -127,7 +127,6 @@ public:
|
||||
bool hide_cursor_enabled = false;
|
||||
bool readback_linear_images = false;
|
||||
bool sync_raw_image_buffers = false;
|
||||
bool support_zmask = false;
|
||||
bool tessellation_enabled = false;
|
||||
bool trophy_enabled = true;
|
||||
int trophy_duration_seconds = 6;
|
||||
@@ -167,7 +166,6 @@ public:
|
||||
hide_cursor_enabled = other.hide_cursor_enabled;
|
||||
readback_linear_images = other.readback_linear_images;
|
||||
sync_raw_image_buffers = other.sync_raw_image_buffers;
|
||||
support_zmask = other.support_zmask;
|
||||
tessellation_enabled = other.tessellation_enabled;
|
||||
trophy_enabled = other.trophy_enabled;
|
||||
trophy_duration_seconds = other.trophy_duration_seconds;
|
||||
|
||||
@@ -14,7 +14,6 @@ QVariantMap Configuration::GameSettings() const {
|
||||
KYTY_CFG_SET(hide_cursor_enabled);
|
||||
KYTY_CFG_SET(readback_linear_images);
|
||||
KYTY_CFG_SET(sync_raw_image_buffers);
|
||||
KYTY_CFG_SET(support_zmask);
|
||||
KYTY_CFG_SET(tessellation_enabled);
|
||||
KYTY_CFG_SET(trophy_enabled);
|
||||
KYTY_CFG_SET(trophy_duration_seconds);
|
||||
@@ -64,7 +63,6 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
|
||||
KYTY_CFG_GET(hide_cursor_enabled);
|
||||
KYTY_CFG_GET(readback_linear_images);
|
||||
KYTY_CFG_GET(sync_raw_image_buffers);
|
||||
KYTY_CFG_GET(support_zmask);
|
||||
KYTY_CFG_GET(tessellation_enabled);
|
||||
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
|
||||
KYTY_CFG_GET(trophy_duration_seconds);
|
||||
|
||||
@@ -287,7 +287,6 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
|
||||
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
|
||||
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
|
||||
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
|
||||
m_ui->checkBox_support_zmask->setChecked(info.support_zmask);
|
||||
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
|
||||
m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen);
|
||||
m_ui->checkBox_network_connected->setChecked(info.network_connected);
|
||||
@@ -481,7 +480,6 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
|
||||
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
|
||||
info.readback_linear_images = ui.checkBox_readback->isChecked();
|
||||
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
|
||||
info.support_zmask = ui.checkBox_support_zmask->isChecked();
|
||||
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
|
||||
info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked();
|
||||
info.network_connected = ui.checkBox_network_connected->isChecked();
|
||||
|
||||
@@ -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();
|
||||
@@ -222,8 +244,10 @@ void MainDialogPrivate::FindInterpreter() {
|
||||
}
|
||||
}
|
||||
|
||||
static QString BoolArg(bool value) {
|
||||
return value ? QStringLiteral("true") : QStringLiteral("false");
|
||||
static void AddFlag(QStringList& args, bool enabled, const char* name) {
|
||||
if (enabled) {
|
||||
args << name;
|
||||
}
|
||||
}
|
||||
|
||||
static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||
@@ -251,55 +275,37 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||
if (info.gpu_index >= 0) {
|
||||
args << "--gpu" << QString::number(info.gpu_index);
|
||||
}
|
||||
if (info.fullscreen_enabled) {
|
||||
args << "--fullscreen";
|
||||
}
|
||||
if (info.hdr_enabled) {
|
||||
args << "--hdr";
|
||||
}
|
||||
if (info.hide_cursor_enabled) {
|
||||
args << "--hide-cursor";
|
||||
}
|
||||
args << "--readback-linear-images" << BoolArg(info.readback_linear_images);
|
||||
args << "--sync-raw-image-buffers" << BoolArg(info.sync_raw_image_buffers);
|
||||
args << "--support-zmask" << BoolArg(info.support_zmask);
|
||||
args << "--trophy-notifications" << BoolArg(info.trophy_enabled);
|
||||
AddFlag(args, info.fullscreen_enabled, "--fullscreen");
|
||||
AddFlag(args, info.hdr_enabled, "--hdr");
|
||||
AddFlag(args, info.hide_cursor_enabled, "--hide-cursor");
|
||||
AddFlag(args, info.readback_linear_images, "--readback-linear-images");
|
||||
AddFlag(args, info.sync_raw_image_buffers, "--sync-raw-image-buffers");
|
||||
AddFlag(args, !info.trophy_enabled, "--no-trophy-notifications");
|
||||
args << "--trophy-duration" << QString::number(info.trophy_duration_seconds);
|
||||
args << "--perf-overlay" << BoolArg(info.perf_overlay_enabled);
|
||||
args << "--skip-notice-screen" << BoolArg(info.skip_notice_screen);
|
||||
args << "--network-connected" << BoolArg(info.network_connected);
|
||||
if (info.tessellation_enabled) {
|
||||
args << "--tessellation";
|
||||
}
|
||||
AddFlag(args, info.perf_overlay_enabled, "--perf-overlay");
|
||||
AddFlag(args, info.skip_notice_screen, "--skip-notice-screen");
|
||||
AddFlag(args, info.network_connected, "--network-connected");
|
||||
AddFlag(args, info.tessellation_enabled, "--tessellation");
|
||||
args << "--vblank-frequency" << QString::number(info.vblank_frequency);
|
||||
args << "--console-language" << QString::number(info.console_language);
|
||||
args << "--vulkan-validation" << BoolArg(info.vulkan_validation_enabled);
|
||||
args << "--shader-validation" << BoolArg(info.shader_validation_enabled);
|
||||
AddFlag(args, info.vulkan_validation_enabled, "--vulkan-validation");
|
||||
AddFlag(args, info.shader_validation_enabled, "--shader-validation");
|
||||
args << "--shader-optimization-type" << EnumToText(info.shader_optimization_type);
|
||||
args << "--shader-log-direction" << EnumToText(info.shader_log_direction);
|
||||
args << "--shader-log-folder" << info.shader_log_folder;
|
||||
args << "--command-buffer-dump" << BoolArg(info.command_buffer_dump_enabled);
|
||||
AddFlag(args, info.command_buffer_dump_enabled, "--command-buffer-dump");
|
||||
args << "--command-buffer-dump-folder" << info.command_buffer_dump_folder;
|
||||
args << "--printf-direction" << EnumToText(info.printf_direction);
|
||||
args << "--printf-output-file" << info.printf_output_file;
|
||||
if (info.profiler_enabled) {
|
||||
args << "--profile";
|
||||
}
|
||||
args << "--spirv-debug-printf" << "false";
|
||||
if (info.amd_cpu_enabled) {
|
||||
args << "--amd-cpu";
|
||||
}
|
||||
AddFlag(args, info.profiler_enabled, "--profile");
|
||||
AddFlag(args, info.amd_cpu_enabled, "--amd-cpu");
|
||||
#if defined(_WIN32)
|
||||
if (info.red_zone_protection_enabled) {
|
||||
args << "--redzone";
|
||||
}
|
||||
AddFlag(args, info.red_zone_protection_enabled, "--redzone");
|
||||
#endif
|
||||
for (const auto& binding: info.host_input_mapping) {
|
||||
args << "--keymap" << binding;
|
||||
}
|
||||
if (info.renderdoc_enabled) {
|
||||
args << "--rd";
|
||||
}
|
||||
AddFlag(args, info.renderdoc_enabled, "--rd");
|
||||
|
||||
QString game = info.basedir;
|
||||
if (!info.elf.isEmpty() && !GameContent::IsArchive(info.basedir)) {
|
||||
@@ -517,6 +523,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();
|
||||
}
|
||||
@@ -526,6 +533,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();
|
||||
}
|
||||
|
||||
@@ -105,7 +105,17 @@ PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
|
||||
auto* local_buttons = new QDialogButtonBox(local);
|
||||
auto* reload = local_buttons->addButton(tr("Reload"), QDialogButtonBox::ActionRole);
|
||||
m_apply = local_buttons->addButton(tr("Apply selection"), QDialogButtonBox::ActionRole);
|
||||
auto* enable_all = local_buttons->addButton(tr("Enable all"), QDialogButtonBox::ActionRole);
|
||||
auto* disable_all = local_buttons->addButton(tr("Disable all"), QDialogButtonBox::ActionRole);
|
||||
local_layout->addWidget(local_buttons);
|
||||
auto set_all = [this](Qt::CheckState state) {
|
||||
for (int i = 0; i < m_patches->count(); ++i)
|
||||
{
|
||||
m_patches->item(i)->setCheckState(state);
|
||||
}
|
||||
};
|
||||
connect(enable_all, &QPushButton::clicked, this, [set_all] { set_all(Qt::Checked); });
|
||||
connect(disable_all, &QPushButton::clicked, this, [set_all] { set_all(Qt::Unchecked); });
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
|
||||
layout->addWidget(buttons);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
@@ -96,7 +97,27 @@ bool UpdateChecker::IsSupported() {
|
||||
|
||||
void UpdateChecker::ShowPreviousResult(QWidget* parent) {
|
||||
const auto directory = InstallDirectory();
|
||||
QFile result(QDir(directory).filePath(QStringLiteral(".kyty-update-result")));
|
||||
if (!directory.isEmpty()) {
|
||||
// The helper cannot delete its own executable on Windows, so its workspace is
|
||||
// removed by the next launcher start.
|
||||
const QDir install(directory);
|
||||
for (const auto& name:
|
||||
install.entryList({QStringLiteral(".kyty-update-??????")},
|
||||
QDir::Dirs | QDir::Hidden | QDir::NoSymLinks | QDir::NoDotAndDotDot)) {
|
||||
QDir workspace(install.filePath(name));
|
||||
QDateTime newest = QFileInfo(workspace.path()).lastModified();
|
||||
for (const auto& entry: workspace.entryInfoList(QDir::Files | QDir::Dirs | QDir::Hidden |
|
||||
QDir::NoDotAndDotDot)) {
|
||||
newest = qMax(newest, entry.lastModified());
|
||||
}
|
||||
// A workspace touched recently may belong to a running download or helper. Windows
|
||||
// lists a file still being downloaded with a stale time, so allow a generous margin.
|
||||
if (newest.secsTo(QDateTime::currentDateTime()) > 60 * 60) {
|
||||
workspace.removeRecursively();
|
||||
}
|
||||
}
|
||||
}
|
||||
QFile result(QDir(directory).filePath(QStringLiteral(".kyty-update-result")));
|
||||
if (directory.isEmpty() || !result.open(QIODevice::ReadOnly)) {
|
||||
return;
|
||||
}
|
||||
|
||||
+1
-86
@@ -670,7 +670,7 @@ int KYTY_SYSV_ABI AgcCreateShader(Shader** dst, void* header, const volatile voi
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI AgcUnknownGetFusedShaderSize(SizeAlign* dst, const Shader* front,
|
||||
int KYTY_SYSV_ABI AgcGetFusedShaderSize(SizeAlign* dst, const Shader* front,
|
||||
const Shader* back) {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -1439,14 +1439,6 @@ int KYTY_SYSV_ABI AgcCreateInterpolantMapping2(ShaderRegister* regs, const Shade
|
||||
int KYTY_SYSV_ABI AgcGetDataPacketPayloadAddress(uint32_t** addr, uint32_t* cmd, int type) {
|
||||
PRINT_NAME();
|
||||
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1) < 64) {
|
||||
LOGF("\t addr = 0x%016" PRIx64 "\n"
|
||||
"\t cmd = 0x%016" PRIx64 "\n"
|
||||
"\t type = %d\n",
|
||||
reinterpret_cast<uint64_t>(addr), reinterpret_cast<uint64_t>(cmd), type);
|
||||
}
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(addr == nullptr);
|
||||
|
||||
if (type != 0) {
|
||||
@@ -1463,20 +1455,10 @@ int KYTY_SYSV_ABI AgcGetDataPacketPayloadAddress(uint32_t** addr, uint32_t* cmd,
|
||||
int KYTY_SYSV_ABI AgcGetDataPacketPayloadRange(MemoryRange* range, uint32_t* cmd, int type) {
|
||||
PRINT_NAME();
|
||||
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
const bool should_log = (log_count.fetch_add(1) < 64);
|
||||
if (should_log) {
|
||||
LOGF("\t range = 0x%016" PRIx64 "\n"
|
||||
"\t cmd = 0x%016" PRIx64 "\n"
|
||||
"\t type = %d\n",
|
||||
reinterpret_cast<uint64_t>(range), reinterpret_cast<uint64_t>(cmd), type);
|
||||
}
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(range == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
||||
const auto cmd_id = cmd[0];
|
||||
const auto len = KYTY_PM4_LEN(cmd_id);
|
||||
|
||||
if (type != 0) {
|
||||
range->m_base = cmd + 2;
|
||||
@@ -1491,13 +1473,6 @@ int KYTY_SYSV_ABI AgcGetDataPacketPayloadRange(MemoryRange* range, uint32_t* cmd
|
||||
}
|
||||
}
|
||||
|
||||
if (should_log) {
|
||||
LOGF("\t cmd_id = 0x%08" PRIx32 ", len = %" PRIu32 "\n"
|
||||
"\t base = 0x%016" PRIx64 "\n"
|
||||
"\t size = 0x%016" PRIx64 "\n",
|
||||
cmd_id, len, reinterpret_cast<uint64_t>(range->m_base), range->m_size);
|
||||
}
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
@@ -1746,15 +1721,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbDispatch(CommandBuffer* buf, uint32_t thread_group_
|
||||
uint32_t modifier) {
|
||||
PRINT_NAME();
|
||||
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1) < 64) {
|
||||
LOGF("\t thread_group_x = %" PRIu32 "\n"
|
||||
"\t thread_group_y = %" PRIu32 "\n"
|
||||
"\t thread_group_z = %" PRIu32 "\n"
|
||||
"\t modifier = 0x%08" PRIx32 "\n",
|
||||
thread_group_x, thread_group_y, thread_group_z, modifier);
|
||||
}
|
||||
|
||||
if (buf == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1862,16 +1828,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbSetShRegisterRangeDirect(CommandBuffer* buf, uint32
|
||||
const uint32_t* values, uint32_t num_values) {
|
||||
PRINT_NAME();
|
||||
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1) < 64) {
|
||||
LOGF("\t buf = 0x%016" PRIx64 "\n"
|
||||
"\t offset = %" PRIx32 "\n"
|
||||
"\t values = 0x%016" PRIx64 "\n"
|
||||
"\t num_values = %" PRIu32 "\n",
|
||||
reinterpret_cast<uint64_t>(buf), offset, reinterpret_cast<uint64_t>(values),
|
||||
num_values);
|
||||
}
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
auto* cmd = buf->AllocateDW(num_values + 2);
|
||||
@@ -1902,12 +1858,6 @@ uint32_t KYTY_SYSV_ABI AgcCbSetShRegisterRangeDirectGetSize(uint32_t num_values)
|
||||
uint32_t* KYTY_SYSV_ABI AgcCbSetShRegistersDirect(CommandBuffer* buf,
|
||||
const volatile ShaderRegister* regs,
|
||||
uint32_t num_regs) {
|
||||
PRINT_NAME();
|
||||
|
||||
AgcTrace("\t regs = 0x%016" PRIx64 "\n"
|
||||
"\t num_regs = %" PRIu32 "\n",
|
||||
reinterpret_cast<uint64_t>(regs), num_regs);
|
||||
|
||||
if (num_regs == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -2047,23 +1997,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbReleaseMem(CommandBuffer* buf, uint8_t action, uint
|
||||
uint8_t interrupt, uint32_t interrupt_ctx_id) {
|
||||
PRINT_NAME();
|
||||
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1) < 64) {
|
||||
LOGF("\t action = 0x%02" PRIx8 "\n"
|
||||
"\t gcr_cntl = 0x%04" PRIx16 "\n"
|
||||
"\t dst = %" PRIu8 "\n"
|
||||
"\t cache_policy = 0x%02" PRIx8 "\n"
|
||||
"\t address = 0x%016" PRIx64 "\n"
|
||||
"\t data_sel = 0x%02" PRIx8 "\n"
|
||||
"\t data = 0x%016" PRIx64 "\n"
|
||||
"\t gds_offset = %" PRIu16 "\n"
|
||||
"\t gds_size = %" PRIu16 "\n"
|
||||
"\t interrupt = 0x%02" PRIx8 "\n"
|
||||
"\t interrupt_ctx_id = %" PRIu32 "\n",
|
||||
action, gcr_cntl, dst, cache_policy, reinterpret_cast<uint64_t>(address), data_sel,
|
||||
data, gds_offset, gds_size, interrupt, interrupt_ctx_id);
|
||||
}
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(dst > 1);
|
||||
EXIT_NOT_IMPLEMENTED(data_sel != 0 && data_sel != 1 && data_sel != 2 && data_sel != 3 &&
|
||||
@@ -2950,13 +2883,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbEventWrite(CommandBuffer* buf, uint8_t event_type,
|
||||
const volatile void* address) {
|
||||
PRINT_NAME();
|
||||
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1) < 64) {
|
||||
LOGF("\t event_type = 0x%02" PRIx8 "\n"
|
||||
"\t address = 0x%016" PRIx64 "\n",
|
||||
event_type, reinterpret_cast<uint64_t>(address));
|
||||
}
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(event_type > 0x3fu);
|
||||
|
||||
@@ -3011,17 +2937,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbAcquireMem(CommandBuffer* buf, uint8_t engine, uin
|
||||
uint64_t size_bytes, uint32_t poll_cycles) {
|
||||
PRINT_NAME();
|
||||
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1) < 64) {
|
||||
LOGF("\t engine = 0x%02" PRIx8 "\n"
|
||||
"\t cb_db_op = 0x%08" PRIx32 "\n"
|
||||
"\t gcr_cntl = 0x%08" PRIx32 "\n"
|
||||
"\t base = 0x%016" PRIx64 "\n"
|
||||
"\t size_bytes = 0x%016" PRIx64 "\n"
|
||||
"\t poll_cycles = %" PRIu32 "\n",
|
||||
engine, cb_db_op, gcr_cntl, reinterpret_cast<uint64_t>(base), size_bytes, poll_cycles);
|
||||
}
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
auto* cmd = buf->AllocateDW(8);
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ int KYTY_SYSV_ABI AgcInit(uint32_t* state, uint32_t ver);
|
||||
void* KYTY_SYSV_ABI AgcGetRegisterDefaults2(uint32_t ver);
|
||||
void* KYTY_SYSV_ABI AgcGetRegisterDefaults2Internal(uint32_t ver);
|
||||
int KYTY_SYSV_ABI AgcCreateShader(Shader** dst, void* header, const volatile void* code);
|
||||
int KYTY_SYSV_ABI AgcUnknownGetFusedShaderSize(SizeAlign* dst, const Shader* front,
|
||||
int KYTY_SYSV_ABI AgcGetFusedShaderSize(SizeAlign* dst, const Shader* front,
|
||||
const Shader* back);
|
||||
int KYTY_SYSV_ABI AgcFuseShaderHalves_0200(Shader* fused_result, const Shader* front,
|
||||
const Shader* back, void* scratch_mem);
|
||||
|
||||
@@ -841,6 +841,12 @@ struct AudioOutPortState {
|
||||
uint64_t reserved2[2];
|
||||
};
|
||||
|
||||
struct AudioOutSystemState {
|
||||
float loudness;
|
||||
uint8_t reserved8[4];
|
||||
uint64_t reserved64[3];
|
||||
};
|
||||
|
||||
int KYTY_SYSV_ABI AudioOutInit() {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -952,6 +958,19 @@ int KYTY_SYSV_ABI AudioOutGetPortState(int handle, AudioOutPortState* state) {
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI AudioOutGetSystemState(AudioOutSystemState* state) {
|
||||
PRINT_NAME();
|
||||
|
||||
if (state == nullptr) {
|
||||
return AUDIO_OUT_ERROR_INVALID_POINTER;
|
||||
}
|
||||
|
||||
// Loudness is undefined until measurement starts; no measurement is emulated.
|
||||
*state = {};
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI AudioOutSetVolume(int handle, uint32_t flag, int* vol) {
|
||||
PRINT_NAME();
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace AudioOut {
|
||||
|
||||
struct AudioOutOutputParam;
|
||||
struct AudioOutPortState;
|
||||
struct AudioOutSystemState;
|
||||
|
||||
int KYTY_SYSV_ABI AudioOutInit();
|
||||
int KYTY_SYSV_ABI AudioOutOpen(int user_id, int type, int index, uint32_t len, uint32_t freq,
|
||||
@@ -27,6 +28,7 @@ int KYTY_SYSV_ABI AudioOutOutputs(AudioOutOutputParam* param, uint32_t num);
|
||||
int KYTY_SYSV_ABI AudioOutOutput(int handle, const void* ptr);
|
||||
int KYTY_SYSV_ABI AudioOutClose(int handle);
|
||||
int KYTY_SYSV_ABI AudioOutGetPortState(int handle, AudioOutPortState* state);
|
||||
int KYTY_SYSV_ABI AudioOutGetSystemState(AudioOutSystemState* state);
|
||||
|
||||
} // namespace AudioOut
|
||||
|
||||
|
||||
+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_
|
||||
@@ -18,7 +18,7 @@ LIB_DEFINE(InitAgcDriver_1) {
|
||||
LIB_FUNC("2JtWUUiYBXs", Gen5::AgcGetRegisterDefaults2);
|
||||
LIB_FUNC("wRbq6ZjNop4", Gen5::AgcGetRegisterDefaults2Internal);
|
||||
LIB_FUNC("f3dg2CSgRKY", Gen5::AgcCreateShader);
|
||||
LIB_FUNC("dolOmWH+huQ", Gen5::AgcUnknownGetFusedShaderSize);
|
||||
LIB_FUNC("dolOmWH+huQ", Gen5::AgcGetFusedShaderSize);
|
||||
LIB_FUNC("fd5Bp5tGTgo", Gen5::AgcFuseShaderHalves_0200);
|
||||
LIB_FUNC("nApJjpKNBl4", Gen5::AgcFuseShaderHalves);
|
||||
LIB_FUNC("vcmNN+AAXnY", Gen5::AgcSetCxRegIndirectPatchSetAddress);
|
||||
|
||||
@@ -131,6 +131,7 @@ LIB_DEFINE(InitAudio_1_AudioOut) {
|
||||
LIB_FUNC("QOQtbeDqsT4", AudioOut::AudioOutOutput);
|
||||
LIB_FUNC("s1--uE9mBFw", AudioOut::AudioOutClose);
|
||||
LIB_FUNC("GrQ9s4IrNaQ", AudioOut::AudioOutGetPortState);
|
||||
LIB_FUNC("R5hemoKKID8", AudioOut::AudioOutGetSystemState);
|
||||
}
|
||||
|
||||
} // namespace LibAudioOut
|
||||
|
||||
+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
|
||||
@@ -1341,14 +1341,6 @@ static int KYTY_SYSV_ABI KernelGetOperationMode(int* mode, int* submode) {
|
||||
return OK;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI KernelFsync(int fd) {
|
||||
PRINT_NAME();
|
||||
|
||||
LOGF("\t fd = %d\n", fd);
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
static void KYTY_SYSV_ABI KernelSync() {
|
||||
PRINT_NAME();
|
||||
}
|
||||
@@ -1986,6 +1978,12 @@ int KYTY_SYSV_ABI ftruncate(int d, int64_t length) {
|
||||
return POSIX_CALL(LibKernel::FileSystem::KernelFtruncate(d, length));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI fsync(int d) {
|
||||
PRINT_NAME();
|
||||
|
||||
return POSIX_CALL(LibKernel::FileSystem::KernelFsync(d));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI socket(int family, int type, int protocol) {
|
||||
PRINT_NAME();
|
||||
return Network::Net::Socket(family, type, protocol);
|
||||
@@ -2174,6 +2172,7 @@ LIB_DEFINE(InitLibKernel_1_Posix) {
|
||||
LIB_FUNC("E6ao34wPw+U", stat);
|
||||
LIB_FUNC("JGMio+21L4c", mkdir);
|
||||
LIB_FUNC("ih4CD9-gghM", Posix::ftruncate);
|
||||
LIB_FUNC("juWbTNM+8hw", Posix::fsync);
|
||||
LIB_FUNC("8nY19bKoiZk", Posix::fcntl);
|
||||
LIB_FUNC("pDuPEf3m4fI", Posix::sem_init);
|
||||
LIB_FUNC("cDW233RAwWo", Posix::sem_destroy);
|
||||
@@ -3394,7 +3393,7 @@ LIB_DEFINE(InitLibKernel_1) {
|
||||
LIB_FUNC("959qrazPIrg", LibKernel::KernelGetProcParam);
|
||||
LIB_FUNC("tU5e3f9gSiU", LibKernel::KernelIsTrinityMode);
|
||||
LIB_FUNC("NH6xARDOVv8", LibKernel::KernelGetOperationMode);
|
||||
LIB_FUNC("fTx66l5iWIA", LibKernel::KernelFsync);
|
||||
LIB_FUNC("fTx66l5iWIA", FileSystem::KernelFsync);
|
||||
LIB_FUNC("uvT2iYBBnkY", LibKernel::KernelSync);
|
||||
LIB_FUNC("HoLVWNanBBc", LibKernel::getpid);
|
||||
LIB_FUNC("9BcDykPmo1I", LibKernel::get_error_addr);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user