mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-27 16:02:37 +00:00
Compare commits
86
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0685bffb2f | ||
|
|
a5b3673a39 | ||
|
|
09753bf8ff | ||
|
|
9d8c458535 | ||
|
|
358333125d | ||
|
|
fd6b28c4b2 | ||
|
|
6cee2dc0bd | ||
|
|
a526a8b9c9 | ||
|
|
1ec69f8e7d | ||
|
|
2463e3aaf0 | ||
|
|
556538bed5 | ||
|
|
770438cee0 | ||
|
|
db12f0c5fb | ||
|
|
3f9666acbb | ||
|
|
7eb8abb279 | ||
|
|
91a5ff6cb7 | ||
|
|
95a507a8ff | ||
|
|
b2053c2861 | ||
|
|
9801fd862b | ||
|
|
78adb9293d | ||
|
|
0a72641678 | ||
|
|
99f4eba223 | ||
|
|
04bfd9070c | ||
|
|
ffa3992c1a | ||
|
|
1b70938052 | ||
|
|
914a3bb4b8 | ||
|
|
ffb6ad6cab | ||
|
|
ff5d16eefc | ||
|
|
d118afbeae | ||
|
|
a3ac6d0d32 | ||
|
|
f297c0bfe1 | ||
|
|
20bb268a48 | ||
|
|
d3768c00e9 | ||
|
|
dfcf1c0b03 | ||
|
|
de3ed905c8 | ||
|
|
0a140911ad | ||
|
|
2483573651 | ||
|
|
986721f5bc | ||
|
|
f90a8b1589 | ||
|
|
17a0f53d39 | ||
|
|
943bc19f46 | ||
|
|
5429888d4c | ||
|
|
025d1f7edf | ||
|
|
3c62269751 | ||
|
|
ceb154d9e4 | ||
|
|
26ca609236 | ||
|
|
4ebae87a1b | ||
|
|
b4209dde1b | ||
|
|
72d7da7266 | ||
|
|
ce5e57aa36 | ||
|
|
6357740fa1 | ||
|
|
be1c84f4d7 | ||
|
|
560756f34c | ||
|
|
52c80be266 | ||
|
|
22a9392d23 | ||
|
|
95bbc9c65d | ||
|
|
c5f7644751 | ||
|
|
4db225008b | ||
|
|
dc3bd68dee | ||
|
|
c1dfb309a1 | ||
|
|
ba7f9969ef | ||
|
|
7524e8f79e | ||
|
|
153dadd0b3 | ||
|
|
463a096249 | ||
|
|
597137e8b1 | ||
|
|
b2c9ec7609 | ||
|
|
99044c7c49 | ||
|
|
83f8bfd3c9 | ||
|
|
dd5bc26c64 | ||
|
|
72903bfce6 | ||
|
|
59d8256b36 | ||
|
|
9a20c6f738 | ||
|
|
67c89e0915 | ||
|
|
5d8381e0f3 | ||
|
|
e864cb20bd | ||
|
|
40aaab482e | ||
|
|
ec8b75b244 | ||
|
|
9210264d47 | ||
|
|
66e21a6245 | ||
|
|
87e49282b8 | ||
|
|
1816004357 | ||
|
|
bfc377627a | ||
|
|
4b651f2ba7 | ||
|
|
8c01d8e849 | ||
|
|
e0c5ae090c | ||
|
|
906d25568f |
No files matched your search
@@ -32,7 +32,7 @@ jobs:
|
||||
windows:
|
||||
name: Build KytyPS5 (Windows)
|
||||
needs: metadata
|
||||
runs-on: windows-2022
|
||||
runs-on: windows-2025
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
Vendored
+22
@@ -42,6 +42,28 @@ if(NOT KYTY_CLANG_CL)
|
||||
endif()
|
||||
install(FILES "${openxr_SOURCE_DIR}/LICENSE" DESTINATION "licenses/openxr")
|
||||
|
||||
# Noto Sans v2.015 (SIL Open Font License 1.1) is libFont's fallback font.
|
||||
set(KYTY_NOTO_SANS_REPOSITORY "https://raw.githubusercontent.com/notofonts/notofonts.github.io")
|
||||
set(KYTY_NOTO_SANS_COMMIT 42af67ccd12bc0e31ed85384bf73e39248265374)
|
||||
set(KYTY_NOTO_SANS_BASE_URL "${KYTY_NOTO_SANS_REPOSITORY}/${KYTY_NOTO_SANS_COMMIT}/fonts")
|
||||
set(KYTY_NOTO_SANS_DIR "${FETCHCONTENT_BASE_DIR}/noto-sans")
|
||||
function(kyty_fetch_noto_sans path sha256)
|
||||
get_filename_component(name "${path}" NAME)
|
||||
file(DOWNLOAD "${KYTY_NOTO_SANS_BASE_URL}/${path}" "${KYTY_NOTO_SANS_DIR}/${name}"
|
||||
EXPECTED_HASH SHA256=${sha256})
|
||||
endfunction()
|
||||
kyty_fetch_noto_sans(LICENSE f2095b08bed08b23a6fe26112fcd679a2bee3f002eef077eb05d215ed1051bd8)
|
||||
kyty_fetch_noto_sans(NotoSans/hinted/ttf/NotoSans-Regular.ttf
|
||||
478c558ea716033cd60c03438f628dfa75694dcf6b5f6d505a2f05fd2b4f3823)
|
||||
kyty_fetch_noto_sans(NotoSans/hinted/ttf/NotoSans-Bold.ttf
|
||||
1df075a380fc7cb898acf64c1f7b3b4dd780de3caa860178bf929de35817a913)
|
||||
kyty_fetch_noto_sans(NotoSans/hinted/ttf/NotoSans-Italic.ttf
|
||||
467e3f89eeca4108bb8710a2b9e0cf2281ac56d5b0609211a83776d0505eecb5)
|
||||
kyty_fetch_noto_sans(NotoSans/hinted/ttf/NotoSans-BoldItalic.ttf
|
||||
1b602a9d6353be42c91df097a4857b69fa2696f26703d7a33b54a15d87c2622c)
|
||||
install(FILES "${KYTY_NOTO_SANS_DIR}/LICENSE" DESTINATION "licenses/noto-sans")
|
||||
set(KYTY_NOTO_SANS_DIR "${KYTY_NOTO_SANS_DIR}" PARENT_SCOPE)
|
||||
|
||||
# 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)
|
||||
|
||||
+18
-1
@@ -337,7 +337,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 OpenXR::openxr_loader)
|
||||
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 libzstd_static)
|
||||
|
||||
# Linux system libraries.
|
||||
if(LINUX)
|
||||
@@ -407,6 +407,8 @@ add_executable(scalar_provenance_tests EXCLUDE_FROM_ALL
|
||||
target_link_libraries(scalar_provenance_tests fmt::fmt xbyak::xbyak)
|
||||
target_include_directories(scalar_provenance_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(build_flags_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/BuildFlagsTests.cpp")
|
||||
|
||||
add_executable(page_manager_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/PageManagerTests.cpp"
|
||||
"${KYTY_SOURCE_DIR}/graphics/host_gpu/pageManager.cpp"
|
||||
@@ -650,6 +652,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME image_page_table COMMAND $<TARGET_FILE:image_page_table_tests>)
|
||||
add_test(NAME memory_tracker COMMAND $<TARGET_FILE:memory_tracker_tests>)
|
||||
add_test(NAME page_manager COMMAND $<TARGET_FILE:page_manager_tests>)
|
||||
add_test(NAME build_flags COMMAND $<TARGET_FILE:build_flags_tests>)
|
||||
add_test(NAME archive_file COMMAND $<TARGET_FILE:archive_file_tests>)
|
||||
add_test(NAME trophy_system COMMAND $<TARGET_FILE:trophy_system_tests>)
|
||||
add_test(NAME avplayer_file COMMAND $<TARGET_FILE:avplayer_file_tests>)
|
||||
@@ -743,6 +746,7 @@ if(BUILD_TESTING)
|
||||
image_page_table_tests
|
||||
memory_tracker_tests
|
||||
page_manager_tests
|
||||
build_flags_tests
|
||||
archive_file_tests
|
||||
trophy_system_tests
|
||||
avplayer_file_tests
|
||||
@@ -811,6 +815,19 @@ add_custom_target(kyty_touch_cursors
|
||||
add_dependencies(kyty_emulator kyty_touch_cursors)
|
||||
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/assets/cursors" DESTINATION assets)
|
||||
|
||||
set(KYTY_NOTO_SANS_FONTS
|
||||
"${KYTY_NOTO_SANS_DIR}/NotoSans-Regular.ttf"
|
||||
"${KYTY_NOTO_SANS_DIR}/NotoSans-Bold.ttf"
|
||||
"${KYTY_NOTO_SANS_DIR}/NotoSans-Italic.ttf"
|
||||
"${KYTY_NOTO_SANS_DIR}/NotoSans-BoldItalic.ttf")
|
||||
add_custom_target(kyty_noto_sans_fonts
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:kyty_emulator>/assets/fonts"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${KYTY_NOTO_SANS_FONTS}
|
||||
"$<TARGET_FILE_DIR:kyty_emulator>/assets/fonts"
|
||||
VERBATIM)
|
||||
add_dependencies(kyty_emulator kyty_noto_sans_fonts)
|
||||
install(FILES ${KYTY_NOTO_SANS_FONTS} DESTINATION assets/fonts)
|
||||
|
||||
target_link_libraries(kyty_emulator ${kyty_emulator_link_libraries})
|
||||
if (WIN32)
|
||||
target_link_libraries(kyty_emulator opengl32)
|
||||
|
||||
@@ -109,4 +109,23 @@ std::vector<AddContentEntry> FindAddContent(const std::filesystem::path& dir,
|
||||
return entries;
|
||||
}
|
||||
|
||||
std::optional<AddContentEntry> ParseAddContent(std::string_view value) {
|
||||
static const std::regex label_pattern("^[A-Z0-9]{16}$");
|
||||
const auto eq = value.find('=');
|
||||
const auto label = value.substr(0, eq);
|
||||
if (!std::regex_match(label.begin(), label.end(), label_pattern)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
AddContentEntry entry {{}, std::string(label), {}, {}};
|
||||
if (eq != std::string_view::npos) {
|
||||
entry.mount_dir = PathFromUtf8(FixFilenameSlash(value.substr(eq + 1)));
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::is_directory(entry.mount_dir, ec)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
@@ -2,7 +2,9 @@
|
||||
#define KYTY_COMMON_ADDITIONAL_CONTENT_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace Common {
|
||||
@@ -10,7 +12,7 @@ namespace Common {
|
||||
struct AddContentEntry {
|
||||
std::string content_id;
|
||||
std::string entitlement_label;
|
||||
std::filesystem::path mount_dir;
|
||||
std::filesystem::path mount_dir; // Empty for an add-on without extra data.
|
||||
std::string title;
|
||||
};
|
||||
|
||||
@@ -18,6 +20,9 @@ struct AddContentEntry {
|
||||
std::vector<AddContentEntry> FindAddContent(const std::filesystem::path& dir,
|
||||
const std::string& title_id);
|
||||
|
||||
// Parses a user add-on: "LABEL" without extra data, or "LABEL=DIR" with its data in DIR.
|
||||
std::optional<AddContentEntry> ParseAddContent(std::string_view value);
|
||||
|
||||
} // namespace Common
|
||||
|
||||
#endif /* KYTY_COMMON_ADDITIONAL_CONTENT_H_ */
|
||||
@@ -133,6 +133,10 @@ bool ShaderValidationEnabled() {
|
||||
return g_config->shader_validation_enabled;
|
||||
}
|
||||
|
||||
bool ShaderCacheEnabled() {
|
||||
return g_config->shader_cache_enabled;
|
||||
}
|
||||
|
||||
ShaderOptimizationType GetShaderOptimizationType() {
|
||||
return g_config->shader_optimization_type;
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ struct ConfigOptions {
|
||||
uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE;
|
||||
bool vulkan_validation_enabled = false;
|
||||
bool shader_validation_enabled = false;
|
||||
bool shader_cache_enabled = false;
|
||||
ShaderOptimizationType shader_optimization_type = ShaderOptimizationType::None;
|
||||
LogDirection shader_log_direction = LogDirection::Silent;
|
||||
std::filesystem::path shader_log_folder = "_Shaders";
|
||||
@@ -132,6 +133,7 @@ uint32_t GetConsoleLanguage();
|
||||
bool VulkanValidationEnabled();
|
||||
|
||||
bool ShaderValidationEnabled();
|
||||
bool ShaderCacheEnabled();
|
||||
ShaderOptimizationType GetShaderOptimizationType();
|
||||
LogDirection GetShaderLogDirection();
|
||||
std::filesystem::path GetShaderLogFolder();
|
||||
|
||||
+1
-5
@@ -99,10 +99,6 @@ static void Init(const Config::ConfigOptions& cfg, const std::filesystem::path&
|
||||
|
||||
if (Common::File::IsFileExisting(param_json)) {
|
||||
Loader::SystemContentLoadParamSfo(param_json);
|
||||
if (const auto flexible_memory_size = Loader::SystemContentGetFlexibleMemorySize();
|
||||
flexible_memory_size != 0) {
|
||||
Libs::LibKernel::Memory::SetFlexibleMemorySize(flexible_memory_size);
|
||||
}
|
||||
}
|
||||
if (cfg.splash_enabled) {
|
||||
Libs::Graphics::InitializeSplashScreen(param_json.parent_path());
|
||||
@@ -188,7 +184,7 @@ void Run(const RunOptions& options) {
|
||||
|
||||
MountSandboxDirs();
|
||||
|
||||
Loader::AddContentLoad(options.addcont_dir, title_id);
|
||||
Loader::AddContentLoad(options.addcont_dir, options.addcont, title_id);
|
||||
auto* rt = Common::Singleton<Loader::RuntimeLinker>::Instance();
|
||||
Libs::InitAll(rt->Symbols());
|
||||
|
||||
|
||||
+7
-5
@@ -1,6 +1,7 @@
|
||||
#ifndef EMULATOR_INCLUDE_EMULATOR_EMULATOR_H_
|
||||
#define EMULATOR_INCLUDE_EMULATOR_EMULATOR_H_
|
||||
|
||||
#include "common/additionalContent.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/stringUtils.h"
|
||||
|
||||
@@ -9,11 +10,12 @@
|
||||
namespace Emulator {
|
||||
|
||||
struct RunOptions {
|
||||
Config::ConfigOptions config;
|
||||
std::filesystem::path app0_dir;
|
||||
std::filesystem::path elf;
|
||||
std::filesystem::path game_patch;
|
||||
std::filesystem::path addcont_dir;
|
||||
Config::ConfigOptions config;
|
||||
std::filesystem::path app0_dir;
|
||||
std::filesystem::path elf;
|
||||
std::filesystem::path game_patch;
|
||||
std::filesystem::path addcont_dir;
|
||||
std::vector<Common::AddContentEntry> addcont;
|
||||
};
|
||||
|
||||
void Run(const RunOptions& options);
|
||||
|
||||
@@ -27,6 +27,7 @@ struct IndirectDrawRegisters {
|
||||
uint32_t vertex_offset;
|
||||
uint32_t instance_offset;
|
||||
uint32_t index_offset;
|
||||
uint32_t draw_index;
|
||||
};
|
||||
|
||||
class Pm4Execution {
|
||||
@@ -97,6 +98,7 @@ public:
|
||||
|
||||
template <typename T>
|
||||
void WaitRegMem(uint32_t func, const T* addr, T ref, T mask, uint32_t poll, uint32_t wait_op);
|
||||
void MemSemaphore(uint64_t* counter, bool signal, bool write);
|
||||
void WriteData(uint32_t* dst, const uint32_t* src, uint32_t dw_num, uint32_t write_control);
|
||||
void WriteReferenceClock(uint64_t dst_address, uint32_t num_bytes);
|
||||
void DmaData(uint8_t engine, uint8_t dst_sel, uint8_t dst_cache_policy,
|
||||
|
||||
@@ -202,6 +202,7 @@ constexpr auto MakeOpcodeDispatchTable() {
|
||||
g_cp_op_func[Pm4::IT_DMA_DATA] = CpOpDmaData;
|
||||
g_cp_op_func[Pm4::IT_ACQUIRE_MEM] = CpOpAcquireMem;
|
||||
g_cp_op_func[Pm4::IT_RELEASE_MEM] = CpOpReleaseMem;
|
||||
g_cp_op_func[Pm4::IT_MEM_SEMAPHORE] = CpOpMemSemaphore;
|
||||
g_cp_op_func[Pm4::IT_REWIND] = CpOpRewind;
|
||||
g_cp_op_func[Pm4::IT_SET_CONTEXT_REG] = CpOpSetContextReg;
|
||||
g_cp_op_func[Pm4::IT_SET_SH_REG] = CpOpSetShaderReg;
|
||||
|
||||
@@ -136,6 +136,7 @@ uint32_t CpOpEventWrite(CommandProcessor&, uint32_t, const uint32_t*, uint32_t,
|
||||
uint32_t CpOpDmaData(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
|
||||
uint32_t CpOpAcquireMem(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
|
||||
uint32_t CpOpReleaseMem(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
|
||||
uint32_t CpOpMemSemaphore(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
|
||||
uint32_t CpOpSetContextReg(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
|
||||
uint32_t CpOpSetShaderReg(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
|
||||
uint32_t CpOpSetUconfigReg(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
|
||||
|
||||
@@ -1582,7 +1582,7 @@ KYTY_CP_OP_PARSER(CpOpDrawIndirect) {
|
||||
// The native indexed packet enables the high-half first-index destination in word 3.
|
||||
const IndirectDrawRegisters registers {
|
||||
buffer[1] & 0xffffu, buffer[2] & 0xffffu,
|
||||
indexed && (buffer[2] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP};
|
||||
indexed && (buffer[2] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP, Pm4::SH_NOP};
|
||||
|
||||
cp.DrawIndirect(data_offset, registers, draw_initiator, indexed);
|
||||
|
||||
@@ -1602,10 +1602,12 @@ KYTY_CP_OP_PARSER(CpOpDrawIndirectMulti) {
|
||||
const auto stride_in_bytes = buffer[7];
|
||||
const auto draw_initiator = buffer[8];
|
||||
const bool indexed = (cmd_id == 0xc0083800);
|
||||
// Multi-draw moves the first-index enable to word 4; the destinations stay in 2/3.
|
||||
// Multi-draw moves the first-index enable to word 4; the destinations stay in 2/3. Word 4
|
||||
// bit 31 enables the subdraw index, written to the register in its low half.
|
||||
const IndirectDrawRegisters registers {
|
||||
buffer[1] & 0xffffu, buffer[2] & 0xffffu,
|
||||
indexed && (buffer[3] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP};
|
||||
indexed && (buffer[3] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP,
|
||||
(buffer[3] >> 31u) != 0 ? buffer[3] & 0xffffu : Pm4::SH_NOP};
|
||||
|
||||
if (count_indirect == 0) {
|
||||
count_addr = nullptr;
|
||||
@@ -2058,6 +2060,21 @@ KYTY_CP_OP_PARSER(CpOpReleaseMem) {
|
||||
return 7;
|
||||
}
|
||||
|
||||
KYTY_CP_OP_PARSER(CpOpMemSemaphore) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
|
||||
const auto control = buffer[2];
|
||||
EXIT_NOT_IMPLEMENTED(cmd_id != 0xc0023900 || (control & ~0xe0110000u) != 0 ||
|
||||
(control >> 29u) < 6u);
|
||||
|
||||
auto* counter = reinterpret_cast<uint64_t*>(
|
||||
(buffer[0] & ~0x7u) | (static_cast<uint64_t>(buffer[1] & 0x3ffffu) << 32u));
|
||||
// A signal updates the counter before the next packet, so its mailbox stall never waits.
|
||||
cp.MemSemaphore(counter, (control >> 29u) == 6u, ((control >> 20u) & 0x1u) != 0);
|
||||
|
||||
return 3;
|
||||
}
|
||||
|
||||
KYTY_CP_OP_PARSER(CpOpSetContextReg) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
|
||||
|
||||
@@ -281,6 +281,16 @@ template void CommandProcessor::WaitRegMem<uint32_t>(uint32_t, const uint32_t*,
|
||||
template void CommandProcessor::WaitRegMem<uint64_t>(uint32_t, const uint64_t*, uint64_t, uint64_t,
|
||||
uint32_t, uint32_t);
|
||||
|
||||
void CommandProcessor::MemSemaphore(uint64_t* counter, bool signal, bool write) {
|
||||
if (signal) {
|
||||
*counter = write ? 1u : *counter + 1u;
|
||||
} else if (*counter == 0) {
|
||||
SuspendPm4();
|
||||
} else {
|
||||
(*counter)--;
|
||||
}
|
||||
}
|
||||
|
||||
void CommandProcessor::WriteData(uint32_t* dst, const uint32_t* src, uint32_t dw_num,
|
||||
uint32_t write_control) {
|
||||
const uint32_t dst_sel = ((write_control >> 30u) & 0x1u) | ((write_control >> 7u) & 0x1eu);
|
||||
@@ -789,7 +799,7 @@ void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_cou
|
||||
|
||||
static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters registers,
|
||||
uint32_t vertex_offset, uint32_t instance_offset,
|
||||
uint32_t first_index) {
|
||||
uint32_t first_index, uint32_t draw_index) {
|
||||
const auto write = [&](uint32_t location, uint32_t value) {
|
||||
if (location == Pm4::SH_NOP) {
|
||||
return;
|
||||
@@ -801,6 +811,28 @@ static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters
|
||||
write(registers.vertex_offset, vertex_offset);
|
||||
write(registers.instance_offset, instance_offset);
|
||||
write(registers.index_offset, first_index);
|
||||
write(registers.draw_index, draw_index);
|
||||
}
|
||||
|
||||
// Writes the GS user-data slots the packet patches; false when the shaders cannot read the
|
||||
// offsets from GPU records: a first-index or subdraw-index destination, or another stage's
|
||||
// user data.
|
||||
static bool IndirectDrawOffsets(IndirectDrawRegisters registers, ShaderIndirectOffsets& offsets) {
|
||||
if (registers.index_offset != Pm4::SH_NOP || registers.draw_index != Pm4::SH_NOP) {
|
||||
return false;
|
||||
}
|
||||
for (const auto [location, slot]: {std::pair {registers.vertex_offset, &offsets.vertex},
|
||||
std::pair {registers.instance_offset, &offsets.instance}}) {
|
||||
if (location == Pm4::SH_NOP) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t index = location - Pm4::SPI_SHADER_USER_DATA_GS_0;
|
||||
if (index >= HW::UserSgprInfo::SGPRS_MAX) {
|
||||
return false;
|
||||
}
|
||||
*slot = static_cast<uint8_t>(index);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CommandProcessor::DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers,
|
||||
@@ -834,12 +866,11 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
|
||||
// NUM_INSTANCES keeps the last draw's instance count.
|
||||
m_num_instances_addr = args_addr + static_cast<uint64_t>(draw_count - 1) * stride_in_bytes +
|
||||
offsetof(vk::DrawIndirectCommand, instanceCount);
|
||||
// With no SGPRs to patch, the GPU reads the records if the shaders allow. Shaders apply
|
||||
// the records' offsets only through those SGPRs; Vulkan applies them, which matches
|
||||
// while they are zero.
|
||||
const DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes};
|
||||
if (registers.vertex_offset == Pm4::SH_NOP && registers.instance_offset == Pm4::SH_NOP &&
|
||||
registers.index_offset == Pm4::SH_NOP && (!indexed || m_index_buffer_size != 0) &&
|
||||
// The GPU reads the records if the shaders allow; the VS prolog loads the patched SGPRs from
|
||||
// Vulkan's bases. With no SGPR patched, Vulkan applies the offsets, which matches while they
|
||||
// are zero.
|
||||
DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes};
|
||||
if (IndirectDrawOffsets(registers, records.offsets) && (!indexed || m_index_buffer_size != 0) &&
|
||||
(indexed ? executor.DrawIndex(m_submit_id, CurrentBuffer(),
|
||||
{.index_count = m_index_buffer_size,
|
||||
.index_addr = IndexAddress(0),
|
||||
@@ -854,7 +885,7 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
|
||||
const auto addr = args_addr + static_cast<uint64_t>(i) * stride_in_bytes;
|
||||
if (!indexed) {
|
||||
const auto& args = *reinterpret_cast<const vk::DrawIndirectCommand*>(addr);
|
||||
PatchIndirectDrawOffsets(*this, registers, args.firstVertex, args.firstInstance, 0);
|
||||
PatchIndirectDrawOffsets(*this, registers, args.firstVertex, args.firstInstance, 0, i);
|
||||
executor.DrawAuto(m_submit_id, CurrentBuffer(),
|
||||
{.vertex_count = args.vertexCount,
|
||||
.instance_count = args.instanceCount,
|
||||
@@ -866,7 +897,7 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
|
||||
|
||||
const auto& args = *reinterpret_cast<const vk::DrawIndexedIndirectCommand*>(addr);
|
||||
PatchIndirectDrawOffsets(*this, registers, static_cast<uint32_t>(args.vertexOffset),
|
||||
args.firstInstance, args.firstIndex);
|
||||
args.firstInstance, args.firstIndex, i);
|
||||
executor.DrawIndex(m_submit_id, CurrentBuffer(),
|
||||
{.index_count = std::min(args.indexCount, max_indices),
|
||||
.index_addr = IndexAddress(args.firstIndex),
|
||||
@@ -964,16 +995,28 @@ void CommandProcessor::ReleaseMem(uint32_t data_sel, uint32_t interrupt_selector
|
||||
// Dumped occlusion counters are written before any end-of-pipe signal reaches the guest.
|
||||
m_renderer.GetOcclusionQueries().Publish();
|
||||
|
||||
// A label beside GPU-written bytes goes to the backing store, so they are not downloaded.
|
||||
// Other labels go through the guest mapping: its fault keeps a clean cached copy current and
|
||||
// downloads GPU-written bytes under the label before the store.
|
||||
const auto write_label = [this, address](const void* value, uint64_t size) {
|
||||
const auto vaddr = reinterpret_cast<uint64_t>(address);
|
||||
auto& buffers = m_renderer.GetBufferCache();
|
||||
if (!GuestRange {vaddr, size}.Valid() || !buffers.IsRegionGpuModified(vaddr, size) ||
|
||||
buffers.HasGpuDirtyBytes(vaddr, size) ||
|
||||
!LibKernel::Memory::TryWriteBacking(vaddr, value, size)) {
|
||||
std::memcpy(address, value, size);
|
||||
}
|
||||
};
|
||||
// EndOfPipeData: kNone, kImmediate32b, kImmediate64b, kRefClk; 5 reads GDS.
|
||||
switch (data_sel) {
|
||||
case 0: break;
|
||||
case 1: {
|
||||
const auto value = static_cast<uint32_t>(data);
|
||||
std::memcpy(address, &value, sizeof(value));
|
||||
write_label(&value, sizeof(value));
|
||||
break;
|
||||
}
|
||||
case 3: data = Sync::ReadReferenceClock(); [[fallthrough]];
|
||||
case 2: std::memcpy(address, &data, sizeof(data)); break;
|
||||
case 2: write_label(&data, sizeof(data)); break;
|
||||
case 5: {
|
||||
GetScheduler().Finish();
|
||||
const auto gds = static_cast<uint32_t>(data);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
@@ -118,6 +119,13 @@ public:
|
||||
|
||||
class const_iterator final {
|
||||
public:
|
||||
// libstdc++'s algorithms need these. Input, not forward, because operator* returns a copy.
|
||||
using iterator_category = std::input_iterator_tag;
|
||||
using value_type = OwnerT;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using pointer = void;
|
||||
using reference = OwnerT;
|
||||
|
||||
[[nodiscard]] OwnerT operator*() const noexcept { return m_owners->At(m_index); }
|
||||
const_iterator& operator++() noexcept {
|
||||
++m_index;
|
||||
|
||||
+52
-31
@@ -488,16 +488,15 @@ void TextureCache::RefreshCopySource(ImageId id) {
|
||||
}
|
||||
|
||||
bool TextureCache::CopyD16(Image& destination, Image& source) {
|
||||
const bool source_depth = source.info.IsDepth();
|
||||
const bool destination_depth = destination.info.IsDepth();
|
||||
if (source_depth == destination_depth) {
|
||||
// Converts guest D16 between a four-byte host depth aspect and two-byte color or D16 depth.
|
||||
const bool source_wide = DepthAspectTransferBytes(source.backing.format) == sizeof(uint32_t);
|
||||
if (source_wide == (DepthAspectTransferBytes(destination.backing.format) == sizeof(uint32_t))) {
|
||||
return false;
|
||||
}
|
||||
auto& depth = source_depth ? source : destination;
|
||||
auto& color = source_depth ? destination : source;
|
||||
const auto transfer_bytes = DepthAspectTransferBytes(depth.backing.format);
|
||||
auto& depth = source_wide ? source : destination;
|
||||
auto& narrow = source_wide ? destination : source;
|
||||
if (depth.info.bytes_per_block != sizeof(uint16_t) ||
|
||||
color.info.bytes_per_block != sizeof(uint16_t) || transfer_bytes != sizeof(uint32_t)) {
|
||||
narrow.info.bytes_per_block != sizeof(uint16_t)) {
|
||||
return false;
|
||||
}
|
||||
EXIT_IF(source.backing.samples != 1 || destination.backing.samples != 1 ||
|
||||
@@ -507,42 +506,42 @@ bool TextureCache::CopyD16(Image& destination, Image& source) {
|
||||
|
||||
const auto layers = depth.info.resources.layers;
|
||||
const uint64_t depth_slice =
|
||||
static_cast<uint64_t>(depth.info.pitch) * depth.info.extent.height * transfer_bytes;
|
||||
const uint64_t color_slice =
|
||||
static_cast<uint64_t>(color.info.pitch) * color.info.extent.height * sizeof(uint16_t);
|
||||
EXIT_IF(layers == 0 || depth_slice > UINT64_MAX / layers || color_slice > UINT64_MAX / layers);
|
||||
const auto depth_size = depth_slice * layers;
|
||||
const auto color_size = color_slice * layers;
|
||||
const std::array depth_copies {
|
||||
static_cast<uint64_t>(depth.info.pitch) * depth.info.extent.height * sizeof(uint32_t);
|
||||
const uint64_t narrow_slice =
|
||||
static_cast<uint64_t>(narrow.info.pitch) * narrow.info.extent.height * sizeof(uint16_t);
|
||||
EXIT_IF(layers == 0 || depth_slice > UINT64_MAX / layers || narrow_slice > UINT64_MAX / layers);
|
||||
const std::array depth_copies {
|
||||
vk::BufferImageCopy {0,
|
||||
depth.info.pitch,
|
||||
depth.info.extent.height,
|
||||
{vk::ImageAspectFlagBits::eDepth, 0, 0, layers},
|
||||
{},
|
||||
depth.info.extent}};
|
||||
const std::array color_copies {
|
||||
const std::array narrow_copies {
|
||||
vk::BufferImageCopy {0,
|
||||
color.info.pitch,
|
||||
color.info.extent.height,
|
||||
{vk::ImageAspectFlagBits::eColor, 0, 0, layers},
|
||||
narrow.info.pitch,
|
||||
narrow.info.extent.height,
|
||||
{narrow.info.IsDepth() ? vk::ImageAspectFlagBits::eDepth
|
||||
: vk::ImageAspectFlagBits::eColor,
|
||||
0, 0, layers},
|
||||
{},
|
||||
color.info.extent}};
|
||||
narrow.info.extent}};
|
||||
|
||||
auto depth_buffer = m_tiler.GetScratchBuffer(depth_size);
|
||||
auto color_buffer = m_tiler.GetScratchBuffer(color_size, depth_buffer.buffer);
|
||||
auto depth_buffer = m_tiler.GetScratchBuffer(depth_slice * layers);
|
||||
auto narrow_buffer = m_tiler.GetScratchBuffer(narrow_slice * layers, depth_buffer.buffer);
|
||||
const TileManager::D16Layout promote_layout {
|
||||
.width = depth.info.extent.width,
|
||||
.height = depth.info.extent.height,
|
||||
.layers = layers,
|
||||
.source_row_stride = static_cast<uint64_t>(color.info.pitch) * sizeof(uint16_t),
|
||||
.target_row_stride = static_cast<uint64_t>(depth.info.pitch) * transfer_bytes,
|
||||
.source_slice_stride = color_slice,
|
||||
.source_row_stride = static_cast<uint64_t>(narrow.info.pitch) * sizeof(uint16_t),
|
||||
.target_row_stride = static_cast<uint64_t>(depth.info.pitch) * sizeof(uint32_t),
|
||||
.source_slice_stride = narrow_slice,
|
||||
.target_slice_stride = depth_slice,
|
||||
};
|
||||
const bool d32 = DepthAspectTransferFormat(depth.backing.format) == vk::Format::eD32Sfloat;
|
||||
if (source_depth) {
|
||||
if (source_wide) {
|
||||
source.Download(depth_copies, depth_buffer.buffer, depth_buffer.offset, depth_buffer.size);
|
||||
m_tiler.ConvertD16(depth_buffer, color_buffer, TileManager::D16Direction::Demote, d32,
|
||||
m_tiler.ConvertD16(depth_buffer, narrow_buffer, TileManager::D16Direction::Demote, d32,
|
||||
{.width = promote_layout.width,
|
||||
.height = promote_layout.height,
|
||||
.layers = promote_layout.layers,
|
||||
@@ -550,11 +549,12 @@ bool TextureCache::CopyD16(Image& destination, Image& source) {
|
||||
.target_row_stride = promote_layout.source_row_stride,
|
||||
.source_slice_stride = promote_layout.target_slice_stride,
|
||||
.target_slice_stride = promote_layout.source_slice_stride});
|
||||
destination.Upload(color_copies, color_buffer.buffer, color_buffer.offset,
|
||||
color_buffer.size);
|
||||
destination.Upload(narrow_copies, narrow_buffer.buffer, narrow_buffer.offset,
|
||||
narrow_buffer.size);
|
||||
} else {
|
||||
source.Download(color_copies, color_buffer.buffer, color_buffer.offset, color_buffer.size);
|
||||
m_tiler.ConvertD16(color_buffer, depth_buffer, TileManager::D16Direction::Promote, d32,
|
||||
source.Download(narrow_copies, narrow_buffer.buffer, narrow_buffer.offset,
|
||||
narrow_buffer.size);
|
||||
m_tiler.ConvertD16(narrow_buffer, depth_buffer, TileManager::D16Direction::Promote, d32,
|
||||
promote_layout);
|
||||
destination.Upload(depth_copies, depth_buffer.buffer, depth_buffer.offset,
|
||||
depth_buffer.size);
|
||||
@@ -783,7 +783,15 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques
|
||||
if (!cached.registered) {
|
||||
return {merged_id};
|
||||
}
|
||||
if (BcAlias::Matches(requested, cached.info) && !cached.IsCpuDirty()) {
|
||||
// PS5: image heaps keep BC images and raw block views of their layers resident together, so a
|
||||
// raw read of a layer subset with the same levels views the BC image instead of evicting it.
|
||||
const bool raw_view = binding == BindingType::Texture && !requested.IsBlock() &&
|
||||
requested.type == cached.info.type &&
|
||||
requested.data.address == cached.info.data.address &&
|
||||
requested.BlockExtent() == cached.info.BlockExtent() &&
|
||||
requested.resources.levels == cached.info.resources.levels &&
|
||||
requested.resources.layers < cached.info.resources.layers;
|
||||
if (!raw_view && BcAlias::Matches(requested, cached.info) && !cached.IsCpuDirty()) {
|
||||
BcAlias::Resolve(*this, cached_id, merged_id);
|
||||
return {merged_id};
|
||||
}
|
||||
@@ -1302,6 +1310,19 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
|
||||
view_layer = overlap.layer;
|
||||
}
|
||||
}
|
||||
// PS5: image heaps hold a descriptor per dynamic-resolution extent of a render target,
|
||||
// and only one extent at an address holds data, so heap lookups create no second one.
|
||||
if (!result && desc.heap &&
|
||||
std::any_of(candidates.begin(), candidates.end(), [&](ImageId id) {
|
||||
const auto* image = m_slot_images.try_get(id);
|
||||
return image != nullptr && image->registered &&
|
||||
image->heap_refs.load(std::memory_order_relaxed) != 0u &&
|
||||
image->info.data.address == desc.info.data.address &&
|
||||
image->info.type == desc.info.type &&
|
||||
image->info.extent != desc.info.extent;
|
||||
})) {
|
||||
return {};
|
||||
}
|
||||
} else if (block_alias) {
|
||||
for (const auto candidate: candidates) {
|
||||
const auto& owner = m_slot_images[candidate];
|
||||
|
||||
@@ -43,6 +43,7 @@ public:
|
||||
ImageInfo info;
|
||||
ImageViewInfo view_info;
|
||||
BindingType type = BindingType::Texture;
|
||||
bool heap = false; // PS5: resolved for a resident image heap.
|
||||
};
|
||||
|
||||
TextureCache(GraphicContext& graphics, CommandScheduler& scheduler, PageManager& page_manager,
|
||||
|
||||
@@ -595,13 +595,6 @@ void LogDrawPhase(const char* draw_name, const char* phase) {
|
||||
}
|
||||
}
|
||||
|
||||
void LogPipelineTrace(const char* phase, uint64_t vertex_program_id, uint64_t pixel_program_id) {
|
||||
if (graphics_debug_dump_enabled()) {
|
||||
LOGF("PipelineTrace: %s VS=%" PRIu64 " PS=%" PRIu64 "\n", phase, vertex_program_id,
|
||||
pixel_program_id);
|
||||
}
|
||||
}
|
||||
|
||||
static void VpPrint(const char* func, const HW::ScreenViewport& vp,
|
||||
const HW::ScanModeControl& smc) {
|
||||
LOGF("%s\n", func);
|
||||
|
||||
@@ -125,6 +125,14 @@ void DynamicState::Commit(const GraphicContext& graphics, vk::CommandBuffer cmdb
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dirty_state.cull_mode) {
|
||||
dirty_state.cull_mode = false;
|
||||
cmdbuf.setCullMode(cull_mode);
|
||||
}
|
||||
if (dirty_state.front_face) {
|
||||
dirty_state.front_face = false;
|
||||
cmdbuf.setFrontFace(front_face);
|
||||
}
|
||||
if (dirty_state.blend_constants) {
|
||||
dirty_state.blend_constants = false;
|
||||
cmdbuf.setBlendConstants(blend_constants.data());
|
||||
|
||||
@@ -52,6 +52,9 @@ struct DynamicState {
|
||||
bool stencil_back_write_mask : 1;
|
||||
bool stencil_back_compare_mask : 1;
|
||||
|
||||
bool cull_mode : 1;
|
||||
bool front_face : 1;
|
||||
|
||||
bool blend_constants : 1;
|
||||
bool color_write_enables : 1;
|
||||
bool line_width : 1;
|
||||
@@ -86,6 +89,9 @@ struct DynamicState {
|
||||
uint32_t stencil_back_write_mask {};
|
||||
uint32_t stencil_back_compare_mask {};
|
||||
|
||||
vk::CullModeFlags cull_mode {};
|
||||
vk::FrontFace front_face {};
|
||||
|
||||
std::array<float, 4> blend_constants {};
|
||||
std::array<vk::Bool32, RENDER_COLOR_ATTACHMENTS_MAX> color_write_enables {};
|
||||
uint32_t num_color_write_enables {};
|
||||
@@ -221,6 +227,20 @@ struct DynamicState {
|
||||
}
|
||||
}
|
||||
|
||||
void SetCullMode(const vk::CullModeFlags cull_mode_) {
|
||||
if (cull_mode != cull_mode_) {
|
||||
cull_mode = cull_mode_;
|
||||
dirty_state.cull_mode = true;
|
||||
}
|
||||
}
|
||||
|
||||
void SetFrontFace(const vk::FrontFace front_face_) {
|
||||
if (front_face != front_face_) {
|
||||
front_face = front_face_;
|
||||
dirty_state.front_face = true;
|
||||
}
|
||||
}
|
||||
|
||||
void SetBlendConstants(const std::array<float, 4> blend_constants_) {
|
||||
if (blend_constants != blend_constants_) {
|
||||
blend_constants = blend_constants_;
|
||||
|
||||
@@ -425,15 +425,15 @@ void Image::Resolve(Image& source, const ImageSubresourceRange& source_range,
|
||||
const auto destination_height =
|
||||
std::max(backing.extent.height >> destination_range.base_level, 1u);
|
||||
const bool copy = source.backing.samples == 1;
|
||||
EXIT_IF(layers == 0 || info.extent.width > source_width || info.extent.height > source_height ||
|
||||
info.extent.width > destination_width || info.extent.height > destination_height ||
|
||||
EXIT_IF(layers == 0 ||
|
||||
(copy ? !ImageViewOps::FormatsCompatible(source.backing.format, backing.format)
|
||||
: source.backing.format != backing.format));
|
||||
auto resolved_source_range = source_range;
|
||||
auto resolved_destination_range = destination_range;
|
||||
resolved_source_range.layer_count = layers;
|
||||
resolved_destination_range.layer_count = layers;
|
||||
const vk::Extent3D resolve_extent {info.extent.width, info.extent.height, 1};
|
||||
const vk::Extent3D resolve_extent {std::min(source_width, destination_width),
|
||||
std::min(source_height, destination_height), 1};
|
||||
|
||||
m_scheduler.EndRendering();
|
||||
auto command = m_scheduler.Current().Handle();
|
||||
|
||||
@@ -378,6 +378,9 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
|
||||
const auto encoded = static_cast<float>(value & 0xffu) / 255.0f;
|
||||
return encoded <= 0.04045f ? encoded / 12.92f : std::pow((encoded + 0.055f) / 1.055f, 2.4f);
|
||||
};
|
||||
const auto unorm16 = [](uint64_t value) {
|
||||
return static_cast<float>(value & 0xffffu) / 65535.0f;
|
||||
};
|
||||
const auto float16 = [](uint32_t value) {
|
||||
const auto sign = (value & 0x8000u) << 16u;
|
||||
const auto exponent = (value >> 10u) & 0x1fu;
|
||||
@@ -403,19 +406,49 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
|
||||
next.float32[1] = float16(packed >> 16u);
|
||||
[[fallthrough]];
|
||||
case vk::Format::eR16Sfloat: next.float32[0] = float16(packed); break;
|
||||
case vk::Format::eR16G16Unorm:
|
||||
next.float32[1] = static_cast<float>(packed >> 16u) / 65535.0f;
|
||||
case vk::Format::eR16G16B16A16Unorm:
|
||||
next.float32[2] = unorm16(clear_words >> 32u);
|
||||
next.float32[3] = unorm16(clear_words >> 48u);
|
||||
[[fallthrough]];
|
||||
case vk::Format::eR16Unorm:
|
||||
next.float32[0] = static_cast<float>(packed & 0xffffu) / 65535.0f;
|
||||
case vk::Format::eR16G16Unorm:
|
||||
next.float32[1] = unorm16(packed >> 16u);
|
||||
[[fallthrough]];
|
||||
case vk::Format::eR16Unorm: next.float32[0] = unorm16(packed); break;
|
||||
case vk::Format::eR16G16B16A16Uint:
|
||||
for (uint32_t i = 0; i < 4; i++) {
|
||||
next.uint32[i] = static_cast<uint16_t>(clear_words >> (16u * i));
|
||||
}
|
||||
break;
|
||||
// Each clear register stores one raw 32-bit float channel.
|
||||
case vk::Format::eR16G16B16A16Sint:
|
||||
for (uint32_t i = 0; i < 4; i++) {
|
||||
next.int32[i] = static_cast<int16_t>(clear_words >> (16u * i));
|
||||
}
|
||||
break;
|
||||
// Each clear register stores one raw 32-bit channel.
|
||||
case vk::Format::eR32G32Sfloat:
|
||||
next.float32[1] = std::bit_cast<float>(static_cast<uint32_t>(clear_words >> 32u));
|
||||
[[fallthrough]];
|
||||
case vk::Format::eR32Sfloat: next.float32[0] = std::bit_cast<float>(packed); break;
|
||||
case vk::Format::eR32Uint: next.uint32[0] = packed; break;
|
||||
case vk::Format::eR32Sint: next.int32[0] = static_cast<int32_t>(packed); break;
|
||||
case vk::Format::eR32G32Uint:
|
||||
case vk::Format::eR32G32Sint:
|
||||
next.uint32[1] = static_cast<uint32_t>(clear_words >> 32u);
|
||||
[[fallthrough]];
|
||||
case vk::Format::eR32Uint:
|
||||
case vk::Format::eR32Sint: next.uint32[0] = packed; break;
|
||||
// Narrow targets hold their channels in the low bits of the first word. UFC 6 (PPSA23566)
|
||||
// fast-clears an R8_UINT map through CMASK every frame and then writes only part of it;
|
||||
// without these the clear was dropped and old contents piled up.
|
||||
case vk::Format::eR8Uint: next.uint32[0] = packed & 0xffu; break;
|
||||
case vk::Format::eR8Unorm: next.float32[0] = unorm8(packed); break;
|
||||
case vk::Format::eR8G8Uint:
|
||||
next.uint32[0] = packed & 0xffu;
|
||||
next.uint32[1] = (packed >> 8u) & 0xffu;
|
||||
break;
|
||||
case vk::Format::eR8G8Unorm:
|
||||
next.float32[0] = unorm8(packed);
|
||||
next.float32[1] = unorm8(packed >> 8u);
|
||||
break;
|
||||
case vk::Format::eR16Uint: next.uint32[0] = packed & 0xffffu; break;
|
||||
case vk::Format::eR8G8B8A8Srgb:
|
||||
next.float32[0] = srgb8(packed);
|
||||
next.float32[1] = srgb8(packed >> 8u);
|
||||
@@ -462,10 +495,16 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
|
||||
// Keep this separate: the first register word alone cannot describe a 64-bit color.
|
||||
[[nodiscard]] inline bool DecodeColorDwordFill(vk::Format format, uint32_t packed,
|
||||
vk::ClearColorValue& clear) {
|
||||
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm) &&
|
||||
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm ||
|
||||
format == vk::Format::eR16Uint || format == vk::Format::eR8G8Unorm ||
|
||||
format == vk::Format::eR8G8Uint) &&
|
||||
(packed & 0xffffu) != (packed >> 16u)) {
|
||||
return false;
|
||||
}
|
||||
if ((format == vk::Format::eR8Unorm || format == vk::Format::eR8Uint) &&
|
||||
packed != (packed & 0xffu) * 0x01010101u) {
|
||||
return false;
|
||||
}
|
||||
if (format == vk::Format::eR16G16B16A16Sfloat) {
|
||||
if (packed != 0) {
|
||||
return false;
|
||||
|
||||
@@ -484,7 +484,7 @@ void TileManager::ConvertD16(Result source, Result target, D16Direction directio
|
||||
vk::AccessFlagBits::eHostWrite |
|
||||
vk::AccessFlagBits::eTransferWrite |
|
||||
vk::AccessFlagBits::eShaderWrite;
|
||||
barriers[1].dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite;
|
||||
barriers[1].dstAccessMask = vk::AccessFlagBits::eShaderWrite;
|
||||
command.pipelineBarrier(
|
||||
vk::PipelineStageFlagBits::eAllCommands | vk::PipelineStageFlagBits::eHost,
|
||||
vk::PipelineStageFlagBits::eComputeShader, {}, 0, nullptr, 2, barriers, 0, nullptr);
|
||||
|
||||
@@ -604,8 +604,13 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
|
||||
view_levels, desc.info.resources.layers);
|
||||
desc.view_info.base_level = view_base;
|
||||
desc.type = storage ? TextureCache::BindingType::Storage : TextureCache::BindingType::Texture;
|
||||
desc.heap = resource.heap;
|
||||
|
||||
auto id = texture_cache.FindImage(desc, shader_conversion);
|
||||
auto id = texture_cache.FindImage(desc, shader_conversion);
|
||||
if (!id) {
|
||||
// Another extent at the address holds it; the heap retries once an image is created.
|
||||
return {.desc = std::move(desc), .layout = vk::ImageLayout::eGeneral};
|
||||
}
|
||||
auto* image = &texture_cache.GetImage(id);
|
||||
const bool stencil_association = static_cast<bool>(image->depth_id);
|
||||
if (stencil_association) {
|
||||
@@ -681,7 +686,7 @@ void RenderExecutor::PrepareBindings(const ShaderStageRuntime& runtime,
|
||||
PreparedBindings& prepared) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
EXIT_IF(!runtime);
|
||||
const auto& program = *runtime.program;
|
||||
const auto& program = runtime.module->info;
|
||||
const auto& snapshot = *runtime.resources;
|
||||
prepared.runtime = &runtime;
|
||||
prepared.gds = {nullptr, 0, VK_WHOLE_SIZE};
|
||||
@@ -723,7 +728,7 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
|
||||
for (auto* stage: stages) {
|
||||
auto& prepared = *stage;
|
||||
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
|
||||
const auto& program = *prepared.runtime->program;
|
||||
const auto& program = prepared.runtime->module->info;
|
||||
const auto& snapshot = *prepared.runtime->resources;
|
||||
const auto& layout = program.bindings;
|
||||
prepared.buffer_sources.resize(layout.descriptor_counts[static_cast<size_t>(BindingKind::Buffers)]);
|
||||
@@ -780,7 +785,7 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
|
||||
void RenderExecutor::RebindBuffers(PreparedBindings& prepared) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
|
||||
const auto& program = *prepared.runtime->program;
|
||||
const auto& program = prepared.runtime->module->info;
|
||||
const auto& snapshot = *prepared.runtime->resources;
|
||||
const auto& layout = program.bindings;
|
||||
EXIT_IF(prepared.buffer_sources.size() != layout.descriptor_counts[static_cast<size_t>(BindingKind::Buffers)]);
|
||||
@@ -801,6 +806,9 @@ void RenderExecutor::RebindBuffers(PreparedBindings& prepared) {
|
||||
prepared.buffers[index] = NativeStorageBuffer(m_context, prepared.buffer_sources[index],
|
||||
resource, buffer_offset);
|
||||
pack_memory_offset(index, buffer_offset);
|
||||
// Null bindings allow 0 bytes, not the shared null buffer's 16, so no store reaches it.
|
||||
prepared.shader_data[layout.MemorySizeDword() + index] =
|
||||
static_cast<uint32_t>(prepared.buffer_sources[index].size + buffer_offset);
|
||||
}
|
||||
if (layout.descriptor_counts[static_cast<size_t>(BindingKind::FlattenedSrt)] != 0) {
|
||||
prepared.flattened_srt = NativeUpload(m_context, snapshot.flattened_srt);
|
||||
@@ -813,7 +821,7 @@ void RenderExecutor::RebindBuffers(PreparedBindings& prepared) {
|
||||
void RenderExecutor::RebindImages(PreparedBindings& prepared) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
|
||||
const auto& program = *prepared.runtime->program;
|
||||
const auto& program = prepared.runtime->module->info;
|
||||
const auto& snapshot = *prepared.runtime->resources;
|
||||
auto& images = prepared.images;
|
||||
EXIT_IF(images.size() != program.info.images.size());
|
||||
@@ -859,8 +867,8 @@ void RenderExecutor::PrepareGraphicsBindings(std::span<PreparedBindings* const>
|
||||
bool uses_image_heap = false;
|
||||
FindBuffers(stages);
|
||||
for (auto* stage: stages) {
|
||||
uses_dma |= stage->runtime->program->info.uses_dma;
|
||||
uses_image_heap |= stage->runtime->program->info.uses_image_heap;
|
||||
uses_dma |= stage->runtime->module->info.info.uses_dma;
|
||||
uses_image_heap |= stage->runtime->module->info.info.uses_image_heap;
|
||||
}
|
||||
if (uses_dma) {
|
||||
m_context.PrepareBda();
|
||||
@@ -909,7 +917,7 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
|
||||
: vk::ShaderStageFlags {};
|
||||
for (const auto* prepared: prepared_bindings) {
|
||||
EXIT_IF(prepared == nullptr || prepared->runtime == nullptr || !*prepared->runtime);
|
||||
const auto& program = *prepared->runtime->program;
|
||||
const auto& program = prepared->runtime->module->info;
|
||||
write_count += std::popcount(program.bindings.descriptor_mask);
|
||||
for (auto mask = program.bindings.descriptor_mask; mask != 0; mask &= mask - 1) {
|
||||
const auto index = std::countr_zero(mask);
|
||||
@@ -930,7 +938,7 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
|
||||
const auto& reads = reader->runtime->resources->specialization_reads;
|
||||
if (reads.empty()) continue;
|
||||
for (const auto* writer: prepared_bindings) {
|
||||
if (writer->runtime->program->has_address_writes) {
|
||||
if (writer->runtime->module->info.has_address_writes) {
|
||||
EXIT("scalar resource reads cannot be proven disjoint from shader address writes\n");
|
||||
}
|
||||
}
|
||||
@@ -947,7 +955,7 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
|
||||
}
|
||||
}
|
||||
for (const auto* writer: prepared_bindings) {
|
||||
const auto& program = *writer->runtime->program;
|
||||
const auto& program = writer->runtime->module->info;
|
||||
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];
|
||||
@@ -973,7 +981,7 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
|
||||
std::array<vk::DescriptorBufferInfo, 2> address_buffers;
|
||||
|
||||
for (auto* prepared: prepared_bindings) {
|
||||
const auto& program = *prepared->runtime->program;
|
||||
const auto& program = prepared->runtime->module->info;
|
||||
auto& descriptors = *prepared;
|
||||
const auto shader_stage = NativeShaderStage(program.stage);
|
||||
const auto shader_stages = ShaderPipelineStages(shader_stage);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "kernel/memory.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <xxhash.h>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
@@ -19,7 +20,7 @@ namespace {
|
||||
|
||||
namespace IR = ShaderRecompiler::IR;
|
||||
|
||||
// Smaller tables are cheap to materialize on each draw and are often transient.
|
||||
// Smaller tables are often written for one draw, so they enter on a second draw of a frame.
|
||||
constexpr uint64_t MinTableBytes = 64ull * 1024ull;
|
||||
constexpr uint32_t ElementMask = (1u << IR::ImageHeapKindShift) - 1u;
|
||||
|
||||
@@ -44,6 +45,20 @@ void Widen(size_t& begin, size_t& end, size_t first, size_t last) {
|
||||
end = empty ? last : std::max(end, last);
|
||||
}
|
||||
|
||||
// Probes at first + k * step whose first dword lies within a table of size bytes.
|
||||
uint64_t Probes(uint64_t size, uint64_t first, uint64_t step) {
|
||||
return size >= first + 4u ? (size - first - 4u) / step + 1u : 0u;
|
||||
}
|
||||
|
||||
// Tables grow a little on most frames, so their slot words get room to double.
|
||||
std::unique_ptr<Buffer> SlotBuffer(RenderContext& context, uint64_t size) {
|
||||
return std::make_unique<Buffer>(
|
||||
context.GetGraphics(), context.GetCommandScheduler(), MemoryUsage::DeviceLocal, 0,
|
||||
vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eStorageBuffer |
|
||||
vk::BufferUsageFlagBits::eShaderDeviceAddress,
|
||||
std::bit_ceil(size + 4u));
|
||||
}
|
||||
|
||||
// Guest bytes of a table that its tracker pages [begin, end) cover.
|
||||
std::pair<uint64_t, uint64_t> PageRange(uint64_t base, uint64_t size, size_t begin, size_t end) {
|
||||
const auto origin = Common::AlignDown(base, TRACKER_PAGE_SIZE);
|
||||
@@ -118,26 +133,25 @@ ImageHeap::~ImageHeap() {
|
||||
bool ImageHeap::Acquire(const IR::ImageHeapRequest& request, IR::ImageHeapView& result) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
const auto size = request.size & ~uint64_t {3};
|
||||
if (size < MinTableBytes || request.count == 0u || request.step == 0u ||
|
||||
if (request.count == 0u || request.step == 0u ||
|
||||
((request.base | request.first | request.step) & 3u) != 0u) {
|
||||
return false;
|
||||
}
|
||||
auto memory = std::ranges::find_if(m_memories, [&](const auto& candidate) {
|
||||
return candidate->base == request.base && candidate->size == size;
|
||||
});
|
||||
if (memory == m_memories.end()) {
|
||||
auto memory = std::ranges::find_if(
|
||||
m_memories, [&](const auto& candidate) { return candidate->base == request.base; });
|
||||
if (memory == m_memories.end() && size < MinTableBytes &&
|
||||
m_sightings.try_emplace(request.base, m_draw).first->second == m_draw) {
|
||||
return false;
|
||||
}
|
||||
// A table follows its descriptor's size, as the first program of a draw acquires it.
|
||||
if (memory == m_memories.end() || ((*memory)->size != size && (*memory)->draw != m_draw)) {
|
||||
if (!m_context.IsMapped(request.base, size)) return false;
|
||||
auto created = std::make_unique<Memory>();
|
||||
created->base = request.base;
|
||||
created->size = size;
|
||||
created->words.resize(size / 4u);
|
||||
created->dead.resize(size / 4u);
|
||||
created->dirty_pages.assign((Common::AlignUp(request.base + size, TRACKER_PAGE_SIZE) -
|
||||
Common::AlignDown(request.base, TRACKER_PAGE_SIZE)) /
|
||||
TRACKER_PAGE_SIZE,
|
||||
uint8_t {1});
|
||||
std::scoped_lock lock {m_mutex};
|
||||
memory = m_memories.insert(m_memories.end(), std::move(created));
|
||||
if (memory == m_memories.end()) {
|
||||
std::scoped_lock lock {m_mutex};
|
||||
memory = m_memories.insert(m_memories.end(),
|
||||
std::make_unique<Memory>(Memory {.base = request.base}));
|
||||
}
|
||||
Resize(**memory, size);
|
||||
}
|
||||
// A draw refreshes a table and a view takes its changes once, so all programs of the draw see
|
||||
// one snapshot and one set of kinds.
|
||||
@@ -146,27 +160,28 @@ bool ImageHeap::Acquire(const IR::ImageHeapRequest& request, IR::ImageHeapView&
|
||||
(*memory)->draw = m_draw;
|
||||
}
|
||||
auto& views = (*memory)->views;
|
||||
const auto bytes = (*memory)->size;
|
||||
const auto profile = IR::ImageHeapResource(*request.root, 0u);
|
||||
const auto probes = Probes(size, request.first, request.step);
|
||||
const auto limit = request.count < probes ? request.count : UINT64_MAX;
|
||||
auto view = std::ranges::find_if(views, [&](const auto& candidate) {
|
||||
return candidate->step == request.step && candidate->first == request.first &&
|
||||
candidate->count == request.count && candidate->profile == profile;
|
||||
candidate->limit == limit && candidate->profile == profile;
|
||||
});
|
||||
if (view == views.end()) {
|
||||
const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed);
|
||||
const auto count = std::min(limit, Probes(bytes, request.first, request.step));
|
||||
auto created = std::make_unique<View>(View {.memory = **memory,
|
||||
.step = request.step,
|
||||
.first = request.first,
|
||||
.count = request.count,
|
||||
.count = count,
|
||||
.limit = limit,
|
||||
.profile = profile,
|
||||
.resolve_epoch = epoch,
|
||||
.layout_epoch = epoch});
|
||||
created->slots.resize(size / 4u + 1u);
|
||||
created->buffer = std::make_unique<Buffer>(
|
||||
m_context.GetGraphics(), m_context.GetCommandScheduler(), MemoryUsage::DeviceLocal, 0,
|
||||
vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eStorageBuffer |
|
||||
vk::BufferUsageFlagBits::eShaderDeviceAddress,
|
||||
size + 4u);
|
||||
UpdateProbes(*created, 0u, size);
|
||||
created->slots.resize(bytes / 4u + 1u);
|
||||
created->buffer = SlotBuffer(m_context, bytes);
|
||||
UpdateProbes(*created, 0u, bytes);
|
||||
// The first upload also clears the slots no probe selects.
|
||||
created->dirty_begin = 0;
|
||||
created->dirty_end = created->slots.size();
|
||||
@@ -254,10 +269,12 @@ void ImageHeap::Unwatch(const Memory& memory) {
|
||||
void ImageHeap::InvalidateMemory(uint64_t address, uint64_t size) {
|
||||
std::scoped_lock lock {m_mutex};
|
||||
for (auto& memory: m_memories) {
|
||||
const auto begin = std::max(address, memory->base);
|
||||
const auto end = std::min(address + size, memory->base + memory->size);
|
||||
if (begin >= end) continue;
|
||||
// Watches cover whole pages, so a write next to the table must unwatch its page too.
|
||||
const auto origin = Common::AlignDown(memory->base, TRACKER_PAGE_SIZE);
|
||||
const auto begin = std::max(address, origin);
|
||||
const auto end = std::min(address + size,
|
||||
origin + memory->dirty_pages.size() * TRACKER_PAGE_SIZE);
|
||||
if (begin >= end) continue;
|
||||
MarkDirty(*memory, (begin - origin) / TRACKER_PAGE_SIZE,
|
||||
(end - 1u - origin) / TRACKER_PAGE_SIZE + 1u);
|
||||
}
|
||||
@@ -381,15 +398,56 @@ uint32_t ImageHeap::Allocate() {
|
||||
}
|
||||
|
||||
void ImageHeap::Retire(View& view) {
|
||||
if (view.released.empty()) return;
|
||||
// A retired index is reused once its tick ends, so no held entry may keep it.
|
||||
std::erase_if(view.held, [&](uint32_t index) { return m_elements[index].refs == 0u; });
|
||||
const auto tick = m_context.GetCommandScheduler().CurrentTick();
|
||||
for (const auto index: view.released) m_retired.emplace_back(tick, index);
|
||||
view.released.clear();
|
||||
}
|
||||
|
||||
void ImageHeap::Resize(Memory& memory, uint64_t size) {
|
||||
{
|
||||
std::scoped_lock lock {m_mutex};
|
||||
Unwatch(memory);
|
||||
memory.size = size;
|
||||
// The next refresh rereads the whole table.
|
||||
memory.dirty_pages.assign((Common::AlignUp(memory.base + size, TRACKER_PAGE_SIZE) -
|
||||
Common::AlignDown(memory.base, TRACKER_PAGE_SIZE)) /
|
||||
TRACKER_PAGE_SIZE,
|
||||
uint8_t {1});
|
||||
}
|
||||
memory.words.resize(size / 4u);
|
||||
memory.dead.resize(size / 4u);
|
||||
auto& scheduler = m_context.GetCommandScheduler();
|
||||
for (auto& view: memory.views) {
|
||||
const auto count = std::min(view->limit, Probes(size, view->first, view->step));
|
||||
for (auto probe = count; probe < view->count; ++probe) {
|
||||
Assign(*view, (view->first + probe * view->step) / 4u, 0u);
|
||||
}
|
||||
view->count = count;
|
||||
const auto slots = view->slots.size();
|
||||
view->slots.resize(size / 4u + 1u);
|
||||
view->dirty_end = std::min(view->dirty_end, view->slots.size());
|
||||
view->dirty_begin = std::min(view->dirty_begin, view->dirty_end);
|
||||
if (view->slots.size() * sizeof(uint32_t) > view->buffer->Size()) {
|
||||
if (scheduler.Active()) {
|
||||
scheduler.DeferOperation([buffer = std::move(view->buffer)] {});
|
||||
}
|
||||
view->buffer = SlotBuffer(m_context, size);
|
||||
Widen(view->dirty_begin, view->dirty_end, 0u, view->slots.size());
|
||||
} else if (slots < view->slots.size()) {
|
||||
// Device words past the old end may hold stale slots.
|
||||
Widen(view->dirty_begin, view->dirty_end, slots, view->slots.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ImageHeap::Rebind(RenderExecutor& executor) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
auto& textures = m_context.GetTextureCache();
|
||||
const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed);
|
||||
const auto serial = Image::serials.load(std::memory_order_relaxed);
|
||||
for (auto* view: m_active) {
|
||||
bool stale = false;
|
||||
const auto mark = [&](uint32_t index) { m_elements[index].stale = stale = true; };
|
||||
@@ -418,6 +476,19 @@ void ImageHeap::Rebind(RenderExecutor& executor) {
|
||||
mark(index);
|
||||
return true;
|
||||
});
|
||||
// Held elements retry once an image is created, such as a render target of their extent.
|
||||
if (view->held_serial != serial) {
|
||||
view->held_serial = serial;
|
||||
std::erase_if(view->held, [&](uint32_t index) {
|
||||
const auto& element = m_elements[index];
|
||||
if (element.refs == 0u || element.stale) return true;
|
||||
const auto binding = executor.ResolveTexture(
|
||||
IR::ImageHeapResource(view->profile, element.kind), element.value);
|
||||
if (binding.image_id) mark(index);
|
||||
// An unmapped texture stays null.
|
||||
return binding.image_id || binding.desc.info.data.Empty();
|
||||
});
|
||||
}
|
||||
if (stale) {
|
||||
// A changed image needs a new element; pending draws still read the old one. The new
|
||||
// element keeps the old value, since a view takes table changes only when acquired.
|
||||
@@ -446,6 +517,8 @@ void ImageHeap::Rebind(RenderExecutor& executor) {
|
||||
view->dcc.push_back(index);
|
||||
}
|
||||
view->away.push_back(index);
|
||||
} else if (!binding.desc.info.data.Empty()) {
|
||||
view->held.push_back(index);
|
||||
}
|
||||
m_elements[index].binding = std::move(binding);
|
||||
m_writes.push_back(index);
|
||||
@@ -524,26 +597,42 @@ void ImageHeap::UnmapMemory(uint64_t address, uint64_t size) {
|
||||
} else {
|
||||
m_unmapped.push_back({address, size});
|
||||
}
|
||||
auto& scheduler = m_context.GetCommandScheduler();
|
||||
std::scoped_lock lock {m_mutex};
|
||||
std::erase_if(m_memories, [&](const std::unique_ptr<Memory>& memory) {
|
||||
if (address >= memory->base + memory->size || memory->base >= address + size) {
|
||||
return false;
|
||||
}
|
||||
Unwatch(*memory);
|
||||
for (auto& view: memory->views) {
|
||||
std::erase(m_active, view.get());
|
||||
for (const auto& [value, index]: view->elements) {
|
||||
Unreference(m_elements[index]);
|
||||
view->released.push_back(index);
|
||||
}
|
||||
Retire(*view);
|
||||
if (scheduler.Active()) {
|
||||
scheduler.DeferOperation([buffer = std::move(view->buffer)] {});
|
||||
}
|
||||
}
|
||||
Drop(*memory);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void ImageHeap::RunGarbageCollector() {
|
||||
// A smaller table rebuilt each frame moves to a new base. Larger ones keep their dead words.
|
||||
std::scoped_lock lock {m_mutex};
|
||||
std::erase_if(m_memories, [&](const std::unique_ptr<Memory>& memory) {
|
||||
if (memory->size >= MinTableBytes || memory->draw > m_suspend_draw) return false;
|
||||
Drop(*memory);
|
||||
return true;
|
||||
});
|
||||
m_suspend_draw = m_draw;
|
||||
m_sightings.clear();
|
||||
}
|
||||
|
||||
void ImageHeap::Drop(Memory& memory) {
|
||||
auto& scheduler = m_context.GetCommandScheduler();
|
||||
Unwatch(memory);
|
||||
for (auto& view: memory.views) {
|
||||
std::erase(m_active, view.get());
|
||||
for (const auto& [value, index]: view->elements) {
|
||||
Unreference(m_elements[index]);
|
||||
view->released.push_back(index);
|
||||
}
|
||||
Retire(*view);
|
||||
if (scheduler.Active()) {
|
||||
scheduler.DeferOperation([buffer = std::move(view->buffer)] {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
@@ -22,10 +22,11 @@ class RenderExecutor;
|
||||
struct ImageHeapTestAccess;
|
||||
|
||||
// Guest image tables that shaders index by GPU-computed offsets, kept resident in one bindless
|
||||
// sampled-image array. Each view of a table owns device slot words, one per table dword, that
|
||||
// select an array element. CPU writes to a table fault in its snapshot, which each draw refreshes
|
||||
// once; a view takes the changes when a draw first acquires it. Changes to the images behind
|
||||
// elements bump Image::heap_epoch.
|
||||
// sampled-image array. A table is keyed by its base and follows its descriptor's size. Each view
|
||||
// of a table owns device slot words, one per table dword, that select an array element. CPU
|
||||
// writes to a table fault in its snapshot, which each draw refreshes once; a view takes the
|
||||
// changes when a draw first acquires it. Changes to the images behind elements bump
|
||||
// Image::heap_epoch. A smaller table leaves at a suspend point if the frame did not acquire it.
|
||||
class ImageHeap final: public ShaderRecompiler::IR::ImageHeapSource {
|
||||
public:
|
||||
ImageHeap(RenderContext& context, PageManager& page_manager);
|
||||
@@ -46,6 +47,8 @@ public:
|
||||
// CPU write fault, on any thread.
|
||||
void InvalidateMemory(uint64_t address, uint64_t size);
|
||||
void UnmapMemory(uint64_t address, uint64_t size);
|
||||
// At a suspend point.
|
||||
void RunGarbageCollector();
|
||||
|
||||
private:
|
||||
using DescriptorValue = ShaderRecompiler::IR::DescriptorValue;
|
||||
@@ -70,6 +73,7 @@ private:
|
||||
uint64_t step = 0;
|
||||
uint64_t first = 0;
|
||||
uint64_t count = 0;
|
||||
uint64_t limit = 0; // Probes a key domain allows, if fewer.
|
||||
ShaderRecompiler::IR::ImageResource profile;
|
||||
std::vector<uint32_t> slots; // Plus a trailing null slot.
|
||||
std::vector<uint32_t> kind_keys;
|
||||
@@ -79,6 +83,7 @@ private:
|
||||
std::vector<uint32_t> released; // No slot selects them after the next upload.
|
||||
std::vector<uint32_t> away; // Not yet moved to the read layout.
|
||||
std::vector<uint32_t> dcc; // Color metadata clears apply on each discovery.
|
||||
std::vector<uint32_t> held; // Another extent holds their address.
|
||||
std::unique_ptr<Buffer> buffer;
|
||||
size_t dirty_begin = 0; // Slots to upload.
|
||||
size_t dirty_end = 0;
|
||||
@@ -86,6 +91,7 @@ private:
|
||||
size_t changed_end = 0;
|
||||
uint64_t resolve_epoch = 0;
|
||||
uint64_t layout_epoch = 0;
|
||||
uint64_t held_serial = 0; // Image::serials when held was last retried.
|
||||
};
|
||||
|
||||
struct Memory {
|
||||
@@ -103,6 +109,7 @@ private:
|
||||
template <bool track>
|
||||
void Watch(const Memory& memory, size_t begin, size_t end);
|
||||
void Unwatch(const Memory& memory);
|
||||
void Drop(Memory& memory);
|
||||
void UpdateProbes(View& view, uint64_t begin, uint64_t end);
|
||||
void UpdateProbe(View& view, uint64_t probe);
|
||||
void Assign(View& view, size_t slot, uint32_t entry);
|
||||
@@ -113,6 +120,7 @@ private:
|
||||
Image* ImageOf(const Element& element) const;
|
||||
uint32_t Allocate();
|
||||
void Retire(View& view);
|
||||
void Resize(Memory& memory, uint64_t size);
|
||||
void WriteDescriptors();
|
||||
|
||||
RenderContext& m_context;
|
||||
@@ -120,9 +128,11 @@ private:
|
||||
std::mutex m_mutex; // Fault-visible Memory state and m_memories.
|
||||
std::vector<std::unique_ptr<Memory>> m_memories;
|
||||
std::vector<View*> m_active; // Acquired since the last commit.
|
||||
uint64_t m_draw = 1;
|
||||
uint64_t m_draw = 1;
|
||||
uint64_t m_suspend_draw = 0; // m_draw at the last suspend point.
|
||||
std::vector<Element> m_elements;
|
||||
std::vector<uint32_t> m_free;
|
||||
std::unordered_map<uint64_t, uint64_t> m_sightings; // Smaller tables: base, first draw.
|
||||
std::deque<std::pair<uint64_t, uint32_t>> m_retired; // Tick, element.
|
||||
std::vector<std::pair<size_t, size_t>> m_runs;
|
||||
std::vector<uint32_t> m_scratch;
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
#include "graphics/host_gpu/renderer/pipeline/blendMapping.h"
|
||||
#include "graphics/host_gpu/renderer/render.h"
|
||||
#include "graphics/host_gpu/renderer/renderContext.h"
|
||||
#include "graphics/shader/recompiler/BufferFormat.h"
|
||||
#include "graphics/shader/recompiler/ShaderRecompiler.h"
|
||||
#include "graphics/shader/recompiler/Tessellation.h"
|
||||
#include "graphics/shader/recompiler/ir/Serialize.h"
|
||||
#include "graphics/shader/shaderCompiler.h"
|
||||
#include "kernel/memory.h"
|
||||
#include "loader/systemContent.h"
|
||||
@@ -29,9 +32,9 @@
|
||||
#include <cstring>
|
||||
#include <fmt/format.h>
|
||||
#include <limits>
|
||||
#include <ranges>
|
||||
#include <span>
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <xxhash.h>
|
||||
@@ -114,14 +117,11 @@ bool ReadShaderGuestMemory(void*, uint64_t address, std::span<uint32_t> values)
|
||||
Libs::LibKernel::Memory::TryReadBufferBacking(address, values.data(), values.size_bytes());
|
||||
}
|
||||
|
||||
void DumpShaderSpirv(const char* stage_name, uint64_t shader_hash,
|
||||
const std::vector<uint32_t>& spirv) {
|
||||
void DumpShader(std::span<const uint32_t> words, const std::string& name) {
|
||||
if (!Config::GraphicsDebugDumpEnabled()) {
|
||||
return;
|
||||
}
|
||||
static std::atomic_int id = 0;
|
||||
const auto path = Config::GetShaderLogFolder() / fmt::format("{:04d}_new_shader_{}_{:016x}.spv",
|
||||
id++, stage_name, shader_hash);
|
||||
const auto path = Config::GetShaderLogFolder() / name;
|
||||
Common::File::CreateDirectories(path.parent_path());
|
||||
Common::File file(path);
|
||||
if (file.IsInvalid()) {
|
||||
@@ -129,32 +129,13 @@ void DumpShaderSpirv(const char* stage_name, uint64_t shader_hash,
|
||||
LOGF_COLOR(Log::Color::BrightRed, "Can't create file: %s\n", path_text.c_str());
|
||||
return;
|
||||
}
|
||||
file.Write(spirv.data(), spirv.size() * sizeof(uint32_t));
|
||||
file.Write(words.data(), words.size_bytes());
|
||||
}
|
||||
|
||||
void DumpShaderOriginal(const char* stage_name, uint64_t shader_hash,
|
||||
std::span<const uint32_t> code) {
|
||||
if (!Config::GraphicsDebugDumpEnabled()) {
|
||||
return;
|
||||
}
|
||||
EXIT_IF(code.empty());
|
||||
static std::atomic_int id = 0;
|
||||
const auto path = Config::GetShaderLogFolder() / "original" /
|
||||
fmt::format("{:04d}_new_shader_{}_{:016x}.bin", id++, stage_name, shader_hash);
|
||||
Common::File::CreateDirectories(path.parent_path());
|
||||
Common::File file(path);
|
||||
if (file.IsInvalid()) {
|
||||
const auto path_text = Common::PathToString(path);
|
||||
LOGF_COLOR(Log::Color::BrightRed, "Can't create file: %s\n", path_text.c_str());
|
||||
return;
|
||||
}
|
||||
file.Write(code.data(), code.size_bytes());
|
||||
}
|
||||
|
||||
bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
|
||||
void ValidateShaderSpirv(const char* stage, uint64_t shader_hash,
|
||||
const std::vector<uint32_t>& spirv, bool maintenance8_enabled) {
|
||||
if (!Config::ShaderValidationEnabled()) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_3);
|
||||
std::string messages;
|
||||
@@ -167,7 +148,7 @@ bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
|
||||
spvtools::ValidatorOptions validator_options;
|
||||
validator_options.SetAllowOffsetTextureOperand(maintenance8_enabled);
|
||||
if (tools.Validate(spirv.data(), spirv.size(), validator_options)) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
spvtools::SpirvTools disassembler(SPV_ENV_VULKAN_1_2);
|
||||
std::string text;
|
||||
@@ -177,332 +158,320 @@ bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
|
||||
static_cast<uint32_t>(SPV_BINARY_TO_TEXT_OPTION_COMMENT) |
|
||||
static_cast<uint32_t>(SPV_BINARY_TO_TEXT_OPTION_INDENT) |
|
||||
static_cast<uint32_t>(SPV_BINARY_TO_TEXT_OPTION_COLOR));
|
||||
LOGF_COLOR(Log::Color::BrightRed, "%s SPIR-V validation failed hash=0x%016" PRIx64 ":\n%s",
|
||||
label, shader_hash, messages.c_str());
|
||||
LOGF("%s\n", text.c_str());
|
||||
return false;
|
||||
EXIT("ShaderRecompiler %s SPIR-V validation failed hash=0x%016" PRIx64 ":\n%s", stage,
|
||||
shader_hash, messages.c_str());
|
||||
}
|
||||
|
||||
// Formats with a narrower host variant drop the components the shader does not read.
|
||||
vk::Format NarrowInputFormat(vk::Format format, uint32_t used_components) {
|
||||
switch (format) {
|
||||
case vk::Format::eR32G32B32A32Sfloat:
|
||||
switch (used_components) {
|
||||
case 1: return vk::Format::eR32Sfloat;
|
||||
case 2: return vk::Format::eR32G32Sfloat;
|
||||
case 3: return vk::Format::eR32G32B32Sfloat;
|
||||
default: return format;
|
||||
}
|
||||
case vk::Format::eR32G32B32Sfloat:
|
||||
switch (used_components) {
|
||||
case 1: return vk::Format::eR32Sfloat;
|
||||
case 2: return vk::Format::eR32G32Sfloat;
|
||||
default: return format;
|
||||
}
|
||||
case vk::Format::eR16G16B16A16Sfloat:
|
||||
switch (used_components) {
|
||||
case 1: return vk::Format::eR16Sfloat;
|
||||
case 2: return vk::Format::eR16G16Sfloat;
|
||||
default: return format;
|
||||
}
|
||||
case vk::Format::eR8G8B8A8Unorm:
|
||||
switch (used_components) {
|
||||
case 1: return vk::Format::eR8Unorm;
|
||||
case 2: return vk::Format::eR8G8Unorm;
|
||||
default: return format;
|
||||
}
|
||||
case vk::Format::eR8G8B8A8Snorm:
|
||||
return used_components == 2 ? vk::Format::eR8G8Snorm : format;
|
||||
case vk::Format::eR8G8B8A8Uint:
|
||||
switch (used_components) {
|
||||
case 1: return vk::Format::eR8Uint;
|
||||
case 2: return vk::Format::eR8G8Uint;
|
||||
default: return format;
|
||||
}
|
||||
default: return format;
|
||||
}
|
||||
}
|
||||
|
||||
vk::Format InputFormat(const ShaderBufferResource& resource, uint32_t used_components) {
|
||||
switch (resource.RawFormat()) {
|
||||
// Temporary: invalid PS5 buffer format 113 (VertexAttribFormat::k16_16SInt).
|
||||
case 113: return NarrowInputFormat(vk::Format::eR32G32B32A32Sfloat, used_components);
|
||||
case 121: return vk::Format::eR16G16Sfloat;
|
||||
default: break;
|
||||
}
|
||||
const auto format = VulkanFormat(resource.Format());
|
||||
if (format == vk::Format::eUndefined ||
|
||||
ShaderRecompiler::Format::GetFormatInfo(resource.Format()).component_count == 0) {
|
||||
EXIT("unknown vertex format: fmt = %u\n", resource.RawFormat());
|
||||
}
|
||||
return NarrowInputFormat(format, used_components);
|
||||
}
|
||||
|
||||
// The host format of each vertex input attribute, narrowed to the components the stage reads.
|
||||
std::vector<vk::Format> VertexInputFormats(const ShaderVertexInputInfo& input_info,
|
||||
const ShaderRecompiler::IR::CompiledShaderInfo& program) {
|
||||
std::vector<vk::Format> formats(input_info.resources_num);
|
||||
for (int index = 0; index < input_info.resources_num; index++) {
|
||||
const auto registers_num = input_info.resources_dst[index].registers_num;
|
||||
EXIT_IF(registers_num < 1 || registers_num > 4);
|
||||
const auto fetched = program.info.vertex_fetch_components[index];
|
||||
formats[index] = InputFormat(input_info.resources[index],
|
||||
fetched > 0 ? fetched : static_cast<uint32_t>(registers_num));
|
||||
}
|
||||
return formats;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::size_t PipelineCache::GraphicsPipelineKeyHash::operator()(const GraphicsPipelineKey& key) const {
|
||||
std::size_t hash = 0;
|
||||
PipelineKeyHash::Mix(hash, key.rendering.color_count);
|
||||
for (uint32_t i = 0; i < key.rendering.color_count; i++) {
|
||||
PipelineKeyHash::Mix(hash, static_cast<uint32_t>(key.rendering.color_formats[i]));
|
||||
}
|
||||
PipelineKeyHash::Mix(hash, static_cast<uint32_t>(key.rendering.depth_format));
|
||||
PipelineKeyHash::Mix(hash, static_cast<uint32_t>(key.rendering.stencil_format));
|
||||
for (const auto id: key.vertex_shader_ids) {
|
||||
PipelineKeyHash::Mix(hash, id);
|
||||
}
|
||||
PipelineKeyHash::Mix(hash, key.ps_shader_id);
|
||||
PipelineKeyHash::Mix(hash, key.vertex_input.binding_count);
|
||||
for (uint32_t i = 0; i < key.vertex_input.binding_count; i++) {
|
||||
PipelineKeyHash::Mix(hash, key.vertex_input.bindings[i].stride);
|
||||
PipelineKeyHash::Mix(hash, key.vertex_input.bindings[i].instance);
|
||||
}
|
||||
PipelineKeyHash::Mix(hash, key.vertex_input.attribute_count);
|
||||
for (uint32_t i = 0; i < key.vertex_input.attribute_count; i++) {
|
||||
PipelineKeyHash::Mix(hash, key.vertex_input.attributes[i].offset);
|
||||
PipelineKeyHash::Mix(hash, key.vertex_input.attributes[i].binding);
|
||||
}
|
||||
PipelineKeyHash::Mix(hash, XXH3_64bits(&key.static_params, sizeof(key.static_params)));
|
||||
return hash;
|
||||
ShaderRecompiler::Spirv::Profile SpirvProfile(const GraphicContext& graphics) {
|
||||
// AMD GPUs (vendor 0x1002) run NMin/NMax as v_min/v_max, which give the guest's NaN and
|
||||
// signed-zero results; GLSL.std.450 alone does not order -0 below +0.
|
||||
return {.maintenance8 = graphics.maintenance8_enabled,
|
||||
.native_float_min_max = graphics.physical_device_properties.vendorID == 0x1002u,
|
||||
.device_clock_period_ps = graphics.device_clock_period_ps};
|
||||
}
|
||||
|
||||
struct PipelineCache::ProgramCache {
|
||||
struct ProgramKey {
|
||||
ShaderType stage = ShaderType::Unknown;
|
||||
uint64_t hash = 0;
|
||||
uint32_t user_data_count = 0;
|
||||
uint32_t code_size = 0;
|
||||
std::vector<uint32_t> static_state;
|
||||
std::filesystem::path ShaderStorePath() {
|
||||
if (KYTY_BUILD != KYTY_BUILD_RELEASE || !Config::ShaderCacheEnabled() ||
|
||||
Config::GraphicsDebugDumpEnabled()) {
|
||||
return {};
|
||||
}
|
||||
const auto title_id = PipelineCacheTitleId();
|
||||
if (title_id.empty()) {
|
||||
return {};
|
||||
}
|
||||
return std::filesystem::path("_PipelineCache") / (title_id + ".shaders");
|
||||
}
|
||||
|
||||
bool operator==(const ProgramKey&) const = default;
|
||||
bool GraphicsPipelineKey::operator==(const GraphicsPipelineKey& other) const {
|
||||
return std::memcmp(this, &other, sizeof(*this)) == 0;
|
||||
}
|
||||
|
||||
std::size_t PipelineCache::GraphicsPipelineKeyHash::operator()(const GraphicsPipelineKey& key) const {
|
||||
return XXH3_64bits(&key, sizeof(key));
|
||||
}
|
||||
|
||||
std::span<const uint32_t> PipelineCache::Program::FunctionCode() const {
|
||||
return call_source
|
||||
? std::span<const uint32_t>(call_source->linked->code).subspan(call_source->code.size())
|
||||
: std::span<const uint32_t>{};
|
||||
}
|
||||
|
||||
bool PipelineCache::Program::RefreshCallSource(const ShaderRecompiler::IR::SrtRuntime& runtime) {
|
||||
const auto revision = call_source->revision;
|
||||
ShaderRecompiler::RefreshShaderSource(*call_source, runtime);
|
||||
resource_plan.source_reads = call_source->reads;
|
||||
return revision != call_source->revision;
|
||||
}
|
||||
|
||||
template <bool HasCalls>
|
||||
ShaderModule* PipelineCache::Program::FindModule(const ShaderRecompiler::IR::SrtRuntime& runtime,
|
||||
uint32_t push_data_cursor) {
|
||||
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(resource_plan, runtime, resources,
|
||||
specialization));
|
||||
for (auto& candidate: modules) {
|
||||
const auto& layout = candidate.info.bindings;
|
||||
if (layout.push_data_start_dword != ShaderRecompiler::IR::PushData::StartFor(
|
||||
push_data_cursor, layout.ShaderDataDwords()) ||
|
||||
candidate.specialization != specialization) {
|
||||
continue;
|
||||
}
|
||||
if constexpr (HasCalls) {
|
||||
if (!std::ranges::equal(candidate.function_code, FunctionCode())) continue;
|
||||
}
|
||||
return &candidate;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ShaderModule& PipelineCache::CompileModule(Program& program, const ProgramKey& key,
|
||||
ShaderRecompiler::CompileOptions options,
|
||||
ShaderRecompiler::TranslateResult translated,
|
||||
uint32_t push_data_cursor) {
|
||||
Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::ShadersCompiled,
|
||||
Common::DebugCounters::Counter::ShaderCompileNs,
|
||||
Common::DebugCounters::Peak::ShaderCompileNs);
|
||||
const auto profile = SpirvProfile(m_graphics);
|
||||
options.profile = &profile;
|
||||
auto result = ShaderRecompiler::CompileProgram(std::move(translated), options,
|
||||
program.specialization, push_data_cursor);
|
||||
auto info = std::move(result.program).TakeCompiledInfo();
|
||||
|
||||
// The same inputs give the same module id across runs.
|
||||
const auto function_code = program.FunctionCode();
|
||||
ShaderRecompiler::IR::Writer inputs;
|
||||
ShaderRecompiler::IR::IoAll(inputs, key.stage, key.hash, key.user_data_count, key.static_state,
|
||||
info.bindings.push_data_start_dword, program.specialization);
|
||||
inputs.Raw(function_code.data(), function_code.size_bytes()); // Last, so it needs no size.
|
||||
const auto id = XXH3_64bits(inputs.bytes.data(), inputs.bytes.size());
|
||||
|
||||
const auto* stage = ShaderRecompiler::StageName(key.stage);
|
||||
DumpShader(result.spirv, fmt::format("{}_{:016x}_{:016x}.spv", stage, key.hash, id));
|
||||
ValidateShaderSpirv(stage, key.hash, result.spirv, profile.maintenance8);
|
||||
auto& module = program.modules.emplace_back(ShaderModule {
|
||||
.id = id,
|
||||
.module = CompileSPV(result.spirv, m_graphics.device),
|
||||
.info = std::move(info),
|
||||
.specialization = program.specialization,
|
||||
.function_code = {function_code.begin(), function_code.end()},
|
||||
});
|
||||
EXIT_IF(module.module == nullptr);
|
||||
m_compiled[static_cast<size_t>(key.stage)]++;
|
||||
// Guest geometry shaders are compiled through the host mesh stage.
|
||||
std::printf("Shaders: VS %u | PS %u | CS %u | GS %u | LS %u | HS %u | TES %u\n",
|
||||
m_compiled[static_cast<size_t>(ShaderType::Vertex)],
|
||||
m_compiled[static_cast<size_t>(ShaderType::Pixel)],
|
||||
m_compiled[static_cast<size_t>(ShaderType::Compute)],
|
||||
m_compiled[static_cast<size_t>(ShaderType::Mesh)],
|
||||
m_compiled[static_cast<size_t>(ShaderType::Local)],
|
||||
m_compiled[static_cast<size_t>(ShaderType::TessellationControl)],
|
||||
m_compiled[static_cast<size_t>(ShaderType::TessellationEvaluation)]);
|
||||
// The inputs of these come from the stage's static key.
|
||||
if (key.stage == ShaderType::Vertex || key.stage == ShaderType::Local) {
|
||||
module.vertex_formats = VertexInputFormats(*options.input_info.vertex, module.info);
|
||||
} else if (key.stage == ShaderType::Pixel &&
|
||||
options.input_info.pixel->parameter_mode == ShaderPixelParameterMode::Rectangle) {
|
||||
module.rect_parameters = RectListParameters(*options.input_info.pixel, module.info);
|
||||
}
|
||||
// Programs with GPU-selected calls depend on their callees' current code.
|
||||
if (program.call_source == nullptr) {
|
||||
StoreModule(key, program, module, result.spirv);
|
||||
}
|
||||
return module;
|
||||
}
|
||||
|
||||
template <typename InputInfo>
|
||||
const ShaderModule* PipelineCache::GetProgram(const ShaderParams& params, InputInfo& input_info,
|
||||
uint32_t& push_data_cursor) {
|
||||
const auto user_data = std::span(params.user_data).first(params.user_data_count);
|
||||
ShaderRecompiler::IR::SrtRuntime runtime {
|
||||
.user_data = user_data,
|
||||
.shader_base = params.Base(),
|
||||
.read_specialization_memory = ReadShaderGuestMemory,
|
||||
.image_heap = m_image_heap,
|
||||
};
|
||||
|
||||
struct Permutation {
|
||||
std::vector<uint32_t> function_code;
|
||||
ShaderRecompiler::IR::ResourceSpecialization specialization;
|
||||
ShaderRecompiler::IR::CompiledShaderInfo program;
|
||||
ShaderProgram handle;
|
||||
ShaderRecompiler::CompileOptions options {.wave_size = input_info.wave_size};
|
||||
if constexpr (std::is_same_v<InputInfo, ShaderVertexInputInfo>) {
|
||||
options.stage = input_info.logical_stage;
|
||||
options.input_info.vertex = &input_info;
|
||||
options.user_data_base = 8;
|
||||
if (options.stage == ShaderType::Mesh ||
|
||||
options.stage == ShaderType::TessellationControl) {
|
||||
options.user_data_base = 0;
|
||||
options.wave_size =
|
||||
options.stage == ShaderType::Mesh ? input_info.mesh.wave_size : 64u;
|
||||
}
|
||||
} else if constexpr (std::is_same_v<InputInfo, ShaderPixelInputInfo>) {
|
||||
options.stage = ShaderType::Pixel;
|
||||
options.input_info.pixel = &input_info;
|
||||
} else {
|
||||
static_assert(std::is_same_v<InputInfo, ShaderComputeInputInfo>);
|
||||
options.stage = ShaderType::Compute;
|
||||
options.input_info.compute = &input_info;
|
||||
runtime.workgroup_counts = input_info.workgroup_counts;
|
||||
runtime.indirect_args_address = input_info.indirect_args_address;
|
||||
}
|
||||
m_lookup_key.stage = options.stage;
|
||||
m_lookup_key.hash = params.hash;
|
||||
m_lookup_key.user_data_count = params.user_data_count;
|
||||
BuildStageStaticKey(input_info, m_lookup_key.static_state);
|
||||
auto entry = m_program_cache.find(m_lookup_key);
|
||||
const auto bind = [&](const ShaderModule& module) {
|
||||
input_info.stage = {.module = &module, .resources = &entry->second.resources};
|
||||
module.info.bindings.AdvancePushData(push_data_cursor);
|
||||
return &module;
|
||||
};
|
||||
|
||||
struct SourceEntry {
|
||||
SourceEntry(ShaderRecompiler::IR::ResourcePlan plan,
|
||||
std::unique_ptr<ShaderRecompiler::ShaderSource> call_source)
|
||||
: call_source(std::move(call_source)), resource_plan(std::move(plan)) {
|
||||
permutations.reserve(8);
|
||||
bool source_changed = false;
|
||||
if (entry != m_program_cache.end()) {
|
||||
auto& cached = entry->second;
|
||||
if (cached.skip_dispatch) return nullptr;
|
||||
ShaderModule* module = nullptr;
|
||||
if (cached.call_source) {
|
||||
source_changed = cached.RefreshCallSource(runtime);
|
||||
if (!source_changed) module = cached.FindModule<true>(runtime, push_data_cursor);
|
||||
} else {
|
||||
module = cached.FindModule<false>(runtime, push_data_cursor);
|
||||
}
|
||||
|
||||
std::span<const uint32_t> FunctionCode() const {
|
||||
return call_source
|
||||
? std::span<const uint32_t>(call_source->linked->code).subspan(call_source->code.size())
|
||||
: std::span<const uint32_t>{};
|
||||
}
|
||||
|
||||
bool RefreshCallSource(const ShaderRecompiler::IR::SrtRuntime& runtime) {
|
||||
const auto revision = call_source->revision;
|
||||
ShaderRecompiler::RefreshShaderSource(*call_source, runtime);
|
||||
resource_plan.source_reads = call_source->reads;
|
||||
return revision != call_source->revision;
|
||||
}
|
||||
|
||||
template <bool HasCalls>
|
||||
Permutation* FindPermutation(const ShaderRecompiler::IR::SrtRuntime& runtime,
|
||||
uint32_t push_data_cursor) {
|
||||
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
|
||||
resource_plan, runtime, resources, specialization));
|
||||
for (auto& candidate: permutations) {
|
||||
const auto& layout = candidate.program.bindings;
|
||||
if (layout.push_data_start_dword != ShaderRecompiler::IR::PushData::StartFor(
|
||||
push_data_cursor, layout.ShaderDataDwords()) ||
|
||||
candidate.specialization != specialization) {
|
||||
continue;
|
||||
}
|
||||
if constexpr (HasCalls) {
|
||||
if (!std::ranges::equal(candidate.function_code, FunctionCode())) continue;
|
||||
}
|
||||
return &candidate;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<ShaderRecompiler::ShaderSource> call_source;
|
||||
ShaderRecompiler::IR::ResourcePlan resource_plan;
|
||||
ShaderRecompiler::IR::ResourceSnapshot resources;
|
||||
ShaderRecompiler::IR::ResourceSpecialization specialization;
|
||||
std::vector<Permutation> permutations;
|
||||
bool skip_dispatch = false;
|
||||
};
|
||||
|
||||
struct ProgramKeyHash {
|
||||
std::size_t operator()(const ProgramKey& key) const {
|
||||
std::size_t hash = static_cast<std::size_t>(key.stage);
|
||||
PipelineKeyHash::Mix(hash, static_cast<std::size_t>(key.hash));
|
||||
if constexpr (sizeof(std::size_t) < sizeof(uint64_t)) {
|
||||
PipelineKeyHash::Mix(hash, static_cast<std::size_t>(key.hash >> 32u));
|
||||
}
|
||||
PipelineKeyHash::Mix(hash, key.user_data_count);
|
||||
PipelineKeyHash::Mix(hash, key.code_size);
|
||||
PipelineKeyHash::Mix(hash, key.static_state.size());
|
||||
// Bucket same-shape static variants by source. ProgramKey equality performs the one
|
||||
// exact state comparison needed on a stable hit without hashing the full state first.
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr std::size_t MaxStaticKeyWords = 32 + ShaderVertexInputInfo::RES_MAX * 6;
|
||||
|
||||
Permutation CompilePermutation(const char* stage_name,
|
||||
const ShaderRecompiler::CompileOptions& options,
|
||||
ShaderRecompiler::TranslateResult translated,
|
||||
ShaderRecompiler::IR::ResourceSpecialization specialization,
|
||||
uint32_t push_data_start_dword,
|
||||
std::span<const uint32_t> function_code) {
|
||||
Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::ShadersCompiled,
|
||||
Common::DebugCounters::Counter::ShaderCompileNs,
|
||||
Common::DebugCounters::Peak::ShaderCompileNs);
|
||||
auto result = ShaderRecompiler::CompileProgram(std::move(translated), options,
|
||||
specialization, push_data_start_dword);
|
||||
if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv,
|
||||
options.profile->maintenance8)) {
|
||||
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
|
||||
EXIT("%s failed hash=0x%016" PRIx64 ": SPIR-V validation failed\n", options.dump_label,
|
||||
options.shader_hash);
|
||||
}
|
||||
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
|
||||
|
||||
const auto module = CompileSPV(result.spirv, graphics.device);
|
||||
EXIT_IF(module == nullptr);
|
||||
if (options.dump_ir) {
|
||||
LOGF("%s SPIR-V words=%" PRIu64 " wave_size=%u\n", options.dump_label,
|
||||
static_cast<uint64_t>(result.spirv.size()), options.wave_size);
|
||||
}
|
||||
return {
|
||||
.function_code = {function_code.begin(), function_code.end()},
|
||||
.specialization = std::move(specialization),
|
||||
.program = std::move(result.program).TakeCompiledInfo(),
|
||||
.handle = {.id = ++next_shader_id, .module = module},
|
||||
};
|
||||
if (module != nullptr) return bind(*module);
|
||||
}
|
||||
|
||||
template <typename InputInfo>
|
||||
ShaderProgram Get(const ShaderParams& params, InputInfo& input_info,
|
||||
uint32_t& push_data_cursor) {
|
||||
const auto user_data = std::span(params.user_data).first(params.user_data_count);
|
||||
ShaderRecompiler::IR::SrtRuntime runtime {
|
||||
.user_data = user_data,
|
||||
.shader_base = params.Base(),
|
||||
.read_specialization_memory = ReadShaderGuestMemory,
|
||||
.image_heap = image_heap,
|
||||
};
|
||||
ShaderRecompiler::CompileOptions options {.wave_size = input_info.wave_size};
|
||||
if constexpr (std::is_same_v<InputInfo, ShaderVertexInputInfo>) {
|
||||
options.stage = input_info.logical_stage;
|
||||
options.input_info.vertex = &input_info;
|
||||
options.user_data_base = 8;
|
||||
if (options.stage == ShaderType::Mesh ||
|
||||
options.stage == ShaderType::TessellationControl) {
|
||||
options.user_data_base = 0;
|
||||
options.wave_size =
|
||||
options.stage == ShaderType::Mesh ? input_info.mesh.wave_size : 64u;
|
||||
}
|
||||
} else if constexpr (std::is_same_v<InputInfo, ShaderPixelInputInfo>) {
|
||||
options.stage = ShaderType::Pixel;
|
||||
options.input_info.pixel = &input_info;
|
||||
} else {
|
||||
static_assert(std::is_same_v<InputInfo, ShaderComputeInputInfo>);
|
||||
options.stage = ShaderType::Compute;
|
||||
options.input_info.compute = &input_info;
|
||||
runtime.workgroup_counts = input_info.workgroup_counts;
|
||||
runtime.indirect_args_address = input_info.indirect_args_address;
|
||||
}
|
||||
lookup_key.stage = options.stage;
|
||||
lookup_key.hash = params.hash;
|
||||
lookup_key.user_data_count = params.user_data_count;
|
||||
lookup_key.code_size = static_cast<uint32_t>(params.code.size());
|
||||
BuildStageStaticKey(input_info, lookup_key.static_state);
|
||||
auto entry = programs.find(lookup_key);
|
||||
const auto bind = [&](const Permutation& permutation) {
|
||||
input_info.stage = {.program = &permutation.program,
|
||||
.resources = &entry->second.resources,
|
||||
.handle = &permutation.handle};
|
||||
permutation.program.bindings.AdvancePushData(push_data_cursor);
|
||||
return permutation.handle;
|
||||
};
|
||||
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);
|
||||
if (!source_changed) permutation = cached.FindPermutation<true>(runtime, push_data_cursor);
|
||||
} else {
|
||||
permutation = cached.FindPermutation<false>(runtime, push_data_cursor);
|
||||
}
|
||||
if (permutation != nullptr) return bind(*permutation);
|
||||
}
|
||||
|
||||
const char* label = nullptr;
|
||||
const char* stage_name = nullptr;
|
||||
switch (options.stage) {
|
||||
case ShaderType::Vertex: label = "ShaderRecompiler VS"; stage_name = "vs"; break;
|
||||
case ShaderType::Mesh: label = "ShaderRecompiler MS"; stage_name = "ms"; break;
|
||||
case ShaderType::Local: label = "ShaderRecompiler LS"; stage_name = "ls"; break;
|
||||
case ShaderType::TessellationControl: label = "ShaderRecompiler HS"; stage_name = "hs"; break;
|
||||
case ShaderType::TessellationEvaluation: label = "ShaderRecompiler DS"; stage_name = "ds"; break;
|
||||
case ShaderType::Pixel: label = "ShaderRecompiler PS"; stage_name = "ps"; break;
|
||||
case ShaderType::Compute: label = "ShaderRecompiler CS"; stage_name = "cs"; break;
|
||||
default: EXIT("invalid pipeline shader stage\n");
|
||||
}
|
||||
options.profile = &profile;
|
||||
options.shader_hash = params.hash;
|
||||
options.user_data = user_data;
|
||||
options.back_code = params.back_code;
|
||||
options.dump_ir = Config::GetShaderLogDirection() != Config::LogDirection::Silent;
|
||||
options.early_dump = options.dump_ir;
|
||||
options.emit_debug_info = Config::GraphicsDebugDumpEnabled();
|
||||
options.dump_label = label;
|
||||
DumpShaderOriginal(stage_name, options.shader_hash, params.code);
|
||||
ShaderRecompiler::ShaderSource source;
|
||||
if (entry == programs.end()) source = ShaderRecompiler::PrepareShaderSource(params.code, options);
|
||||
options.shader_hash = params.hash;
|
||||
options.user_data = user_data;
|
||||
options.back_code = params.back_code;
|
||||
options.dump_ir = Config::GetShaderLogDirection() != Config::LogDirection::Silent;
|
||||
options.emit_debug_info = Config::GraphicsDebugDumpEnabled();
|
||||
ShaderRecompiler::ShaderSource source;
|
||||
if (entry == m_program_cache.end()) {
|
||||
DumpShader(params.code, fmt::format("original/{}_{:016x}.bin",
|
||||
ShaderRecompiler::StageName(options.stage), params.hash));
|
||||
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 {};
|
||||
m_program_cache.emplace(m_lookup_key, Program {.skip_dispatch = true});
|
||||
return nullptr;
|
||||
}
|
||||
auto* current = entry == programs.end() ? &source : entry->second.call_source.get();
|
||||
ShaderRecompiler::TranslateResult translated;
|
||||
if (current != nullptr) {
|
||||
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;
|
||||
} else {
|
||||
translated = ShaderRecompiler::TranslateProgram(current->decoded, options);
|
||||
}
|
||||
}
|
||||
auto* current = entry == m_program_cache.end() ? &source : entry->second.call_source.get();
|
||||
ShaderRecompiler::TranslateResult translated;
|
||||
if (current != nullptr) {
|
||||
if (!current->calls.empty()) {
|
||||
if (entry == m_program_cache.end()) ShaderRecompiler::RefreshShaderSource(*current, runtime);
|
||||
translated = ShaderRecompiler::TranslateProgram(current->linked->decoded, options);
|
||||
translated.program.source_reads = current->reads;
|
||||
} else {
|
||||
translated = ShaderRecompiler::TranslateProgram(params.code, options);
|
||||
translated = ShaderRecompiler::TranslateProgram(current->decoded, options);
|
||||
}
|
||||
if (entry == programs.end()) {
|
||||
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(
|
||||
entry->second.resource_plan, runtime, entry->second.resources,
|
||||
entry->second.specialization));
|
||||
} else if (source_changed) {
|
||||
entry->second.resource_plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program);
|
||||
if (const auto* permutation = entry->second.FindPermutation<true>(runtime, push_data_cursor)) {
|
||||
return bind(*permutation);
|
||||
}
|
||||
}
|
||||
entry->second.permutations.push_back(CompilePermutation(
|
||||
stage_name, options, std::move(translated), entry->second.specialization, push_data_cursor,
|
||||
entry->second.FunctionCode()));
|
||||
|
||||
std::array<size_t, static_cast<size_t>(ShaderType::TessellationEvaluation) + 1> counts {};
|
||||
for (const auto& [key, source]: programs) {
|
||||
counts[static_cast<size_t>(key.stage)] += source.permutations.size();
|
||||
}
|
||||
// Guest geometry shaders are compiled through the host mesh stage.
|
||||
std::printf("Shaders: VS %zu | PS %zu | CS %zu | GS %zu | LS %zu | HS %zu | TES %zu\n",
|
||||
counts[static_cast<size_t>(ShaderType::Vertex)],
|
||||
counts[static_cast<size_t>(ShaderType::Pixel)],
|
||||
counts[static_cast<size_t>(ShaderType::Compute)],
|
||||
counts[static_cast<size_t>(ShaderType::Mesh)],
|
||||
counts[static_cast<size_t>(ShaderType::Local)],
|
||||
counts[static_cast<size_t>(ShaderType::TessellationControl)],
|
||||
counts[static_cast<size_t>(ShaderType::TessellationEvaluation)]);
|
||||
return bind(entry->second.permutations.back());
|
||||
} else {
|
||||
translated = ShaderRecompiler::TranslateProgram(params.code, options);
|
||||
}
|
||||
|
||||
ProgramCache(GraphicContext& graphics, ShaderRecompiler::IR::ImageHeapSource* image_heap)
|
||||
: graphics(graphics), image_heap(image_heap),
|
||||
// AMD GPUs (vendor 0x1002) run NMin/NMax as v_min/v_max, which give the guest's NaN and
|
||||
// signed-zero results; GLSL.std.450 alone does not order -0 below +0.
|
||||
profile {.maintenance8 = graphics.maintenance8_enabled,
|
||||
.native_float_min_max =
|
||||
graphics.physical_device_properties.vendorID == 0x1002u,
|
||||
.device_clock_period_ps = graphics.device_clock_period_ps} {
|
||||
lookup_key.static_state.reserve(MaxStaticKeyWords);
|
||||
}
|
||||
~ProgramCache() {
|
||||
for (const auto& [key, entry]: programs) {
|
||||
(void)key;
|
||||
for (const auto& permutation: entry.permutations) {
|
||||
graphics.device.destroyShaderModule(permutation.handle.module, nullptr);
|
||||
}
|
||||
if (entry == m_program_cache.end()) {
|
||||
auto retained = !source.calls.empty()
|
||||
? std::make_unique<ShaderRecompiler::ShaderSource>(std::move(source))
|
||||
: nullptr;
|
||||
entry = m_program_cache.emplace(m_lookup_key, Program {
|
||||
.call_source = std::move(retained),
|
||||
.resource_plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program),
|
||||
}).first;
|
||||
auto& program = entry->second;
|
||||
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
|
||||
program.resource_plan, runtime, program.resources, program.specialization));
|
||||
} else if (source_changed) {
|
||||
entry->second.resource_plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program);
|
||||
if (const auto* module = entry->second.FindModule<true>(runtime, push_data_cursor)) {
|
||||
return bind(*module);
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_map<ProgramKey, SourceEntry, ProgramKeyHash> programs;
|
||||
ProgramKey lookup_key;
|
||||
GraphicContext& graphics;
|
||||
ShaderRecompiler::IR::ImageHeapSource* image_heap;
|
||||
const ShaderRecompiler::Spirv::Profile profile;
|
||||
uint64_t next_shader_id = 0;
|
||||
};
|
||||
return bind(CompileModule(entry->second, entry->first, options, std::move(translated),
|
||||
push_data_cursor));
|
||||
}
|
||||
|
||||
PipelineCache::PipelineCache(GraphicContext& graphics,
|
||||
ShaderRecompiler::IR::ImageHeapSource* image_heap)
|
||||
: m_graphics(graphics), m_program_cache(std::make_unique<ProgramCache>(graphics, image_heap)),
|
||||
ShaderRecompiler::IR::ImageHeapSource* image_heap,
|
||||
const std::filesystem::path& shader_store)
|
||||
: m_graphics(graphics), m_image_heap(image_heap),
|
||||
m_next_driver_cache_save(std::chrono::steady_clock::now() + DriverCacheSaveInterval) {
|
||||
EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
|
||||
InitializeDriverCache();
|
||||
WarmUp(shader_store);
|
||||
}
|
||||
|
||||
PipelineCache::~PipelineCache() {
|
||||
Save();
|
||||
auto destroy = [this](const auto& pipelines) {
|
||||
for (const auto& [key, pipeline]: pipelines) {
|
||||
(void)key;
|
||||
for (const auto& pipeline: pipelines | std::views::values) {
|
||||
m_graphics.device.destroyPipeline(pipeline.pipeline, nullptr);
|
||||
m_graphics.device.destroyPipelineLayout(pipeline.pipeline_layout, nullptr);
|
||||
m_graphics.device.destroyDescriptorSetLayout(pipeline.descriptor_set_layout, nullptr);
|
||||
@@ -510,6 +479,11 @@ PipelineCache::~PipelineCache() {
|
||||
};
|
||||
destroy(m_graphics_pipelines);
|
||||
destroy(m_compute_pipelines);
|
||||
for (const auto& program: m_program_cache | std::views::values) {
|
||||
for (const auto& module: program.modules) {
|
||||
m_graphics.device.destroyShaderModule(module.module, nullptr);
|
||||
}
|
||||
}
|
||||
if (m_driver_cache != nullptr) {
|
||||
m_graphics.device.destroyPipelineCache(m_driver_cache, nullptr);
|
||||
}
|
||||
@@ -695,10 +669,27 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
|
||||
std::array<ShaderParams, 3> vertex_params;
|
||||
if (tess_active) {
|
||||
vertex_params = PrepareTessellationPrograms(vertex_regs, context, vertex_info);
|
||||
// Reflection decodes both programs; the key also covers the VGT fields in tess.
|
||||
const uint64_t layout_key[] = {vertex_params[0].hash, vertex_params[1].hash,
|
||||
uint64_t {sh.m_vgtLsHsConfig & 0xfff00u} << 32u |
|
||||
sh.m_vgtTfParam};
|
||||
const auto [layout, inserted] = m_tess_layouts.try_emplace(
|
||||
XXH3_64bits(layout_key, sizeof(layout_key)), vertex_info[0].tess);
|
||||
if (inserted) {
|
||||
ShaderRecompiler::AnalyzeTessellationPrograms(vertex_params[0].code,
|
||||
vertex_params[1].code, layout->second);
|
||||
StoreLayout(layout->first, layout->second);
|
||||
}
|
||||
for (auto& stage: vertex_info) {
|
||||
stage.tess = layout->second;
|
||||
}
|
||||
} else {
|
||||
vertex_params[0] = PrepareProgram(vertex_regs, context, user_config, vertex_info[0]);
|
||||
}
|
||||
const bool mesh_active = vertex_info[0].logical_stage == ShaderType::Mesh;
|
||||
if (!tess_active && !mesh_active) {
|
||||
vertex_info[0].indirect_offsets = state.indirect_offsets;
|
||||
}
|
||||
if (object_primitive_id) {
|
||||
// CxObjectIdMode: register source plus a 32-bit pipelined primitive-ID addition.
|
||||
EXIT_NOT_IMPLEMENTED(sh.m_paClObjprimIdCntl != 2u || mesh_active || tess_active ||
|
||||
@@ -723,8 +714,13 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
|
||||
mesh.max_vertices > limits.maxMeshOutputVertices ||
|
||||
mesh.max_primitives > limits.maxMeshOutputPrimitives ||
|
||||
mesh.lds_size_dwords * sizeof(uint32_t) > limits.maxMeshSharedMemorySize) {
|
||||
EXIT("mesh shader exceeds host limits: threads=%u vertices=%u primitives=%u LDS=%u\n",
|
||||
host_threads, mesh.max_vertices, mesh.max_primitives, mesh.lds_size_dwords);
|
||||
static std::atomic_bool warned = false;
|
||||
if (!warned.exchange(true, std::memory_order_relaxed)) {
|
||||
Log::WriteToConsoleAndLog(fmt::format(
|
||||
"Warning: dropping mesh draws the host does not support (shader 0x{:016x}).\n",
|
||||
vertex_params[0].hash));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
ShaderParams pixel_params;
|
||||
@@ -748,6 +744,11 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
|
||||
const auto& rt = context.GetRenderTarget(slot);
|
||||
const auto target_mask = render_target_mask_slot(context.GetRenderTargetMask(), slot);
|
||||
if (rt.base.addr == 0 || target_mask == 0u) continue;
|
||||
if (rt.info.channel_type == Prospero::ChannelType::kUInt) {
|
||||
pixel_info.target_uint_mask |= 1u << export_index;
|
||||
} else if (rt.info.channel_type == Prospero::ChannelType::kSInt) {
|
||||
pixel_info.target_sint_mask |= 1u << export_index;
|
||||
}
|
||||
const auto format = Prospero::ResolveRenderTargetFormat(rt.info.format, rt.info.channel_type);
|
||||
if (format.buffer_format != Prospero::BufferFormat::k11_11_10UNorm) continue;
|
||||
const auto mapping = pixel_info.target_export_mapping[export_index];
|
||||
@@ -779,9 +780,11 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
|
||||
(blend.separate_alpha_blend && (BlendFactorIsDualSource(blend.alpha_srcblend) ||
|
||||
BlendFactorIsDualSource(blend.alpha_destblend))));
|
||||
if (pixel_info.dual_source_blending) {
|
||||
// MRT1 supplies the second blend source for target 0.
|
||||
// MRT1 supplies the second blend source for target 0; blended targets are not integer.
|
||||
pixel_info.target_output_mode[1] = pixel_info.target_output_mode[0];
|
||||
pixel_info.target_export_mapping[1] = pixel_info.target_export_mapping[0];
|
||||
pixel_info.target_uint_mask &= ~2u;
|
||||
pixel_info.target_sint_mask &= ~2u;
|
||||
} else if (blend.enable && !context.GetRenderTarget(0).info.blend_bypass &&
|
||||
pixel_info.target_output_mode[0] != 0 && pixel_info.target_output_mode[0] != 7 &&
|
||||
std::all_of(std::begin(pixel_info.target_output_mode) + 1,
|
||||
@@ -832,8 +835,8 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
|
||||
uint32_t push_data_cursor = ShaderRecompiler::IR::PushData::GraphicsDrawDwords(
|
||||
mesh_active, object_primitive_id, last_stage.user_clip_planes);
|
||||
if (pixel_active) {
|
||||
m_program_cache->Get(pixel_params, pixel_info, push_data_cursor);
|
||||
for (const auto& output: pixel_info.stage.program->info.outputs) {
|
||||
const auto& pixel_module = *GetProgram(pixel_params, pixel_info, push_data_cursor);
|
||||
for (const auto& output: pixel_module.info.info.outputs) {
|
||||
if (output.kind == ShaderRecompiler::IR::StageOutputKind::Mrt ||
|
||||
output.kind == ShaderRecompiler::IR::StageOutputKind::ObjectPrimitiveId) {
|
||||
state.mrt_mask |= 1u << output.location;
|
||||
@@ -842,52 +845,52 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
|
||||
state.mrt_mask &= color_output_mask;
|
||||
}
|
||||
for (uint32_t i = 0; i < (tess_active ? 3u : 1u); i++) {
|
||||
m_program_cache->Get(vertex_params[i], vertex_info[i], push_data_cursor);
|
||||
GetProgram(vertex_params[i], vertex_info[i], push_data_cursor);
|
||||
}
|
||||
}
|
||||
|
||||
ShaderProgram PipelineCache::GetComputeProgram(const HW::ComputeShaderInfo& regs,
|
||||
ShaderComputeInputInfo& input_info) {
|
||||
const ShaderModule* PipelineCache::GetComputeProgram(const HW::ComputeShaderInfo& regs,
|
||||
ShaderComputeInputInfo& input_info) {
|
||||
input_info.host_subgroup_size = m_graphics.SupportsComputeWave64() ? 64u : 32u;
|
||||
const auto params = PrepareProgram(regs, input_info);
|
||||
input_info.lds_storage = input_info.lds_size_dwords * 4u >
|
||||
m_graphics.GetPhysicalDeviceProperties().limits.maxComputeSharedMemorySize;
|
||||
uint32_t push_data_cursor = 0;
|
||||
return m_program_cache->Get(params, input_info, push_data_cursor);
|
||||
}
|
||||
|
||||
bool PipelineStaticParameters::operator==(const PipelineStaticParameters& other) const noexcept {
|
||||
return std::memcmp(this, &other, sizeof(*this)) == 0;
|
||||
return GetProgram(params, input_info, push_data_cursor);
|
||||
}
|
||||
|
||||
PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(const CommandBuffer& command,
|
||||
const DrawRenderState& state) {
|
||||
const auto* colors = state.color_info;
|
||||
const auto& depth = state.depth_info;
|
||||
const auto vertex_info = state.VertexStages();
|
||||
const auto& ps_input_info = state.ps_input_info;
|
||||
const auto& vs_input_info = vertex_info.front();
|
||||
const auto* vertex_program = vs_input_info.stage.handle;
|
||||
const auto* pixel_program = state.ps_input_info.stage.handle;
|
||||
const auto* colors = state.color_info;
|
||||
const auto& depth = state.depth_info;
|
||||
const auto vertex_info = state.VertexStages();
|
||||
const auto& ps_input_info = state.ps_input_info;
|
||||
const auto& vs_input_info = vertex_info.front();
|
||||
KYTY_PROFILER_BLOCK("PipelineCache::CreatePipeline(Gfx)", profiler::colors::DeepOrangeA200);
|
||||
|
||||
EXIT_IF(vertex_program == nullptr);
|
||||
const bool ps_active = state.ps_active;
|
||||
EXIT_IF(ps_active && pixel_program == nullptr);
|
||||
const bool ps_active = state.ps_active;
|
||||
const bool tessellation = vertex_info.size() == 3;
|
||||
std::array<const ShaderModule*, 4> stages {};
|
||||
for (uint32_t i = 0; i < vertex_info.size(); i++) {
|
||||
stages[i] = vertex_info[i].stage.module;
|
||||
}
|
||||
if (ps_active) {
|
||||
stages[3] = ps_input_info.stage.module;
|
||||
}
|
||||
EXIT_IF(stages[0] == nullptr || (ps_active && stages[3] == nullptr));
|
||||
const bool mesh = stages[0]->info.stage == ShaderType::Mesh;
|
||||
const auto color_count = state.color_count;
|
||||
|
||||
auto& ctx = command.GetRegisters();
|
||||
|
||||
const HW::ModeControl& mc = ctx.GetModeControl();
|
||||
|
||||
const auto vs_id = vertex_program->id;
|
||||
const auto ps_id = ps_active ? pixel_program->id : 0;
|
||||
|
||||
GraphicsPipelineKey key {};
|
||||
for (uint32_t i = 0; i < vertex_info.size(); i++) {
|
||||
key.vertex_shader_ids[i] = vertex_info[i].stage.handle->id;
|
||||
for (uint32_t i = 0; i < stages.size(); i++) {
|
||||
if (stages[i] != nullptr) {
|
||||
key.stage_ids[i] = stages[i]->id;
|
||||
}
|
||||
}
|
||||
key.ps_shader_id = ps_id;
|
||||
auto& static_params = key.static_params;
|
||||
auto& rendering = key.rendering;
|
||||
rendering.color_count = 0;
|
||||
@@ -1003,14 +1006,17 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(const CommandBuffer&
|
||||
}
|
||||
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;
|
||||
static_params.face = mc.face;
|
||||
static_params.provoking_vtx_last = mc.provoking_vtx_last;
|
||||
static_params.polygon_mode =
|
||||
ResolvePolygonMode(mc, static_params.cull_front, static_params.cull_back);
|
||||
ResolvePolygonMode(mc, !rect_list && mc.cull_front, !rect_list && mc.cull_back);
|
||||
if (tessellation) {
|
||||
static_params.patch_control_points = vs_input_info.tess.input_control_points;
|
||||
} else if (!mesh && static_params.topology == vk::PrimitiveTopology::ePatchList) {
|
||||
// Rectangle lists expand through generated tessellation stages.
|
||||
static_params.patch_control_points = 3;
|
||||
}
|
||||
|
||||
if (vs_input_info.stage.program->stage != ShaderType::Mesh) {
|
||||
if (!mesh) {
|
||||
EXIT_IF(vs_input_info.buffers_num < 0 ||
|
||||
vs_input_info.buffers_num > ShaderVertexInputInfo::RES_MAX ||
|
||||
vs_input_info.resources_num < 0 ||
|
||||
@@ -1045,22 +1051,17 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(const CommandBuffer&
|
||||
ShaderDbgDumpInputInfo(ps_input_info);
|
||||
}
|
||||
LOGF("PipelineTrace: shader modules VS=%" PRIu64 " module=%p PS=%" PRIu64 " module=%p\n",
|
||||
vs_id, static_cast<void*>(vertex_program->module), ps_id,
|
||||
ps_active ? static_cast<void*>(pixel_program->module) : nullptr);
|
||||
key.stage_ids[0], static_cast<void*>(stages[0]->module), key.stage_ids[3],
|
||||
ps_active ? static_cast<void*>(stages[3]->module) : nullptr);
|
||||
}
|
||||
|
||||
LogPipelineTrace("CreatePipelineInternal begin", vs_id, ps_id);
|
||||
{
|
||||
Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::PipelinesCreated,
|
||||
Common::DebugCounters::Counter::PipelineCreateNs,
|
||||
Common::DebugCounters::Peak::PipelineCreateNs);
|
||||
CreatePipelineInternal(m_graphics, pipeline, rendering, key.vertex_input, state,
|
||||
static_params, m_driver_cache);
|
||||
CreatePipelineInternal(m_graphics, pipeline, key, stages, m_driver_cache);
|
||||
}
|
||||
LogPipelineTrace("CreatePipelineInternal done", vs_id, ps_id);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
|
||||
StorePipeline(key);
|
||||
CheckpointDriverCache();
|
||||
|
||||
return pipeline;
|
||||
@@ -1069,10 +1070,10 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(const CommandBuffer&
|
||||
PipelineCache::Pipeline& PipelineCache::GetComputePipeline(const ShaderComputeInputInfo& input_info) {
|
||||
KYTY_PROFILER_BLOCK("PipelineCache::CreatePipeline(Compute)", profiler::colors::RedA100);
|
||||
|
||||
EXIT_IF(input_info.stage.handle == nullptr);
|
||||
const auto& compute_program = *input_info.stage.handle;
|
||||
EXIT_IF(input_info.stage.module == nullptr);
|
||||
const auto& module = *input_info.stage.module;
|
||||
|
||||
auto [iter, inserted] = m_compute_pipelines.try_emplace(compute_program.id);
|
||||
auto [iter, inserted] = m_compute_pipelines.try_emplace(module.id);
|
||||
auto& pipeline = iter->second;
|
||||
if (!inserted) {
|
||||
return pipeline;
|
||||
@@ -1086,12 +1087,8 @@ PipelineCache::Pipeline& PipelineCache::GetComputePipeline(const ShaderComputeIn
|
||||
Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::PipelinesCreated,
|
||||
Common::DebugCounters::Counter::PipelineCreateNs,
|
||||
Common::DebugCounters::Peak::PipelineCreateNs);
|
||||
CreatePipelineInternal(m_graphics, pipeline, input_info, compute_program.module,
|
||||
m_driver_cache);
|
||||
CreatePipelineInternal(m_graphics, pipeline, module, m_driver_cache);
|
||||
}
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
|
||||
CheckpointDriverCache();
|
||||
|
||||
return pipeline;
|
||||
|
||||
@@ -4,43 +4,62 @@
|
||||
#include "common/abi.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/file.h"
|
||||
#include "common/uniqueFunction.h"
|
||||
#include "graphics/host_gpu/renderer/colorRenderTarget.h"
|
||||
#include "graphics/host_gpu/renderer/depthRenderTarget.h"
|
||||
#include "graphics/host_gpu/renderer/renderTarget.h"
|
||||
#include "graphics/host_gpu/vulkanCommon.h"
|
||||
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
|
||||
#include "graphics/shader/rectListShader.h"
|
||||
#include "graphics/shader/shader.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <span>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
struct GraphicContext;
|
||||
struct ShaderParams;
|
||||
class CommandBuffer;
|
||||
|
||||
namespace ShaderRecompiler {
|
||||
struct CompileOptions;
|
||||
struct ShaderSource;
|
||||
struct TranslateResult;
|
||||
} // namespace ShaderRecompiler
|
||||
|
||||
namespace ShaderRecompiler::IR {
|
||||
class ImageHeapSource;
|
||||
} // namespace ShaderRecompiler::IR
|
||||
|
||||
namespace ShaderRecompiler::Spirv {
|
||||
struct Profile;
|
||||
} // namespace ShaderRecompiler::Spirv
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
struct PipelineStaticParameters {
|
||||
bool negative_one_to_one = false;
|
||||
bool depth_clip_enable = true;
|
||||
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
|
||||
// 0 without tessellation, 3 for a rectangle list, else the input control points.
|
||||
uint32_t patch_control_points = 0;
|
||||
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;
|
||||
bool face = false;
|
||||
bool provoking_vtx_last = false;
|
||||
vk::PolygonMode polygon_mode = vk::PolygonMode::eFill;
|
||||
uint8_t color_srcblend[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||
@@ -51,51 +70,59 @@ struct PipelineStaticParameters {
|
||||
uint8_t alpha_destblend[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||
bool separate_alpha_blend[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||
bool blend_enable[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||
|
||||
bool operator==(const PipelineStaticParameters& other) const noexcept;
|
||||
};
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
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) == 117);
|
||||
|
||||
struct PipelineRenderingState {
|
||||
std::array<vk::Format, RENDER_COLOR_ATTACHMENTS_MAX> color_formats {};
|
||||
vk::Format depth_format = vk::Format::eUndefined;
|
||||
vk::Format stencil_format = vk::Format::eUndefined;
|
||||
uint32_t color_count = 0;
|
||||
|
||||
bool operator==(const PipelineRenderingState&) const = default;
|
||||
};
|
||||
|
||||
struct PipelineVertexInputState {
|
||||
struct Binding {
|
||||
uint32_t stride = 0;
|
||||
bool instance = false;
|
||||
bool operator==(const Binding&) const = default;
|
||||
uint32_t stride = 0;
|
||||
uint8_t instance = 0;
|
||||
};
|
||||
struct Attribute {
|
||||
uint32_t offset = 0;
|
||||
uint8_t binding = 0;
|
||||
bool operator==(const Attribute&) const = default;
|
||||
uint32_t offset = 0;
|
||||
uint8_t binding = 0;
|
||||
};
|
||||
|
||||
std::array<Binding, ShaderVertexInputInfo::RES_MAX> bindings {};
|
||||
std::array<Attribute, ShaderVertexInputInfo::RES_MAX> attributes {};
|
||||
uint8_t binding_count = 0;
|
||||
uint8_t attribute_count = 0;
|
||||
|
||||
bool operator==(const PipelineVertexInputState&) const = default;
|
||||
};
|
||||
|
||||
struct ShaderProgram {
|
||||
uint64_t id = 0;
|
||||
vk::ShaderModule module = nullptr;
|
||||
// Everything a graphics pipeline is made of besides the stage modules its ids name. Aligned so
|
||||
// that rendering.color_formats can be passed to Vulkan.
|
||||
struct alignas(4) GraphicsPipelineKey {
|
||||
// VS, LS or mesh; HS; TES; PS. 0 marks an absent stage.
|
||||
std::array<uint64_t, 4> stage_ids {};
|
||||
PipelineRenderingState rendering;
|
||||
PipelineVertexInputState vertex_input;
|
||||
PipelineStaticParameters static_params;
|
||||
|
||||
explicit operator bool() const { return id != 0 && module != nullptr; }
|
||||
bool operator==(const GraphicsPipelineKey& other) const;
|
||||
};
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
// Keys are compared and hashed as bytes.
|
||||
static_assert(std::has_unique_object_representations_v<GraphicsPipelineKey>);
|
||||
|
||||
// A compiled stage with what pipeline creation needs from it besides the key.
|
||||
struct ShaderModule {
|
||||
uint64_t id = 0;
|
||||
vk::ShaderModule module = nullptr;
|
||||
ShaderRecompiler::IR::CompiledShaderInfo info;
|
||||
ShaderRecompiler::IR::ResourceSpecialization specialization;
|
||||
std::vector<uint32_t> function_code;
|
||||
// VS or LS: the host format of each vertex input attribute.
|
||||
std::vector<vk::Format> vertex_formats;
|
||||
// Rectangle-list PS: the parameters it reads.
|
||||
std::vector<RectListParameter> rect_parameters;
|
||||
};
|
||||
|
||||
// A draw's topology, shader stages (from PipelineCache::GetGraphicsPrograms) and targets.
|
||||
@@ -110,16 +137,43 @@ struct DrawRenderState {
|
||||
bool ps_active = false;
|
||||
std::array<ShaderVertexInputInfo, 3> vertex_info;
|
||||
ShaderPixelInputInfo ps_input_info;
|
||||
// Input: the user-data slots a GPU-read indirect draw patches.
|
||||
ShaderIndirectOffsets indirect_offsets;
|
||||
|
||||
[[nodiscard]] std::span<const ShaderVertexInputInfo> VertexStages() const {
|
||||
return {vertex_info.data(), vertex_info[1].stage ? 3u : 1u};
|
||||
}
|
||||
};
|
||||
|
||||
// Writes the shader store on its own thread, running jobs in the order they were posted.
|
||||
class ShaderStoreWriter {
|
||||
public:
|
||||
// Keeps the first `kept` bytes of the store at `path`, or starts it over with `header`. The
|
||||
// thread runs only if that succeeded.
|
||||
void Open(const std::filesystem::path& path, std::span<const uint8_t> header, uint64_t kept);
|
||||
explicit operator bool() const { return m_thread.joinable(); }
|
||||
// Compresses `record` and appends it on the store thread, in call order.
|
||||
void Append(std::vector<uint8_t>&& record);
|
||||
|
||||
private:
|
||||
friend struct PipelineCacheTestAccess;
|
||||
|
||||
void Post(Common::UniqueFunction<void>&& job);
|
||||
|
||||
Common::File m_file;
|
||||
std::mutex m_mutex;
|
||||
std::condition_variable_any m_ready;
|
||||
std::queue<Common::UniqueFunction<void>> m_jobs;
|
||||
// Declared last, so it drains the queue and joins before the rest is destroyed.
|
||||
std::jthread m_thread;
|
||||
};
|
||||
|
||||
// The owning renderer serializes access, including saves while the GPU is running.
|
||||
class PipelineCache {
|
||||
public:
|
||||
PipelineCache(GraphicContext& graphics, ShaderRecompiler::IR::ImageHeapSource* image_heap);
|
||||
// Without a shader store path nothing is kept across runs.
|
||||
PipelineCache(GraphicContext& graphics, ShaderRecompiler::IR::ImageHeapSource* image_heap,
|
||||
const std::filesystem::path& shader_store = {});
|
||||
~PipelineCache();
|
||||
KYTY_CLASS_NO_COPY(PipelineCache);
|
||||
void Save();
|
||||
@@ -134,64 +188,104 @@ public:
|
||||
|
||||
// Reads the shaders and registers bound in `command` into the state's shader stages.
|
||||
void GetGraphicsPrograms(const CommandBuffer& command, DrawRenderState& state);
|
||||
// An empty program skips the dispatch.
|
||||
ShaderProgram GetComputeProgram(const HW::ComputeShaderInfo& regs,
|
||||
ShaderComputeInputInfo& input_info);
|
||||
// Null skips the dispatch.
|
||||
const ShaderModule* GetComputeProgram(const HW::ComputeShaderInfo& regs,
|
||||
ShaderComputeInputInfo& input_info);
|
||||
|
||||
Pipeline& GetGraphicsPipeline(const CommandBuffer& command, const DrawRenderState& state);
|
||||
Pipeline& GetComputePipeline(const ShaderComputeInputInfo& input_info);
|
||||
|
||||
private:
|
||||
struct ProgramCache;
|
||||
friend struct PipelineCacheTestAccess;
|
||||
|
||||
struct GraphicsPipelineKey {
|
||||
PipelineRenderingState rendering;
|
||||
std::array<uint64_t, 3> vertex_shader_ids {};
|
||||
uint64_t ps_shader_id = 0;
|
||||
PipelineVertexInputState vertex_input;
|
||||
PipelineStaticParameters static_params;
|
||||
struct Program {
|
||||
std::unique_ptr<ShaderRecompiler::ShaderSource> call_source;
|
||||
ShaderRecompiler::IR::ResourcePlan resource_plan;
|
||||
ShaderRecompiler::IR::ResourceSnapshot resources;
|
||||
ShaderRecompiler::IR::ResourceSpecialization specialization;
|
||||
std::vector<ShaderModule> modules;
|
||||
bool skip_dispatch = false;
|
||||
|
||||
bool operator==(const GraphicsPipelineKey& other) const {
|
||||
return rendering == other.rendering && vertex_shader_ids == other.vertex_shader_ids &&
|
||||
ps_shader_id == other.ps_shader_id && vertex_input == other.vertex_input &&
|
||||
static_params == other.static_params;
|
||||
}
|
||||
[[nodiscard]] std::span<const uint32_t> FunctionCode() const;
|
||||
bool RefreshCallSource(const ShaderRecompiler::IR::SrtRuntime& runtime);
|
||||
template <bool HasCalls>
|
||||
ShaderModule* FindModule(const ShaderRecompiler::IR::SrtRuntime& runtime,
|
||||
uint32_t push_data_cursor);
|
||||
};
|
||||
|
||||
struct PipelineKeyHash {
|
||||
static void Mix(std::size_t& hash, std::size_t value) {
|
||||
hash ^= value + static_cast<std::size_t>(0x9e3779b97f4a7c15ull) + (hash << 6u) +
|
||||
(hash >> 2u);
|
||||
}
|
||||
struct ProgramKey {
|
||||
ShaderType stage = ShaderType::Unknown;
|
||||
uint64_t hash = 0;
|
||||
uint32_t user_data_count = 0;
|
||||
std::vector<uint32_t> static_state;
|
||||
|
||||
bool operator==(const ProgramKey&) const = default;
|
||||
};
|
||||
|
||||
// Variants of one source share a bucket; ProgramKey equality compares the state.
|
||||
struct ProgramKeyHash {
|
||||
std::size_t operator()(const ProgramKey& key) const { return key.hash; }
|
||||
};
|
||||
|
||||
struct GraphicsPipelineKeyHash {
|
||||
std::size_t operator()(const GraphicsPipelineKey& key) const;
|
||||
};
|
||||
|
||||
GraphicContext& m_graphics;
|
||||
std::unique_ptr<ProgramCache> m_program_cache;
|
||||
using ModuleMap = std::unordered_map<uint64_t, const ShaderModule*>;
|
||||
|
||||
GraphicContext& m_graphics;
|
||||
ShaderRecompiler::IR::ImageHeapSource* m_image_heap;
|
||||
std::unordered_map<ProgramKey, Program, ProgramKeyHash> m_program_cache;
|
||||
// Modules compiled per stage.
|
||||
std::array<uint32_t, static_cast<size_t>(ShaderType::TessellationEvaluation) + 1> m_compiled {};
|
||||
ProgramKey m_lookup_key;
|
||||
vk::PipelineCache m_driver_cache = nullptr;
|
||||
std::filesystem::path m_driver_cache_path;
|
||||
std::chrono::steady_clock::time_point m_next_driver_cache_save;
|
||||
bool m_driver_cache_dirty = false;
|
||||
std::unordered_map<GraphicsPipelineKey, Pipeline, GraphicsPipelineKeyHash> m_graphics_pipelines;
|
||||
std::unordered_map<uint64_t, Pipeline> m_compute_pipelines;
|
||||
std::unordered_map<uint64_t, ShaderTessellationInputInfo> m_tess_layouts;
|
||||
// Records everything first created; closed without a shader store.
|
||||
ShaderStoreWriter m_store;
|
||||
|
||||
template <typename InputInfo>
|
||||
const ShaderModule* GetProgram(const ShaderParams& params, InputInfo& input_info,
|
||||
uint32_t& push_data_cursor);
|
||||
ShaderModule& CompileModule(Program& program, const ProgramKey& key,
|
||||
ShaderRecompiler::CompileOptions options,
|
||||
ShaderRecompiler::TranslateResult translated,
|
||||
uint32_t push_data_cursor);
|
||||
void InitializeDriverCache();
|
||||
void CheckpointDriverCache();
|
||||
|
||||
// pipelineSerialization.cpp
|
||||
void WarmUp(const std::filesystem::path& path);
|
||||
bool LoadRecord(std::span<const uint8_t>& records, std::vector<GraphicsPipelineKey>& pipelines);
|
||||
void StoreModule(const ProgramKey& key, const Program& program, const ShaderModule& module,
|
||||
const std::vector<uint32_t>& spirv);
|
||||
void StorePipeline(const GraphicsPipelineKey& key);
|
||||
void StoreLayout(uint64_t key, const ShaderTessellationInputInfo& layout);
|
||||
// Every module by id, with id 0 naming an absent stage.
|
||||
[[nodiscard]] ModuleMap MapModules() const;
|
||||
// False when a stage id of the key is not in `modules`.
|
||||
bool CreateGraphicsPipeline(Pipeline& pipeline, const GraphicsPipelineKey& key,
|
||||
const ModuleMap& modules);
|
||||
};
|
||||
|
||||
void LogPipelineTrace(const char* phase, uint64_t vertex_program_id, uint64_t pixel_program_id);
|
||||
// The shader store of the running title; empty unless --shader-cache is on in a Release build
|
||||
// without debug dumps, which change the SPIR-V.
|
||||
std::filesystem::path ShaderStorePath();
|
||||
// Host capabilities that change the emitted SPIR-V.
|
||||
ShaderRecompiler::Spirv::Profile SpirvProfile(const GraphicContext& graphics);
|
||||
|
||||
// Stages are indexed like GraphicsPipelineKey::stage_ids.
|
||||
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& pipeline,
|
||||
const PipelineRenderingState& rendering,
|
||||
const PipelineVertexInputState& vertex_input,
|
||||
const DrawRenderState& state,
|
||||
const PipelineStaticParameters& static_params,
|
||||
vk::PipelineCache driver_cache);
|
||||
const GraphicsPipelineKey& key,
|
||||
const std::array<const ShaderModule*, 4>& stages,
|
||||
vk::PipelineCache driver_cache);
|
||||
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& pipeline,
|
||||
const ShaderComputeInputInfo& input_info,
|
||||
vk::ShaderModule compute_module, vk::PipelineCache driver_cache);
|
||||
const ShaderModule& module, vk::PipelineCache driver_cache);
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h"
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "graphics/host_gpu/vulkanCommon.h"
|
||||
#include "graphics/shader/recompiler/ShaderRecompiler.h"
|
||||
#include "graphics/shader/recompiler/ir/Serialize.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <fmt/format.h>
|
||||
#include <ranges>
|
||||
#include <xxhash.h>
|
||||
#include <zstd.h>
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#endif
|
||||
|
||||
// The shader store: a header, then zstd-compressed records of everything a run created first.
|
||||
// Records are only appended, so a store cut short by a crash keeps every complete record.
|
||||
namespace Libs::Graphics {
|
||||
|
||||
namespace {
|
||||
|
||||
using ShaderRecompiler::IR::IoAll;
|
||||
using ShaderRecompiler::IR::Reader;
|
||||
using ShaderRecompiler::IR::Writer;
|
||||
|
||||
enum class Record : uint8_t { Module, GraphicsPipeline, TessellationLayout };
|
||||
|
||||
// Precedes each record's zstd frame.
|
||||
struct Frame {
|
||||
uint32_t compressed_size = 0;
|
||||
uint32_t raw_size = 0;
|
||||
uint64_t checksum = 0; // XXH3 of the compressed bytes.
|
||||
};
|
||||
|
||||
// Changes with every build, so records never meet code that encodes them differently. Zero when
|
||||
// the executable cannot be found.
|
||||
uint64_t BuildStamp() {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
std::array<wchar_t, 32768> name {};
|
||||
GetModuleFileNameW(nullptr, name.data(), static_cast<DWORD>(name.size()));
|
||||
const std::filesystem::path path = name.data();
|
||||
#elif defined(__APPLE__)
|
||||
std::array<char, 4096> name {};
|
||||
auto size = static_cast<uint32_t>(name.size());
|
||||
_NSGetExecutablePath(name.data(), &size);
|
||||
const std::filesystem::path path = name.data();
|
||||
#else
|
||||
const std::filesystem::path path = "/proc/self/exe";
|
||||
#endif
|
||||
std::error_code error;
|
||||
const auto time = std::filesystem::last_write_time(path, error).time_since_epoch().count();
|
||||
return error ? 0 : static_cast<uint64_t>(time);
|
||||
}
|
||||
|
||||
// What every record depends on besides its own fields; empty when the build is unknown.
|
||||
std::vector<uint8_t> StoreHeader(const GraphicContext& graphics) {
|
||||
const auto stamp = BuildStamp();
|
||||
if (stamp == 0) {
|
||||
return {};
|
||||
}
|
||||
Writer header;
|
||||
IoAll(header, stamp, SpirvProfile(graphics),
|
||||
graphics.GetPhysicalDeviceProperties().limits.maxComputeSharedMemorySize);
|
||||
return std::move(header.bytes);
|
||||
}
|
||||
|
||||
// Record fields, shared by writing and reading.
|
||||
template <class S, class Key> void IoKey(S& io, Key& key) {
|
||||
auto& [stage, hash, user_data_count, static_state] = key;
|
||||
IoAll(io, stage, hash, user_data_count, static_state);
|
||||
}
|
||||
|
||||
template <class S, class Module> void IoModule(S& io, Module& stage) {
|
||||
// The handle comes from the SPIR-V; stored programs have no function code.
|
||||
auto& [id, handle, info, specialization, function_code, vertex_formats, rect_parameters] = stage;
|
||||
IoAll(io, id, info, specialization, vertex_formats, rect_parameters);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ShaderStoreWriter::Open(const std::filesystem::path& path, std::span<const uint8_t> header,
|
||||
uint64_t kept) {
|
||||
Common::File::CreateDirectories(path.parent_path());
|
||||
if (kept == 0) {
|
||||
if (m_file.Create(path)) {
|
||||
m_file.Write(header.data(), static_cast<uint32_t>(header.size()));
|
||||
}
|
||||
} else if (m_file.Open(path, Common::File::Mode::ReadWrite)) {
|
||||
m_file.Truncate(kept);
|
||||
m_file.Seek(kept);
|
||||
}
|
||||
if (m_file.IsInvalid()) {
|
||||
return;
|
||||
}
|
||||
m_thread = std::jthread([this](std::stop_token stop) {
|
||||
std::unique_lock lock(m_mutex);
|
||||
// Once stop is requested the wait returns false only when the queue is empty.
|
||||
while (m_ready.wait(lock, stop, [this] { return !m_jobs.empty(); })) {
|
||||
const auto job = std::move(m_jobs.front());
|
||||
m_jobs.pop();
|
||||
lock.unlock();
|
||||
job();
|
||||
lock.lock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ShaderStoreWriter::Append(std::vector<uint8_t>&& record) {
|
||||
Post([this, record = std::move(record)] {
|
||||
std::vector<uint8_t> bytes(sizeof(Frame) + ZSTD_compressBound(record.size()));
|
||||
const auto size = ZSTD_compress(bytes.data() + sizeof(Frame), bytes.size() - sizeof(Frame),
|
||||
record.data(), record.size(), 3);
|
||||
EXIT_IF(ZSTD_isError(size));
|
||||
const Frame frame {.compressed_size = static_cast<uint32_t>(size),
|
||||
.raw_size = static_cast<uint32_t>(record.size()),
|
||||
.checksum = XXH3_64bits(bytes.data() + sizeof(Frame), size)};
|
||||
std::memcpy(bytes.data(), &frame, sizeof(frame));
|
||||
// A failed write leaves a partial record; the next run drops it and everything after it.
|
||||
m_file.Write(bytes.data(), static_cast<uint32_t>(sizeof(Frame) + size));
|
||||
});
|
||||
}
|
||||
|
||||
void ShaderStoreWriter::Post(Common::UniqueFunction<void>&& job) {
|
||||
{
|
||||
std::scoped_lock lock(m_mutex);
|
||||
m_jobs.push(std::move(job));
|
||||
}
|
||||
m_ready.notify_one();
|
||||
}
|
||||
|
||||
void PipelineCache::WarmUp(const std::filesystem::path& path) {
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto header = StoreHeader(m_graphics);
|
||||
if (header.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
std::vector<uint8_t> bytes;
|
||||
if (Common::File file;
|
||||
Common::File::IsFileExisting(path) && file.Open(path, Common::File::Mode::Read)) {
|
||||
bytes.resize(file.Size());
|
||||
uint32_t read = 0;
|
||||
file.Read(bytes.data(), static_cast<uint32_t>(bytes.size()), &read);
|
||||
bytes.resize(read);
|
||||
}
|
||||
Log::WriteToConsoleAndLog(
|
||||
fmt::format("Shader cache: compiling shaders and pipelines from {} ({:.1f} MB)\n",
|
||||
Common::PathToString(path), static_cast<double>(bytes.size()) / 1'000'000.0));
|
||||
std::vector<GraphicsPipelineKey> pipelines;
|
||||
auto records = std::span<const uint8_t>(bytes);
|
||||
const bool reusable =
|
||||
records.size() >= header.size() && std::ranges::equal(records.first(header.size()), header);
|
||||
if (reusable) {
|
||||
records = records.subspan(header.size());
|
||||
while (LoadRecord(records, pipelines)) {
|
||||
}
|
||||
}
|
||||
// A damaged record ends the store; the records before it stay.
|
||||
m_store.Open(path, header, reusable ? bytes.size() - records.size() : 0);
|
||||
|
||||
const auto modules = MapModules();
|
||||
for (const auto& key: pipelines) {
|
||||
Pipeline pipeline;
|
||||
if (CreateGraphicsPipeline(pipeline, key, modules)) {
|
||||
m_graphics_pipelines.emplace(key, pipeline);
|
||||
}
|
||||
}
|
||||
// A compute pipeline depends only on its module.
|
||||
for (const auto* module: modules | std::views::values) {
|
||||
if (module != nullptr && module->info.stage == ShaderType::Compute) {
|
||||
CreatePipelineInternal(m_graphics, m_compute_pipelines[module->id], *module,
|
||||
m_driver_cache);
|
||||
}
|
||||
}
|
||||
if (!m_graphics_pipelines.empty() || !m_compute_pipelines.empty()) {
|
||||
CheckpointDriverCache();
|
||||
}
|
||||
Log::WriteToConsoleAndLog(fmt::format(
|
||||
"Shader cache: loaded {} programs, {} modules, {} pipelines in {} ms\n",
|
||||
m_program_cache.size(), modules.size() - 1,
|
||||
m_graphics_pipelines.size() + m_compute_pipelines.size(),
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() -
|
||||
start)
|
||||
.count()));
|
||||
}
|
||||
|
||||
bool PipelineCache::LoadRecord(std::span<const uint8_t>& records,
|
||||
std::vector<GraphicsPipelineKey>& pipelines) {
|
||||
Frame frame;
|
||||
if (records.size() < sizeof(frame)) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(&frame, records.data(), sizeof(frame));
|
||||
if (records.size() - sizeof(frame) < frame.compressed_size) {
|
||||
return false;
|
||||
}
|
||||
const auto compressed = records.subspan(sizeof(frame), frame.compressed_size);
|
||||
if (XXH3_64bits(compressed.data(), compressed.size()) != frame.checksum ||
|
||||
ZSTD_getFrameContentSize(compressed.data(), compressed.size()) != frame.raw_size) {
|
||||
return false;
|
||||
}
|
||||
std::vector<uint8_t> raw(frame.raw_size);
|
||||
if (ZSTD_decompress(raw.data(), raw.size(), compressed.data(), compressed.size()) != raw.size()) {
|
||||
return false;
|
||||
}
|
||||
Reader reader {raw};
|
||||
auto type = Record::Module;
|
||||
const auto complete = [&] { return reader.ok && reader.bytes.empty(); };
|
||||
IoAll(reader, type);
|
||||
switch (type) {
|
||||
case Record::Module: {
|
||||
ProgramKey key;
|
||||
std::vector<uint8_t> plan;
|
||||
ShaderModule module;
|
||||
std::vector<uint32_t> spirv;
|
||||
IoKey(reader, key);
|
||||
IoAll(reader, plan);
|
||||
IoModule(reader, module);
|
||||
IoAll(reader, spirv);
|
||||
if (!complete()) {
|
||||
return false;
|
||||
}
|
||||
const auto [program, inserted] = m_program_cache.try_emplace(std::move(key));
|
||||
if (inserted &&
|
||||
!ShaderRecompiler::IR::LoadResourcePlan(plan, program->second.resource_plan)) {
|
||||
m_program_cache.erase(program);
|
||||
return false;
|
||||
}
|
||||
module.module = CompileSPV(spirv, m_graphics.device);
|
||||
if (module.module == nullptr) {
|
||||
return false;
|
||||
}
|
||||
program->second.modules.push_back(std::move(module));
|
||||
break;
|
||||
}
|
||||
case Record::GraphicsPipeline: {
|
||||
GraphicsPipelineKey key;
|
||||
reader.Raw(&key, sizeof(key));
|
||||
if (!complete()) {
|
||||
return false;
|
||||
}
|
||||
pipelines.push_back(key);
|
||||
break;
|
||||
}
|
||||
case Record::TessellationLayout: {
|
||||
uint64_t key = 0;
|
||||
ShaderTessellationInputInfo layout;
|
||||
IoAll(reader, key, layout);
|
||||
if (!complete()) {
|
||||
return false;
|
||||
}
|
||||
m_tess_layouts.try_emplace(key, layout);
|
||||
break;
|
||||
}
|
||||
default: return false;
|
||||
}
|
||||
records = records.subspan(sizeof(frame) + compressed.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Records are encoded here, since programs and modules change later.
|
||||
void PipelineCache::StoreModule(const ProgramKey& key, const Program& program,
|
||||
const ShaderModule& module, const std::vector<uint32_t>& spirv) {
|
||||
if (!m_store) {
|
||||
return;
|
||||
}
|
||||
Writer record;
|
||||
IoAll(record, Record::Module);
|
||||
IoKey(record, key);
|
||||
IoAll(record, ShaderRecompiler::IR::SaveResourcePlan(program.resource_plan));
|
||||
IoModule(record, module);
|
||||
IoAll(record, spirv);
|
||||
m_store.Append(std::move(record.bytes));
|
||||
}
|
||||
|
||||
void PipelineCache::StorePipeline(const GraphicsPipelineKey& key) {
|
||||
if (!m_store) {
|
||||
return;
|
||||
}
|
||||
Writer record;
|
||||
IoAll(record, Record::GraphicsPipeline);
|
||||
record.Raw(&key, sizeof(key));
|
||||
m_store.Append(std::move(record.bytes));
|
||||
}
|
||||
|
||||
void PipelineCache::StoreLayout(uint64_t key, const ShaderTessellationInputInfo& layout) {
|
||||
if (!m_store) {
|
||||
return;
|
||||
}
|
||||
Writer record;
|
||||
IoAll(record, Record::TessellationLayout, key, layout);
|
||||
m_store.Append(std::move(record.bytes));
|
||||
}
|
||||
|
||||
PipelineCache::ModuleMap PipelineCache::MapModules() const {
|
||||
ModuleMap modules {{0, nullptr}};
|
||||
for (const auto& program: m_program_cache | std::views::values) {
|
||||
for (const auto& module: program.modules) {
|
||||
modules.emplace(module.id, &module);
|
||||
}
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
bool PipelineCache::CreateGraphicsPipeline(Pipeline& pipeline, const GraphicsPipelineKey& key,
|
||||
const ModuleMap& modules) {
|
||||
std::array<const ShaderModule*, 4> stages {};
|
||||
for (size_t i = 0; i < stages.size(); i++) {
|
||||
const auto found = modules.find(key.stage_ids[i]);
|
||||
if (found == modules.end()) {
|
||||
return false;
|
||||
}
|
||||
stages[i] = found->second;
|
||||
}
|
||||
CreatePipelineInternal(m_graphics, pipeline, key, stages, m_driver_cache);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "graphics/host_gpu/renderer/pipeline/shaderResourceBarrier.h"
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h"
|
||||
#include "graphics/shader/shader.h"
|
||||
#include "graphics/shader/shaderBindings.h"
|
||||
|
||||
@@ -90,7 +91,7 @@ vk::BufferMemoryBarrier MakeGdsDependency(vk::Buffer buffer) {
|
||||
|
||||
bool HasShaderBufferWrites(const ShaderStageRuntime& runtime) {
|
||||
EXIT_IF(!runtime);
|
||||
const auto& program = *runtime.program;
|
||||
const auto& program = runtime.module->info;
|
||||
const auto& resources = *runtime.resources;
|
||||
EXIT_IF(resources.buffers.size() != program.info.buffers.size());
|
||||
// Address writes reach memory outside the bound buffer descriptors.
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "graphics/host_gpu/renderer/renderContext.h"
|
||||
#include "graphics/host_gpu/renderer/renderTarget.h"
|
||||
#include "graphics/host_gpu/vulkanCommon.h"
|
||||
#include "graphics/shader/recompiler/BufferFormat.h"
|
||||
#include "graphics/shader/recompiler/ir/ShaderIR.h"
|
||||
#include "graphics/shader/rectListShader.h"
|
||||
#include "graphics/shader/shader.h"
|
||||
@@ -21,124 +20,11 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
// IDK: maybe we can remove it?
|
||||
constexpr uint8_t kTemporaryVertexAttribFormat113 =
|
||||
static_cast<uint8_t>(Prospero::VertexAttribFormat::k16_16SInt);
|
||||
constexpr uint32_t kTemporaryPs5BufferFormat121 = 121u;
|
||||
|
||||
static bool NarrowInputFormat(vk::Format& format, uint32_t& size, uint32_t used_components) {
|
||||
if (used_components == 0 || used_components >= size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (format) {
|
||||
case vk::Format::eR32G32B32A32Sfloat:
|
||||
switch (used_components) {
|
||||
case 1: format = vk::Format::eR32Sfloat; break;
|
||||
case 2: format = vk::Format::eR32G32Sfloat; break;
|
||||
case 3: format = vk::Format::eR32G32B32Sfloat; break;
|
||||
default: return false;
|
||||
}
|
||||
size = used_components;
|
||||
return true;
|
||||
case vk::Format::eR32G32B32Sfloat:
|
||||
switch (used_components) {
|
||||
case 1: format = vk::Format::eR32Sfloat; break;
|
||||
case 2: format = vk::Format::eR32G32Sfloat; break;
|
||||
default: return false;
|
||||
}
|
||||
size = used_components;
|
||||
return true;
|
||||
case vk::Format::eR16G16B16A16Sfloat:
|
||||
switch (used_components) {
|
||||
case 1: format = vk::Format::eR16Sfloat; break;
|
||||
case 2: format = vk::Format::eR16G16Sfloat; break;
|
||||
default: return false;
|
||||
}
|
||||
size = used_components;
|
||||
return true;
|
||||
case vk::Format::eR8G8B8A8Unorm:
|
||||
switch (used_components) {
|
||||
case 1: format = vk::Format::eR8Unorm; break;
|
||||
case 2: format = vk::Format::eR8G8Unorm; break;
|
||||
default: return false;
|
||||
}
|
||||
size = used_components;
|
||||
return true;
|
||||
case vk::Format::eR8G8B8A8Snorm:
|
||||
if (used_components != 2) {
|
||||
return false;
|
||||
}
|
||||
format = vk::Format::eR8G8Snorm;
|
||||
size = 2;
|
||||
return true;
|
||||
case vk::Format::eR8G8B8A8Uint:
|
||||
switch (used_components) {
|
||||
case 1: format = vk::Format::eR8Uint; break;
|
||||
case 2: format = vk::Format::eR8G8Uint; break;
|
||||
default: return false;
|
||||
}
|
||||
size = used_components;
|
||||
return true;
|
||||
default: break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void GetInputFormat(const ShaderBufferResource& res, vk::Format& format, uint32_t& size,
|
||||
uint32_t used_components) {
|
||||
const auto fmt = res.Format();
|
||||
const auto raw_format = res.RawFormat();
|
||||
if (raw_format == kTemporaryVertexAttribFormat113) {
|
||||
static bool logged_113 = false;
|
||||
if (!logged_113) {
|
||||
LOGF("InputFormat: temporary: accepting invalid PS5 buffer format 113 as "
|
||||
"vk::Format::eR32G32B32A32Sfloat\n");
|
||||
logged_113 = true;
|
||||
}
|
||||
format = vk::Format::eR32G32B32A32Sfloat;
|
||||
size = 4;
|
||||
if (NarrowInputFormat(format, size, used_components)) {
|
||||
LOGF("InputFormat: narrowing fmt=%u to %s for used_components=%u\n", raw_format,
|
||||
vk::to_string(format).c_str(), used_components);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (raw_format == kTemporaryPs5BufferFormat121) {
|
||||
static bool logged_121 = false;
|
||||
if (!logged_121) {
|
||||
LOGF("InputFormat: accepting PS5 buffer format 121 as vk::Format::eR16G16Sfloat\n");
|
||||
logged_121 = true;
|
||||
}
|
||||
format = vk::Format::eR16G16Sfloat;
|
||||
size = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
format = VulkanFormat(fmt);
|
||||
size = ShaderRecompiler::Format::GetFormatInfo(fmt).component_count;
|
||||
if (format == vk::Format::eUndefined || size == 0) {
|
||||
EXIT("unknown vertex format: fmt = %u\n", raw_format);
|
||||
}
|
||||
|
||||
if (NarrowInputFormat(format, size, used_components)) {
|
||||
static std::atomic<uint64_t> log_count = 0;
|
||||
auto log_id = log_count.fetch_add(1);
|
||||
if (log_id < 32) {
|
||||
LOGF("VertexInput: narrowed vertex format to %" PRIu32
|
||||
" component(s) for shader fetch\n",
|
||||
used_components);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static vk::BlendFactor GetBlendFactor(uint32_t factor) {
|
||||
switch (static_cast<Prospero::BlendFactor>(factor)) {
|
||||
case Prospero::BlendFactor::kZero: return vk::BlendFactor::eZero;
|
||||
@@ -242,30 +128,27 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe
|
||||
|
||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
||||
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& pipeline,
|
||||
const PipelineRenderingState& rendering,
|
||||
const PipelineVertexInputState& vertex_input,
|
||||
const DrawRenderState& state,
|
||||
const PipelineStaticParameters& static_params,
|
||||
vk::PipelineCache driver_cache) {
|
||||
const auto vertex_info = state.VertexStages();
|
||||
const auto& vs_input_info = vertex_info.front();
|
||||
const auto* ps_input_info = state.ps_active ? &state.ps_input_info : nullptr;
|
||||
const auto& vertex_program = *vs_input_info.stage.handle;
|
||||
const auto* pixel_program = state.ps_input_info.stage.handle;
|
||||
const bool tessellation = vertex_info.size() == 3;
|
||||
const bool ps_active = state.ps_active;
|
||||
const bool with_depth = rendering.depth_format != vk::Format::eUndefined ||
|
||||
rendering.stencil_format != vk::Format::eUndefined;
|
||||
const bool mesh = vs_input_info.stage.program->stage == ShaderType::Mesh;
|
||||
const GraphicsPipelineKey& key,
|
||||
const std::array<const ShaderModule*, 4>& stages,
|
||||
vk::PipelineCache driver_cache) {
|
||||
const auto& rendering = key.rendering;
|
||||
const auto& vertex_input = key.vertex_input;
|
||||
const auto& static_params = key.static_params;
|
||||
const auto& vertex = *stages[0];
|
||||
const auto* pixel = stages[3];
|
||||
const bool with_depth = rendering.depth_format != vk::Format::eUndefined ||
|
||||
rendering.stencil_format != vk::Format::eUndefined;
|
||||
const bool mesh = vertex.info.stage == ShaderType::Mesh;
|
||||
EXIT_NOT_IMPLEMENTED(mesh && !graphics.mesh_shader_enabled);
|
||||
const bool rect_list =
|
||||
!mesh && !tessellation && static_params.topology == vk::PrimitiveTopology::ePatchList;
|
||||
const bool rect_list = static_params.patch_control_points != 0 && stages[1] == nullptr;
|
||||
|
||||
vk::ShaderModule tess_control_shader_module = nullptr;
|
||||
vk::ShaderModule tess_eval_shader_module = nullptr;
|
||||
|
||||
if (rect_list) {
|
||||
const auto shaders = BuildRectListShaders(vs_input_info, ps_input_info);
|
||||
std::span<const RectListParameter> parameters;
|
||||
if (pixel != nullptr) parameters = pixel->rect_parameters;
|
||||
const auto shaders = BuildRectListShaders(parameters, vertex.info.param_export_mask);
|
||||
tess_control_shader_module = CompileSPV(shaders.control, graphics.device);
|
||||
if (graphics_debug_dump_enabled()) {
|
||||
LOGF("PipelineTrace: vkCreateShaderModule RectList TCS done module=%p\n",
|
||||
@@ -284,11 +167,12 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
|
||||
vk::PipelineShaderStageCreateInfo shader_stages[4] {};
|
||||
uint32_t shader_stage_count = 0;
|
||||
for (uint32_t i = 0; i < vertex_info.size(); i++) {
|
||||
shader_stages[shader_stage_count++] = {.stage =
|
||||
NativeShaderStage(vertex_info[i].logical_stage),
|
||||
.module = vertex_info[i].stage.handle->module,
|
||||
.pName = "main"};
|
||||
for (const auto* stage: stages) {
|
||||
if (stage != nullptr) {
|
||||
shader_stages[shader_stage_count++] = {.stage = NativeShaderStage(stage->info.stage),
|
||||
.module = stage->module,
|
||||
.pName = "main"};
|
||||
}
|
||||
}
|
||||
if (rect_list) {
|
||||
shader_stages[shader_stage_count++] = {.stage =
|
||||
@@ -300,11 +184,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
.module = tess_eval_shader_module,
|
||||
.pName = "main"};
|
||||
}
|
||||
if (ps_active) {
|
||||
shader_stages[shader_stage_count++] = {.stage = vk::ShaderStageFlagBits::eFragment,
|
||||
.module = pixel_program->module,
|
||||
.pName = "main"};
|
||||
}
|
||||
|
||||
vk::VertexInputAttributeDescription input_attr[ShaderVertexInputInfo::RES_MAX] {};
|
||||
vk::VertexInputBindingDescription input_desc[ShaderVertexInputInfo::RES_MAX] {};
|
||||
@@ -320,46 +199,7 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
input_attr[index].binding = vertex_input.attributes[index].binding;
|
||||
input_attr[index].location = index;
|
||||
input_attr[index].offset = vertex_input.attributes[index].offset;
|
||||
|
||||
uint32_t attr_size = 4;
|
||||
const auto registers_num = vs_input_info.resources_dst[index].registers_num;
|
||||
const auto compiled_components =
|
||||
vs_input_info.stage.program->info.vertex_fetch_components[index];
|
||||
const auto used_components =
|
||||
compiled_components > 0 ? static_cast<int>(compiled_components) : registers_num;
|
||||
GetInputFormat(vs_input_info.resources[index], input_attr[index].format, attr_size,
|
||||
static_cast<uint32_t>(used_components));
|
||||
|
||||
if (graphics_debug_dump_enabled()) {
|
||||
static std::atomic_uint log_count = 0;
|
||||
const auto log_id = log_count.fetch_add(1, std::memory_order_relaxed);
|
||||
if (log_id < 128) {
|
||||
LOGF("VertexInputState[%u]: attr=%u binding=%u offset=%u stride=%u fmt=%d "
|
||||
"src_fmt=%u dst=v%u regs=%u"
|
||||
" fetched_components=%u attr_size=%u swizzle=%u,%u,%u,%u\n",
|
||||
log_id, index, input_attr[index].binding, input_attr[index].offset,
|
||||
input_desc[input_attr[index].binding].stride,
|
||||
static_cast<int>(input_attr[index].format),
|
||||
static_cast<uint32_t>(vs_input_info.resources[index].Format()),
|
||||
static_cast<uint32_t>(vs_input_info.resources_dst[index].register_start),
|
||||
static_cast<uint32_t>(registers_num), static_cast<uint32_t>(used_components),
|
||||
attr_size, static_cast<uint32_t>(vs_input_info.resources[index].DstSelX()),
|
||||
static_cast<uint32_t>(vs_input_info.resources[index].DstSelY()),
|
||||
static_cast<uint32_t>(vs_input_info.resources[index].DstSelZ()),
|
||||
static_cast<uint32_t>(vs_input_info.resources[index].DstSelW()));
|
||||
}
|
||||
}
|
||||
|
||||
if (vs_input_info.resources[index].OutOfBounds() != 0) {
|
||||
static bool logged = false;
|
||||
if (!logged) {
|
||||
LOGF("VertexInput: temporary: accepting PS5 out-of-bounds behavior %" PRIu8 "\n",
|
||||
vs_input_info.resources[index].OutOfBounds());
|
||||
logged = true;
|
||||
}
|
||||
}
|
||||
|
||||
EXIT_IF(registers_num < 1 || registers_num > 4);
|
||||
input_attr[index].format = vertex.vertex_formats[index];
|
||||
}
|
||||
|
||||
vk::PipelineVertexInputStateCreateInfo vertex_input_info {};
|
||||
@@ -379,17 +219,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
vk::PipelineViewportStateCreateInfo viewport_state {};
|
||||
viewport_state.pNext = &depth_clip_control;
|
||||
|
||||
vk::CullModeFlags cull_mode = vk::CullModeFlagBits::eNone;
|
||||
if (static_params.cull_back) {
|
||||
cull_mode |= vk::CullModeFlagBits::eBack;
|
||||
}
|
||||
if (static_params.cull_front) {
|
||||
cull_mode |= vk::CullModeFlagBits::eFront;
|
||||
}
|
||||
|
||||
vk::FrontFace front_face =
|
||||
(static_params.face ? vk::FrontFace::eClockwise : vk::FrontFace::eCounterClockwise);
|
||||
|
||||
vk::PipelineRasterizationDepthClipStateCreateInfoEXT clip_ext {};
|
||||
clip_ext.depthClipEnable = static_params.depth_clip_enable ? VK_TRUE : VK_FALSE;
|
||||
|
||||
@@ -410,8 +239,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
provoking_vertex.pNext = rasterizer.pNext;
|
||||
rasterizer.pNext = &provoking_vertex;
|
||||
}
|
||||
rasterizer.cullMode = cull_mode;
|
||||
rasterizer.frontFace = front_face;
|
||||
rasterizer.polygonMode = static_params.polygon_mode;
|
||||
rasterizer.lineWidth = 1.0f;
|
||||
|
||||
@@ -467,16 +294,13 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
|
||||
std::vector<vk::DescriptorSetLayoutBinding> descriptor_bindings;
|
||||
vk::ShaderStageFlags graphics_stages = vk::ShaderStageFlagBits::eFragment;
|
||||
for (const auto& stage: vertex_info) {
|
||||
const auto native_stage = NativeShaderStage(stage.logical_stage);
|
||||
AddLayoutBindings(descriptor_bindings, *stage.stage.program, native_stage);
|
||||
pipeline.uses_image_heap |= stage.stage.program->info.uses_image_heap;
|
||||
graphics_stages |= native_stage;
|
||||
}
|
||||
if (ps_active) {
|
||||
AddLayoutBindings(descriptor_bindings, *ps_input_info->stage.program,
|
||||
vk::ShaderStageFlagBits::eFragment);
|
||||
pipeline.uses_image_heap |= ps_input_info->stage.program->info.uses_image_heap;
|
||||
for (const auto* stage: stages) {
|
||||
if (stage != nullptr) {
|
||||
const auto native_stage = NativeShaderStage(stage->info.stage);
|
||||
AddLayoutBindings(descriptor_bindings, stage->info, native_stage);
|
||||
pipeline.uses_image_heap |= stage->info.info.uses_image_heap;
|
||||
graphics_stages |= native_stage;
|
||||
}
|
||||
}
|
||||
CreateDescriptorLayout(graphics, pipeline, descriptor_bindings);
|
||||
const vk::PushConstantRange push_constants {graphics_stages, 0,
|
||||
@@ -494,7 +318,7 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
if (graphics_debug_dump_enabled()) {
|
||||
LOGF("PipelineTrace: vkCreatePipelineLayout begin VS=%" PRIu64 " PS=%" PRIu64
|
||||
" set_layouts=1 push_constants=%" PRIu32 "\n",
|
||||
vertex_program.id, ps_active ? pixel_program->id : 0, 1u);
|
||||
key.stage_ids[0], key.stage_ids[3], 1u);
|
||||
}
|
||||
auto result = graphics.device.createPipelineLayout(&pipeline_layout_info, nullptr,
|
||||
&pipeline.pipeline_layout);
|
||||
@@ -504,14 +328,14 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
|
||||
|
||||
vk::PipelineDepthStencilStateCreateInfo depth_stencil_info {};
|
||||
|
||||
std::vector<vk::DynamicState> dynamic_states {
|
||||
vk::DynamicState::eViewportWithCount,
|
||||
vk::DynamicState::eScissorWithCount,
|
||||
vk::DynamicState::eLineWidth,
|
||||
vk::DynamicState::eCullMode,
|
||||
vk::DynamicState::eFrontFace,
|
||||
vk::DynamicState::eDepthTestEnable,
|
||||
vk::DynamicState::eDepthWriteEnable,
|
||||
vk::DynamicState::eDepthCompareOp,
|
||||
@@ -551,9 +375,9 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
pipeline_info.pVertexInputState = mesh ? nullptr : &vertex_input_info;
|
||||
pipeline_info.pInputAssemblyState = mesh ? nullptr : &input_assembly;
|
||||
vk::PipelineTessellationStateCreateInfo tessellation_state {};
|
||||
tessellation_state.patchControlPoints =
|
||||
tessellation ? vs_input_info.tess.input_control_points : 3u;
|
||||
pipeline_info.pTessellationState = (rect_list || tessellation) ? &tessellation_state : nullptr;
|
||||
tessellation_state.patchControlPoints = static_params.patch_control_points;
|
||||
pipeline_info.pTessellationState =
|
||||
static_params.patch_control_points != 0 ? &tessellation_state : nullptr;
|
||||
pipeline_info.pViewportState = &viewport_state;
|
||||
pipeline_info.pRasterizationState = &rasterizer;
|
||||
pipeline_info.pMultisampleState = &multisampling;
|
||||
@@ -569,7 +393,7 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
LOGF("PipelineTrace: vkCreateGraphicsPipelines begin VS=%" PRIu64 " PS=%" PRIu64
|
||||
" topology=%" PRIu32 " color_mask=0x%08" PRIx32
|
||||
" depth=%s blend=%s dyn_states=%" PRIu32 "\n",
|
||||
vertex_program.id, ps_active ? pixel_program->id : 0,
|
||||
key.stage_ids[0], key.stage_ids[3],
|
||||
static_cast<uint32_t>(static_params.topology), static_params.color_mask[0],
|
||||
(with_depth ? "true" : "false"), (static_params.blend_enable[0] ? "true" : "false"),
|
||||
dynamic_state.dynamicStateCount);
|
||||
@@ -582,29 +406,18 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline == nullptr);
|
||||
|
||||
if (tess_control_shader_module != nullptr) {
|
||||
graphics.device.destroyShaderModule(tess_control_shader_module, nullptr);
|
||||
}
|
||||
if (tess_eval_shader_module != nullptr) {
|
||||
graphics.device.destroyShaderModule(tess_eval_shader_module, nullptr);
|
||||
}
|
||||
graphics.device.destroyShaderModule(tess_control_shader_module, nullptr);
|
||||
graphics.device.destroyShaderModule(tess_eval_shader_module, nullptr);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
||||
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& pipeline,
|
||||
const ShaderComputeInputInfo& input_info,
|
||||
vk::ShaderModule compute_module, vk::PipelineCache driver_cache) {
|
||||
EXIT_IF(compute_module == nullptr);
|
||||
|
||||
const ShaderModule& module, vk::PipelineCache driver_cache) {
|
||||
vk::PipelineShaderStageCreateInfo comp_shader_stage_info {};
|
||||
vk::PipelineShaderStageRequiredSubgroupSizeCreateInfo comp_subgroup_size {};
|
||||
comp_shader_stage_info.stage = vk::ShaderStageFlagBits::eCompute;
|
||||
comp_shader_stage_info.module = compute_module;
|
||||
comp_shader_stage_info.module = module.module;
|
||||
comp_shader_stage_info.pName = "main";
|
||||
EXIT_IF(!input_info.stage);
|
||||
const auto wave_size = input_info.stage.program->wave_size;
|
||||
const auto wave_size = module.info.wave_size;
|
||||
if (graphics.compute_subgroup_size_control_enabled &&
|
||||
wave_size >= graphics.min_subgroup_size && wave_size <= graphics.max_subgroup_size) {
|
||||
comp_subgroup_size.requiredSubgroupSize = wave_size;
|
||||
@@ -612,9 +425,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
}
|
||||
|
||||
std::vector<vk::DescriptorSetLayoutBinding> descriptor_bindings;
|
||||
AddLayoutBindings(descriptor_bindings, *input_info.stage.program,
|
||||
vk::ShaderStageFlagBits::eCompute);
|
||||
pipeline.uses_image_heap = input_info.stage.program->info.uses_image_heap;
|
||||
AddLayoutBindings(descriptor_bindings, module.info, vk::ShaderStageFlagBits::eCompute);
|
||||
pipeline.uses_image_heap = module.info.info.uses_image_heap;
|
||||
CreateDescriptorLayout(graphics, pipeline, descriptor_bindings);
|
||||
const vk::PushConstantRange push_constants {vk::ShaderStageFlagBits::eCompute, 0,
|
||||
ShaderRecompiler::IR::NativePushConstantSize};
|
||||
@@ -628,18 +440,12 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
|
||||
EXIT_IF(pipeline.pipeline_layout != nullptr);
|
||||
|
||||
LOGF("PipelineTrace: vkCreatePipelineLayout CS begin set_layouts=1 push_constants=%u\n",
|
||||
1u);
|
||||
auto result = graphics.device.createPipelineLayout(&pipeline_layout_info, nullptr,
|
||||
&pipeline.pipeline_layout);
|
||||
LOGF("PipelineTrace: vkCreatePipelineLayout CS done result=%s layout=%p\n",
|
||||
vk::to_string(result).c_str(), static_cast<void*>(pipeline.pipeline_layout));
|
||||
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
|
||||
|
||||
vk::ComputePipelineCreateInfo info {};
|
||||
if (input_info.stage.program->info.uses_gds) {
|
||||
if (module.info.info.uses_gds) {
|
||||
info.flags = vk::PipelineCreateFlagBits::eDispatchBase;
|
||||
}
|
||||
info.stage = comp_shader_stage_info;
|
||||
@@ -648,15 +454,9 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
|
||||
EXIT_IF(pipeline.pipeline != nullptr);
|
||||
|
||||
LOGF("PipelineTrace: vkCreateComputePipelines begin layout=%p\n",
|
||||
static_cast<void*>(pipeline.pipeline_layout));
|
||||
result = graphics.device.createComputePipelines(driver_cache, 1, &info, nullptr,
|
||||
&pipeline.pipeline);
|
||||
LOGF("PipelineTrace: vkCreateComputePipelines done result=%s pipeline=%p\n",
|
||||
vk::to_string(result).c_str(), static_cast<void*>(pipeline.pipeline));
|
||||
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline == nullptr);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
@@ -45,9 +45,10 @@ enum class DrawOffsetSource : uint8_t {
|
||||
|
||||
// Nonzero address: the GPU reads count Vulkan indirect draw records, stride bytes apart.
|
||||
struct DrawIndirectArgs {
|
||||
uint64_t address = 0;
|
||||
uint32_t count = 0;
|
||||
uint32_t stride = 0;
|
||||
uint64_t address = 0;
|
||||
uint32_t count = 0;
|
||||
uint32_t stride = 0;
|
||||
ShaderIndirectOffsets offsets;
|
||||
};
|
||||
|
||||
struct DrawIndexArgs {
|
||||
|
||||
@@ -53,7 +53,7 @@ static bool FillSourcesDisjoint(std::span<const ShaderRecompiler::IR::Descriptor
|
||||
|
||||
bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer) {
|
||||
const auto& program = *input.stage.program;
|
||||
const auto& program = input.stage.module->info;
|
||||
const auto& resources = *input.stage.resources;
|
||||
if (resources.buffers.size() != program.info.buffers.size()) {
|
||||
EXIT("compute runtime buffer count does not match shader metadata\n");
|
||||
@@ -135,7 +135,7 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
|
||||
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& program = input.stage.module->info;
|
||||
const auto& resources = *input.stage.resources;
|
||||
const auto& fill = resources.uniform_fill;
|
||||
auto& cache = command.GetContext().GetTextureCache();
|
||||
@@ -208,7 +208,7 @@ bool RenderExecutor::TryConsumeComputeFill(const ShaderComputeInputInfo& input,
|
||||
if (logged_clears.fetch_add(1, std::memory_order_relaxed) < 32) {
|
||||
LOGF("GraphicsRenderDispatchDirect: compute %s shader=0x%016" PRIx64
|
||||
" addr=0x%016" PRIx64 " size=0x%016" PRIx64 " value=0x%08" PRIx32 "\n",
|
||||
image_clear ? "image clear" : "buffer fill", input.stage.program->shader_hash,
|
||||
image_clear ? "image clear" : "buffer fill", input.stage.module->info.shader_hash,
|
||||
descriptor.Base48(), size, packed_clear);
|
||||
}
|
||||
return true;
|
||||
@@ -216,7 +216,7 @@ bool RenderExecutor::TryConsumeComputeFill(const ShaderComputeInputInfo& input,
|
||||
|
||||
static void BindSharedMemory(RenderContext& context, ShaderComputeInputInfo& input,
|
||||
PreparedBindings& bindings, uint64_t indirect_args = 0) {
|
||||
if (input.stage.program->bindings.descriptor_counts[
|
||||
if (input.stage.module->info.bindings.descriptor_counts[
|
||||
static_cast<size_t>(ShaderRecompiler::IR::DescriptorBindingKind::SharedMemory)] == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -317,7 +317,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
input_info.dispatch_threads_num[2] = thread_group_z;
|
||||
}
|
||||
|
||||
const auto& program = *input_info.stage.program;
|
||||
const auto& program = input_info.stage.module->info;
|
||||
const auto& resources = *input_info.stage.resources;
|
||||
if (resources.specialization_reads.empty() &&
|
||||
(TryConsumeComputeMetaClear(input_info, buffer) ||
|
||||
@@ -504,7 +504,7 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
PrepareBindings(input_info.stage, bindings);
|
||||
PreparedBindings* descriptor_stage = &bindings;
|
||||
FindBuffers(std::span {&descriptor_stage, 1u});
|
||||
const auto& program = *input_info.stage.program;
|
||||
const auto& program = input_info.stage.module->info;
|
||||
if (program.info.uses_dma) {
|
||||
m_context.PrepareBda();
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
RenderContext::RenderContext(GraphicContext& graphics)
|
||||
RenderContext::RenderContext(GraphicContext& graphics, const std::filesystem::path& shader_store)
|
||||
: m_graphics(graphics), m_render_executor(*this), m_command_scheduler(*this, graphics),
|
||||
m_descriptor_heap(graphics, m_command_scheduler.GetMasterSemaphore()),
|
||||
m_pipeline_cache(graphics, &m_image_heap), m_sampler_cache(graphics),
|
||||
m_pipeline_cache(graphics, &m_image_heap, shader_store), m_sampler_cache(graphics),
|
||||
m_buffer_cache(graphics, m_command_scheduler, m_page_manager, m_texture_cache),
|
||||
m_texture_cache(graphics, m_command_scheduler, m_page_manager, m_buffer_cache),
|
||||
m_image_heap(*this, m_page_manager), m_occlusion_queries(graphics, m_command_scheduler) {
|
||||
@@ -147,6 +147,7 @@ void RenderContext::RunGarbageCollector() {
|
||||
m_fault_process_pending = false;
|
||||
m_buffer_cache.ProcessFaultBuffer();
|
||||
}
|
||||
m_image_heap.RunGarbageCollector();
|
||||
m_texture_cache.ProcessDownloadImages();
|
||||
m_texture_cache.RunGarbageCollector();
|
||||
m_buffer_cache.RunGarbageCollector();
|
||||
|
||||
@@ -31,7 +31,8 @@ class GuestGpu;
|
||||
|
||||
class RenderContext {
|
||||
public:
|
||||
explicit RenderContext(GraphicContext& graphics);
|
||||
explicit RenderContext(GraphicContext& graphics,
|
||||
const std::filesystem::path& shader_store = ShaderStorePath());
|
||||
~RenderContext();
|
||||
KYTY_CLASS_NO_COPY(RenderContext);
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ std::pair<int32_t, uint32_t> ResolveDrawOffsets(uint32_t index_offset,
|
||||
}
|
||||
|
||||
EXIT_IF(!vs_input_info.stage);
|
||||
const auto& program = *vs_input_info.stage.program;
|
||||
const auto& program = vs_input_info.stage.module->info;
|
||||
const auto& resources = *vs_input_info.stage.resources;
|
||||
if (index_offset == 0 &&
|
||||
program.info.vertex_offset_sgpr >= static_cast<int32_t>(program.user_data_base)) {
|
||||
@@ -197,7 +197,7 @@ static void LogDrawTargetState(const char* draw_name, const RenderColorInfo& col
|
||||
const auto& dc = ctx.GetDepthControl();
|
||||
const auto& vp = ctx.GetScreenViewport();
|
||||
const auto& vp0 = vp.viewports[0];
|
||||
const auto& ps_resources = ps_input_info.stage.program->info;
|
||||
const auto& ps_resources = ps_input_info.stage.module->info.info;
|
||||
const auto sampled_images = std::count_if(
|
||||
ps_resources.images.begin(), ps_resources.images.end(), [](const auto& image) {
|
||||
return image.resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled;
|
||||
@@ -320,7 +320,7 @@ static void UpdateGraphicsDynamicState(const CommandBuffer& buffer, DynamicState
|
||||
const auto& ctx = buffer.GetRegisters();
|
||||
const auto& vp = ctx.GetScreenViewport();
|
||||
const vk::Extent2D framebuffer_extent {rendering.width, rendering.height};
|
||||
const auto& outputs = vs_input_info.stage.program->info.outputs;
|
||||
const auto& outputs = vs_input_info.stage.module->info.info.outputs;
|
||||
const bool indexed_viewports =
|
||||
std::any_of(outputs.begin(), outputs.end(), [](const auto& output) {
|
||||
return output.kind == ShaderRecompiler::IR::StageOutputKind::ViewportIndex;
|
||||
@@ -403,6 +403,20 @@ static void UpdateGraphicsDynamicState(const CommandBuffer& buffer, DynamicState
|
||||
dynamic_state.SetDepthBias(constant_factor, poly_offset.clamp, slope_factor);
|
||||
}
|
||||
|
||||
// Rect lists are drawn without culling.
|
||||
vk::CullModeFlags cull_mode {};
|
||||
if (!Prospero::IsRectList(buffer.GetUserConfig().GetPrimType())) {
|
||||
if (mode.cull_front) {
|
||||
cull_mode |= vk::CullModeFlagBits::eFront;
|
||||
}
|
||||
if (mode.cull_back) {
|
||||
cull_mode |= vk::CullModeFlagBits::eBack;
|
||||
}
|
||||
}
|
||||
dynamic_state.SetCullMode(cull_mode);
|
||||
dynamic_state.SetFrontFace(mode.face ? vk::FrontFace::eClockwise
|
||||
: vk::FrontFace::eCounterClockwise);
|
||||
|
||||
dynamic_state.SetStencilTestEnabled(depth.stencil_test_enable);
|
||||
if (depth.stencil_test_enable) {
|
||||
const auto& front = depth.stencil_front;
|
||||
@@ -843,6 +857,11 @@ bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCal
|
||||
LogDrawPhase(draw.Name(), "GetGraphicsPrograms");
|
||||
}
|
||||
m_context.GetPipelineCache().GetGraphicsPrograms(buffer, state);
|
||||
// Vertex programs beyond the host mesh limits or with unsupported call targets are not
|
||||
// compiled.
|
||||
if (state.vertex_info[0].stage.module == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (draw.IsIndexed()) {
|
||||
LogDrawPhase(draw.Name(), "ResolveRenderColorTarget");
|
||||
}
|
||||
@@ -1054,7 +1073,7 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
return;
|
||||
}
|
||||
const auto vertex_stages = state.VertexStages();
|
||||
const bool mesh_active = state.vertex_info[0].stage.program->stage == ShaderType::Mesh;
|
||||
const bool mesh_active = state.vertex_info[0].stage.module->info.stage == ShaderType::Mesh;
|
||||
uint32_t mesh_groups = 0;
|
||||
if (mesh_active) {
|
||||
const auto& mesh = state.vertex_info[0].mesh;
|
||||
@@ -1156,7 +1175,6 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
if (mesh_active) {
|
||||
draw_data = {draw.index_count,
|
||||
static_cast<uint32_t>(emit.vertex_offset),
|
||||
emit.first_instance,
|
||||
index_source.guest_element_size,
|
||||
static_cast<uint32_t>(index_source.address),
|
||||
static_cast<uint32_t>(index_source.address >> 32u)};
|
||||
@@ -1232,8 +1250,9 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
}
|
||||
}
|
||||
|
||||
// Vulkan offsets for a prepared draw, or nullopt when its indirect records need CPU counts, as
|
||||
// mesh and quad-list draws do. Indirect records carry their own offsets.
|
||||
// Vulkan offsets for a prepared draw, or nullopt when its indirect records need the CPU: mesh
|
||||
// and quad-list draws for counts, tessellation draws for patched SGPRs. Indirect records carry
|
||||
// their own offsets.
|
||||
static std::optional<DrawEmitInfo> ResolveDrawEmit(const HW::UserConfig& ucfg,
|
||||
const ShaderVertexInputInfo& vs,
|
||||
DrawOffsetSource source,
|
||||
@@ -1241,8 +1260,9 @@ static std::optional<DrawEmitInfo> ResolveDrawEmit(const HW::UserConfig&
|
||||
uint32_t first_instance,
|
||||
const DrawIndirectArgs& indirect) {
|
||||
if (indirect.address != 0) {
|
||||
if (vs.stage.program->stage == ShaderType::Mesh ||
|
||||
ucfg.GetPrimType() == Prospero::PrimitiveType::kQuadListLegacy) {
|
||||
if (vs.stage.module->info.stage == ShaderType::Mesh ||
|
||||
ucfg.GetPrimType() == Prospero::PrimitiveType::kQuadListLegacy ||
|
||||
vs.indirect_offsets != indirect.offsets) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return DrawEmitInfo {.indirect = indirect};
|
||||
@@ -1306,7 +1326,7 @@ bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
|
||||
|
||||
hw_check(buffer);
|
||||
|
||||
DrawRenderState state {};
|
||||
DrawRenderState state {.indirect_offsets = args.indirect.offsets};
|
||||
if (!GetDrawTopology(ucfg, state.topology)) {
|
||||
return true;
|
||||
}
|
||||
@@ -1411,7 +1431,7 @@ bool RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
|
||||
const DrawCallInfo draw {CommandBufferDebugOp::DrawIndexAuto,
|
||||
args.vertex_count, args.instance_count, args.first_instance};
|
||||
|
||||
DrawRenderState state {};
|
||||
DrawRenderState state {.indirect_offsets = args.indirect.offsets};
|
||||
if (!GetDrawTopology(ucfg, state.topology)) {
|
||||
ResetBindings();
|
||||
return true;
|
||||
@@ -1423,7 +1443,7 @@ bool RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
|
||||
|
||||
const bool rect_list = Prospero::IsRectList(ucfg.GetPrimType());
|
||||
if (rect_list && state.vertex_info[0].buffers_num == 0 &&
|
||||
state.vertex_info[0].stage.program->param_export_mask == 0 &&
|
||||
state.vertex_info[0].stage.module->info.param_export_mask == 0 &&
|
||||
state.ps_input_info.input_num != 0) {
|
||||
if (graphics_debug_dump_enabled()) {
|
||||
LOGF("DrawIndexAuto: skipping rect-list draw with no VS param exports and PS inputs: "
|
||||
@@ -1475,8 +1495,9 @@ bool RenderExecutor::ResolveColorTargets(CommandBuffer& buffer, uint32_t render_
|
||||
cache.MarkGpuWritten(dst.image_id);
|
||||
auto& source = cache.GetImage(src.image_id);
|
||||
auto& destination = cache.GetImage(dst.image_id);
|
||||
destination.Resolve(source, {src.guest_mip_level, 1, src.guest_array_layer, 1},
|
||||
{dst.guest_mip_level, 1, dst.guest_array_layer, 1});
|
||||
destination.Resolve(source,
|
||||
{src.desc.view_info.base_level, 1, src.desc.view_info.base_layer, 1},
|
||||
{dst.desc.view_info.base_level, 1, dst.desc.view_info.base_layer, 1});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#version 450
|
||||
#extension GL_EXT_shader_16bit_storage : require
|
||||
|
||||
layout(constant_id = 0) const uint D32 = 0u;
|
||||
layout(local_size_x = 64) in;
|
||||
|
||||
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } input_buffer;
|
||||
layout(set = 0, binding = 1, std430) buffer Output { uint data[]; } output_buffer;
|
||||
layout(set = 0, binding = 1, std430) buffer Output { uint16_t data[]; } output_buffer;
|
||||
|
||||
layout(push_constant, std430) uniform Conversion {
|
||||
uint src_base;
|
||||
@@ -32,9 +33,5 @@ void main() {
|
||||
}
|
||||
uint src = params.src_base + y * params.source_pitch + x * 4u;
|
||||
uint dst = params.dst_base + y * params.target_pitch + x * 2u;
|
||||
uint shift = (dst & 2u) * 8u;
|
||||
uint mask = 0xffffu << shift;
|
||||
uint word = dst >> 2u;
|
||||
atomicAnd(output_buffer.data[word], ~mask);
|
||||
atomicOr(output_buffer.data[word], decode(input_buffer.data[src >> 2u]) << shift);
|
||||
output_buffer.data[dst >> 1u] = uint16_t(decode(input_buffer.data[src >> 2u]));
|
||||
}
|
||||
@@ -533,16 +533,16 @@ void DrawMemoryAndIo(const Snapshot& shown) {
|
||||
Bytes(guest.pooled_allocated) + " (" + Bytes(guest.pool_committed) + " committed)");
|
||||
Row("Flexible", Bytes(guest.flexible_used) + " / " + Bytes(guest.flexible_total));
|
||||
Row("Extended", Bytes(guest.automatic_allocated));
|
||||
// Past the pool size the guest sees no page-table entries available.
|
||||
const auto entries = std::max(guest.cpu_page_entries, guest.gpu_page_entries);
|
||||
// Past its total a side sees no page-table entries available.
|
||||
const auto percent = std::max(guest.cpu_page_entries * 100 / guest.cpu_page_entries_total,
|
||||
guest.gpu_page_entries * 100 / guest.gpu_page_entries_total);
|
||||
Row("Page table (2 MB)",
|
||||
Fmt("CPU %llu GPU %llu / %llu",
|
||||
Fmt("CPU %llu / %llu GPU %llu / %llu",
|
||||
static_cast<unsigned long long>(guest.cpu_page_entries),
|
||||
static_cast<unsigned long long>(guest.cpu_page_entries_total),
|
||||
static_cast<unsigned long long>(guest.gpu_page_entries),
|
||||
static_cast<unsigned long long>(guest.page_entries_total)),
|
||||
entries > guest.page_entries_total ? BAD
|
||||
: entries * 10 > guest.page_entries_total * 9 ? MEDIUM
|
||||
: GOOD);
|
||||
static_cast<unsigned long long>(guest.gpu_page_entries_total)),
|
||||
LevelColor(static_cast<double>(percent), 90, 100));
|
||||
ImGui::EndTable();
|
||||
}
|
||||
if (BeginSection("Emulator process")) {
|
||||
|
||||
@@ -51,6 +51,7 @@ struct VulkanExtensions {
|
||||
vk::PhysicalDeviceVulkan11Features WindowContext::RequiredVulkan11Features() noexcept {
|
||||
vk::PhysicalDeviceVulkan11Features features {};
|
||||
features.storageBuffer16BitAccess = VK_TRUE;
|
||||
features.shaderDrawParameters = VK_TRUE;
|
||||
return features;
|
||||
}
|
||||
|
||||
@@ -278,6 +279,8 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
||||
required_features12.samplerMirrorClampToEdge);
|
||||
check_feature(features11.storageBuffer16BitAccess, "storageBuffer16BitAccess",
|
||||
WindowContext::RequiredVulkan11Features().storageBuffer16BitAccess);
|
||||
check_feature(features11.shaderDrawParameters, "shaderDrawParameters",
|
||||
WindowContext::RequiredVulkan11Features().shaderDrawParameters);
|
||||
check_feature(features12.storageBuffer8BitAccess, "storageBuffer8BitAccess",
|
||||
required_features12.storageBuffer8BitAccess);
|
||||
check_feature(features12.timelineSemaphore, "timelineSemaphore",
|
||||
@@ -337,6 +340,7 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
||||
|
||||
check_feature(device_features2.features.shaderImageGatherExtended,
|
||||
"shaderImageGatherExtended");
|
||||
check_feature(device_features2.features.shaderResourceMinLod, "shaderResourceMinLod");
|
||||
|
||||
check_feature(device_features2.features.independentBlend, "independentBlend");
|
||||
check_feature(device_features2.features.tessellationShader, "tessellationShader");
|
||||
@@ -636,6 +640,7 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
||||
device_features.shaderStorageImageMultisample =
|
||||
supported_features2.features.shaderStorageImageMultisample;
|
||||
device_features.shaderImageGatherExtended = VK_TRUE;
|
||||
device_features.shaderResourceMinLod = VK_TRUE;
|
||||
device_features.independentBlend = VK_TRUE;
|
||||
device_features.dualSrcBlend = VK_TRUE;
|
||||
device_features.tessellationShader = VK_TRUE;
|
||||
@@ -733,6 +738,15 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
||||
conditional_rendering.pNext = const_cast<void*>(create_info.pNext);
|
||||
create_info.pNext = &conditional_rendering;
|
||||
}
|
||||
// The renderer only uses the Vulkan 1.3 core dynamic states, but RADV (Mesa 26.3) creates its
|
||||
// PS epilog cache only when this EXT feature is enabled and crashes in vkCmdDraw when a
|
||||
// depth- or stencil-writing fragment shader needs an epilog.
|
||||
vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT extended_dynamic_state {};
|
||||
extended_dynamic_state.extendedDynamicState = VK_TRUE;
|
||||
if (HasExtension(device_extensions, VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME)) {
|
||||
extended_dynamic_state.pNext = const_cast<void*>(create_info.pNext);
|
||||
create_info.pNext = &extended_dynamic_state;
|
||||
}
|
||||
create_info.pQueueCreateInfos = &queue_create_info;
|
||||
create_info.queueCreateInfoCount = 1;
|
||||
create_info.enabledExtensionCount = static_cast<uint32_t>(device_extensions.size());
|
||||
@@ -1111,7 +1125,8 @@ void WindowContext::CreateVulkan() {
|
||||
VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME,
|
||||
VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME,
|
||||
VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME,
|
||||
VK_EXT_CONDITIONAL_RENDERING_EXTENSION_NAME}) {
|
||||
VK_EXT_CONDITIONAL_RENDERING_EXTENSION_NAME,
|
||||
VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME}) {
|
||||
if (HasExtension(available_extensions, extension)) {
|
||||
device_extensions.push_back(extension);
|
||||
}
|
||||
|
||||
@@ -30,10 +30,6 @@ namespace Libs::Graphics::ShaderRecompiler {
|
||||
|
||||
namespace {
|
||||
|
||||
const char* GetDumpLabel(const CompileOptions& options) {
|
||||
return options.dump_label != nullptr ? options.dump_label : "ShaderRecompiler";
|
||||
}
|
||||
|
||||
std::string MakeIrDump(std::string_view cfg, const IR::Program& ir) {
|
||||
std::string dump = "CFG:\n";
|
||||
dump += cfg;
|
||||
@@ -44,30 +40,17 @@ std::string MakeIrDump(std::string_view cfg, const IR::Program& ir) {
|
||||
return dump;
|
||||
}
|
||||
|
||||
const char* StageName(ShaderType stage) {
|
||||
switch (stage) {
|
||||
case ShaderType::Compute: return "CS";
|
||||
case ShaderType::Vertex: return "VS";
|
||||
case ShaderType::Local: return "LS";
|
||||
case ShaderType::TessellationControl: return "HS";
|
||||
case ShaderType::TessellationEvaluation: return "TES";
|
||||
case ShaderType::Mesh: return "MS";
|
||||
case ShaderType::Pixel: return "PS";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
void LogDispatcherFallback(const CompileOptions& options, const CFG::Graph& cfg, const char* phase) {
|
||||
const auto* block = cfg.FindBlock(cfg.failure_block);
|
||||
const auto start = block != nullptr ? block->start_pc : UINT32_MAX;
|
||||
const auto end = block != nullptr ? block->end_pc : UINT32_MAX;
|
||||
const auto predecessors = block != nullptr ? block->predecessors.size() : 0u;
|
||||
const auto successors = block != nullptr ? block->successors.size() : 0u;
|
||||
LOGF("%s CFG dispatcher fallback: stage=%s hash=0x%016" PRIx64
|
||||
LOGF("ShaderRecompiler %s CFG dispatcher fallback: hash=0x%016" PRIx64
|
||||
" phase=%s failure=%s block=%" PRIu32 " pc=0x%08" PRIx32 "..0x%08" PRIx32 " preds=%" PRIu64
|
||||
" succs=%" PRIu64 " blocks=%" PRIu64 " loops=%" PRIu64 " back_edges=%" PRIu64
|
||||
" reason=%s\n",
|
||||
GetDumpLabel(options), StageName(options.stage), options.shader_hash, phase,
|
||||
StageName(options.stage), options.shader_hash, phase,
|
||||
CFG::FailureKindToString(cfg.failure_kind).c_str(), cfg.failure_block, start, end,
|
||||
static_cast<uint64_t>(predecessors), static_cast<uint64_t>(successors),
|
||||
static_cast<uint64_t>(cfg.blocks.size()), static_cast<uint64_t>(cfg.natural_loops.size()),
|
||||
@@ -484,6 +467,7 @@ Decoder::Program DecodeFusedProgram(std::span<const uint32_t> front, std::span<c
|
||||
void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
|
||||
using namespace IR;
|
||||
EXIT_NOT_IMPLEMENTED(options.stage != ShaderType::Compute);
|
||||
source.wave_size = options.wave_size;
|
||||
for (uint32_t i = 0; i < source.decoded.instructions.size(); ++i) {
|
||||
if (source.decoded.instructions[i].opcode != Decoder::Opcode::S_SWAPPC_B64) continue;
|
||||
source.calls.push_back({.instruction = i});
|
||||
@@ -520,7 +504,8 @@ void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
|
||||
};
|
||||
Register = [&](uint32_t before, uint32_t code) -> Value {
|
||||
std::vector<uint32_t> crossed_calls;
|
||||
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code, crossed_calls);
|
||||
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code,
|
||||
source.wave_size, crossed_calls);
|
||||
for (const auto crossed: crossed_calls) {
|
||||
std::ranges::find(source.calls, crossed, &ShaderSource::Call::instruction)->preserved_registers.set(code);
|
||||
}
|
||||
@@ -595,6 +580,19 @@ void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* StageName(ShaderType stage) {
|
||||
switch (stage) {
|
||||
case ShaderType::Compute: return "CS";
|
||||
case ShaderType::Vertex: return "VS";
|
||||
case ShaderType::Local: return "LS";
|
||||
case ShaderType::TessellationControl: return "HS";
|
||||
case ShaderType::TessellationEvaluation: return "TES";
|
||||
case ShaderType::Mesh: return "MS";
|
||||
case ShaderType::Pixel: return "PS";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOptions& options) {
|
||||
EXIT_IF(code.empty());
|
||||
ShaderSource source;
|
||||
@@ -706,10 +704,11 @@ const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtR
|
||||
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));
|
||||
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg, source.wave_size) ||
|
||||
CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u, source.wave_size));
|
||||
for (uint32_t code = 0; code < site.preserved_registers.size(); ++code) {
|
||||
if (site.preserved_registers[code] && CFG::MayWriteScalarRegister(inst, code))
|
||||
if (site.preserved_registers[code] &&
|
||||
CFG::MayWriteScalarRegister(inst, code, source.wave_size))
|
||||
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);
|
||||
@@ -764,6 +763,7 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
|
||||
static_cast<unsigned>(options.stage));
|
||||
}
|
||||
|
||||
const auto* stage = StageName(options.stage);
|
||||
const auto compile_begin = std::chrono::steady_clock::now();
|
||||
const auto phase_ms = [&compile_begin]() {
|
||||
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
@@ -773,27 +773,25 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
|
||||
std::string decoded_dump;
|
||||
if (options.dump_ir) {
|
||||
decoded_dump = Decoder::ProgramToString(decoded);
|
||||
if (options.early_dump) {
|
||||
LOGF("%s decoded RDNA2 (early):\n%s", GetDumpLabel(options), decoded_dump.c_str());
|
||||
}
|
||||
LOGF("ShaderRecompiler %s decoded RDNA2:\n%s", stage, decoded_dump.c_str());
|
||||
}
|
||||
|
||||
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " CFG BuildGraph\n", GetDumpLabel(options),
|
||||
StageName(options.stage), options.shader_hash);
|
||||
LOGF("ShaderRecompiler %s phase begin: hash=0x%016" PRIx64 " CFG BuildGraph\n", stage,
|
||||
options.shader_hash);
|
||||
auto native_cfg = CFG::BuildGraph(decoded);
|
||||
CFG::Graph structured_cfg;
|
||||
auto* selected_cfg = &native_cfg;
|
||||
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " CFG BuildGraph blocks=%" PRIu64
|
||||
LOGF("ShaderRecompiler %s phase end: hash=0x%016" PRIx64 " CFG BuildGraph blocks=%" PRIu64
|
||||
" loops=%" PRIu64 " back_edges=%" PRIu64 " elapsed_ms=%" PRIu64 "\n",
|
||||
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
|
||||
stage, options.shader_hash,
|
||||
static_cast<uint64_t>(native_cfg.blocks.size()),
|
||||
static_cast<uint64_t>(native_cfg.natural_loops.size()),
|
||||
static_cast<uint64_t>(native_cfg.back_edges.size()), phase_ms());
|
||||
if (native_cfg.irreducible) {
|
||||
LogDispatcherFallback(options, native_cfg, "build");
|
||||
} else {
|
||||
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " CFG Structurize\n",
|
||||
GetDumpLabel(options), StageName(options.stage), options.shader_hash);
|
||||
LOGF("ShaderRecompiler %s phase begin: hash=0x%016" PRIx64 " CFG Structurize\n", stage,
|
||||
options.shader_hash);
|
||||
structured_cfg = CFG::Structurize(native_cfg);
|
||||
if (structured_cfg.unsupported) {
|
||||
native_cfg.unsupported = true;
|
||||
@@ -803,12 +801,12 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
|
||||
LogDispatcherFallback(options, native_cfg, "structurize");
|
||||
} else {
|
||||
selected_cfg = &structured_cfg;
|
||||
LOGF("%s structured CFG success: blocks=%" PRIu64 "\n", GetDumpLabel(options),
|
||||
LOGF("ShaderRecompiler %s structured CFG success: blocks=%" PRIu64 "\n", stage,
|
||||
static_cast<uint64_t>(selected_cfg->blocks.size()));
|
||||
}
|
||||
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " CFG Structurize blocks=%" PRIu64
|
||||
LOGF("ShaderRecompiler %s phase end: hash=0x%016" PRIx64 " CFG Structurize blocks=%" PRIu64
|
||||
" loops=%" PRIu64 " elapsed_ms=%" PRIu64 "\n",
|
||||
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
|
||||
stage, options.shader_hash,
|
||||
static_cast<uint64_t>(selected_cfg->blocks.size()),
|
||||
static_cast<uint64_t>(selected_cfg->natural_loops.size()), phase_ms());
|
||||
}
|
||||
@@ -821,7 +819,7 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
|
||||
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),
|
||||
LOGF("ShaderRecompiler %s embedded vertex fetch plan: detected=%" PRIu64 "\n", stage,
|
||||
static_cast<uint64_t>(embedded_fetch.loads.size()));
|
||||
}
|
||||
}
|
||||
@@ -834,12 +832,12 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
|
||||
.input_info = options.input_info,
|
||||
.embedded_fetch = embedded_fetch.loads.empty() ? nullptr : &embedded_fetch,
|
||||
};
|
||||
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " IR TranslateProgram\n",
|
||||
GetDumpLabel(options), StageName(options.stage), options.shader_hash);
|
||||
LOGF("ShaderRecompiler %s phase begin: hash=0x%016" PRIx64 " IR TranslateProgram\n", stage,
|
||||
options.shader_hash);
|
||||
auto ir = Frontend::TranslateProgram(decoded, cfg, translate_options);
|
||||
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " IR TranslateProgram blocks=%" PRIu64
|
||||
LOGF("ShaderRecompiler %s phase end: hash=0x%016" PRIx64 " IR TranslateProgram blocks=%" PRIu64
|
||||
" elapsed_ms=%" PRIu64 "\n",
|
||||
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
|
||||
stage, options.shader_hash,
|
||||
static_cast<uint64_t>(ir.blocks.size()), phase_ms());
|
||||
IR::RewriteToSsa(ir.blocks);
|
||||
const auto simplify = [&ir] {
|
||||
@@ -850,8 +848,9 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
|
||||
simplify();
|
||||
const auto read_lane_stats = IR::EliminateReadLane(ir, ir.wave_size);
|
||||
if (read_lane_stats.rewritten_reads != 0 || read_lane_stats.rewritten_reductions != 0) {
|
||||
LOGF("%s read-lane elimination: reads=%" PRIu32 " reductions=%" PRIu32 "\n",
|
||||
GetDumpLabel(options), read_lane_stats.rewritten_reads, read_lane_stats.rewritten_reductions);
|
||||
LOGF("ShaderRecompiler %s read-lane elimination: reads=%" PRIu32
|
||||
" reductions=%" PRIu32 "\n",
|
||||
stage, read_lane_stats.rewritten_reads, read_lane_stats.rewritten_reductions);
|
||||
}
|
||||
// Cleanup also exposes profitable folds by removing dead predicate consumers.
|
||||
simplify();
|
||||
@@ -865,10 +864,8 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
|
||||
std::string cfg_dump;
|
||||
if (options.dump_ir) {
|
||||
cfg_dump = CFG::GraphToString(cfg);
|
||||
if (options.early_dump) {
|
||||
LOGF("%s native IR before resource tracking:\n%s", GetDumpLabel(options),
|
||||
MakeIrDump(cfg_dump, ir).c_str());
|
||||
}
|
||||
LOGF("ShaderRecompiler %s native IR before resource tracking:\n%s", stage,
|
||||
MakeIrDump(cfg_dump, ir).c_str());
|
||||
}
|
||||
IR::TrackResources(ir, decoded, native_cfg, options.input_info);
|
||||
TranslateResult result;
|
||||
@@ -885,6 +882,7 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o
|
||||
uint32_t push_data_start_dword) {
|
||||
const auto emit_begin = std::chrono::steady_clock::now();
|
||||
auto& ir = translated.program;
|
||||
const auto* stage = StageName(ir.stage);
|
||||
IR::ApplyResourceSpecialization(ir, specialization);
|
||||
// The resource plan owns host descriptor evaluation now. Keep only dependencies consumed
|
||||
// by GPU memory operations; bound descriptor dwords must not retain shader instructions.
|
||||
@@ -931,19 +929,16 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o
|
||||
std::string ir_dump;
|
||||
if (options.dump_ir) {
|
||||
ir_dump = MakeIrDump(translated.cfg_dump, ir);
|
||||
if (options.early_dump) {
|
||||
LOGF("%s native IR (early):\n%s", GetDumpLabel(options), ir_dump.c_str());
|
||||
}
|
||||
LOGF("ShaderRecompiler %s native IR:\n%s", stage, ir_dump.c_str());
|
||||
}
|
||||
|
||||
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram\n",
|
||||
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash);
|
||||
LOGF("ShaderRecompiler %s phase begin: hash=0x%016" PRIx64 " SPIR-V EmitProgram\n", stage,
|
||||
ir.shader_hash);
|
||||
auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword, *options.profile,
|
||||
options.emit_debug_info);
|
||||
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram words=%" PRIu64
|
||||
" elapsed_ms=%" PRIu64 "\n",
|
||||
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash,
|
||||
static_cast<uint64_t>(spirv.size()),
|
||||
LOGF("ShaderRecompiler %s phase end: hash=0x%016" PRIx64 " SPIR-V EmitProgram words=%" PRIu64
|
||||
" wave_size=%u elapsed_ms=%" PRIu64 "\n",
|
||||
stage, ir.shader_hash, static_cast<uint64_t>(spirv.size()), ir.wave_size,
|
||||
static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - emit_begin)
|
||||
.count()));
|
||||
|
||||
@@ -22,10 +22,8 @@ struct CompileOptions {
|
||||
uint32_t user_data_base = 0;
|
||||
uint64_t shader_hash = 0;
|
||||
bool dump_ir = true;
|
||||
bool early_dump = false;
|
||||
bool emit_debug_info = false;
|
||||
const Spirv::Profile* profile = &Spirv::DefaultProfile;
|
||||
const char* dump_label = nullptr;
|
||||
std::span<const uint32_t> user_data;
|
||||
std::span<const uint32_t> back_code;
|
||||
ShaderStageInputInfo input_info;
|
||||
@@ -64,11 +62,13 @@ struct ShaderSource {
|
||||
IR::ResourcePlan call_targets;
|
||||
std::vector<Call> calls;
|
||||
uint32_t unsupported_call_pc = UINT32_MAX;
|
||||
uint32_t wave_size = 64;
|
||||
std::optional<Linked> linked;
|
||||
uint64_t revision = 0;
|
||||
std::vector<std::pair<uint64_t, uint64_t>> reads;
|
||||
};
|
||||
|
||||
[[nodiscard]] const char* StageName(ShaderType stage);
|
||||
[[nodiscard]] ShaderSource PrepareShaderSource(std::span<const uint32_t> code,
|
||||
const CompileOptions& options);
|
||||
const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtRuntime& runtime);
|
||||
|
||||
@@ -54,9 +54,30 @@ uint32_t EmitExt(EmitterState& state, uint32_t type, uint32_t opcode,
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t EmitF32ToF16RtzBits(EmitterState& state, uint32_t value) {
|
||||
uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
|
||||
// Clamping first keeps the truncating conversion defined; NaN and values past the largest
|
||||
// float below the integer limit are selected afterwards.
|
||||
const auto clamped =
|
||||
EmitExt(state, TypeF32(state), GLSLstd450NClamp,
|
||||
{src, ConstantF32(state, signed_value ? 0xcf000000u : 0u),
|
||||
ConstantF32(state, signed_value ? 0x4effffffu : 0x4f7fffffu)});
|
||||
const auto converted = signed_value
|
||||
? Unary(state, spv::OpBitcast, TypeU32(state),
|
||||
Unary(state, spv::OpConvertFToS, TypeI32(state), clamped))
|
||||
: Unary(state, spv::OpConvertFToU, TypeU32(state), clamped);
|
||||
const auto above = Binary(state, spv::OpFOrdGreaterThanEqual, TypeBool(state), src,
|
||||
ConstantF32(state, signed_value ? 0x4f000000u : 0x4f800000u));
|
||||
const auto high =
|
||||
Select(state, TypeU32(state), above,
|
||||
ConstantU32(state, signed_value ? 0x7fffffffu : 0xffffffffu), converted);
|
||||
return Select(state, TypeU32(state), Unary(state, spv::OpIsNan, TypeBool(state), src),
|
||||
ConstantU32(state, 0), high);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t EmitF32BitsToF16RtzBits(EmitterState& state, uint32_t bits) {
|
||||
// Native half conversion causes regressions; use compact integer RTZ conversion.
|
||||
const auto bits = EmitBitcastF32ToU32(state, value);
|
||||
const auto abs = EmitAndConstant(state, bits, 0x7fffffffu);
|
||||
const auto sign = EmitAndConstant(
|
||||
state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), bits, ConstantU32(state, 16)),
|
||||
@@ -92,28 +113,6 @@ uint32_t EmitF32ToF16RtzBits(EmitterState& state, uint32_t value) {
|
||||
return EmitOrU32(state, sign, magnitude);
|
||||
}
|
||||
|
||||
uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
|
||||
// Clamping first keeps the truncating conversion defined; NaN and values past the largest
|
||||
// float below the integer limit are selected afterwards.
|
||||
const auto clamped =
|
||||
EmitExt(state, TypeF32(state), GLSLstd450NClamp,
|
||||
{src, ConstantF32(state, signed_value ? 0xcf000000u : 0u),
|
||||
ConstantF32(state, signed_value ? 0x4effffffu : 0x4f7fffffu)});
|
||||
const auto converted = signed_value
|
||||
? Unary(state, spv::OpBitcast, TypeU32(state),
|
||||
Unary(state, spv::OpConvertFToS, TypeI32(state), clamped))
|
||||
: Unary(state, spv::OpConvertFToU, TypeU32(state), clamped);
|
||||
const auto above = Binary(state, spv::OpFOrdGreaterThanEqual, TypeBool(state), src,
|
||||
ConstantF32(state, signed_value ? 0x4f000000u : 0x4f800000u));
|
||||
const auto high =
|
||||
Select(state, TypeU32(state), above,
|
||||
ConstantU32(state, signed_value ? 0x7fffffffu : 0xffffffffu), converted);
|
||||
return Select(state, TypeU32(state), Unary(state, spv::OpIsNan, TypeBool(state), src),
|
||||
ConstantU32(state, 0), high);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t EmitFPFma32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
|
||||
const auto result = EmitGlsl<GLSLstd450Fma, IR::Type::F32>(state, a, b, c);
|
||||
state.builder.AddAnnotation(spv::OpDecorate, result, spv::DecorationNoContraction);
|
||||
@@ -199,7 +198,8 @@ uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value
|
||||
|
||||
uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return EmitNative<spv::OpBitFieldInsert, IR::Type::U32>(
|
||||
state, EmitF32ToF16RtzBits(state, arg0), EmitF32ToF16RtzBits(state, arg1),
|
||||
state, EmitF32BitsToF16RtzBits(state, EmitBitcastF32ToU32(state, arg0)),
|
||||
EmitF32BitsToF16RtzBits(state, EmitBitcastF32ToU32(state, arg1)),
|
||||
ConstantU32(state, 16), ConstantU32(state, 16));
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,13 @@ bool PixelParameterIsCustom(const EmitterState& state, uint32_t attr) {
|
||||
ShaderPixelParameterIsCustom(*state.input_info.pixel, attr);
|
||||
}
|
||||
|
||||
uint32_t MrtOutputType(EmitterState& state, uint32_t index) {
|
||||
const auto& pixel = *state.input_info.pixel;
|
||||
if ((pixel.target_sint_mask & (1u << index)) != 0u) return TypeI32Vector(state, 4);
|
||||
if ((pixel.target_uint_mask & (1u << index)) != 0u) return TypeU32Vector(state, 4);
|
||||
return TypeF32Vector(state, 4);
|
||||
}
|
||||
|
||||
uint32_t OutputVariableForExport(const EmitterState& state, const IR::ExportInfo& exp) {
|
||||
if (exp.kind == IR::ExportTargetKind::Position && exp.index == 0) {
|
||||
return state.per_vertex_variable;
|
||||
|
||||
@@ -63,7 +63,8 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
|
||||
}
|
||||
if (kind == IR::StageInputKind::VertexIndex || kind == IR::StageInputKind::InstanceIndex ||
|
||||
kind == IR::StageInputKind::InvocationId || kind == IR::StageInputKind::PrimitiveId ||
|
||||
kind == IR::StageInputKind::Layer || kind == IR::StageInputKind::SampleId) {
|
||||
kind == IR::StageInputKind::Layer || kind == IR::StageInputKind::SampleId ||
|
||||
kind == IR::StageInputKind::BaseVertex || kind == IR::StageInputKind::BaseInstance) {
|
||||
const auto value = state.builder.AllocateId();
|
||||
const auto bits = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpLoad, TypeI32(state), value, variable);
|
||||
@@ -114,6 +115,18 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
|
||||
return EmitInputComponentU32(state, kind, component);
|
||||
}
|
||||
|
||||
// Whether EXEC enables a source lane within the caller's 32-lane half.
|
||||
uint32_t EmitSourceLaneActive(ValueEmitContext& ctx, uint32_t exec, uint32_t lane) {
|
||||
auto& state = ctx.state;
|
||||
const auto scope = ConstantU32(state, spv::ScopeSubgroup);
|
||||
const auto ballot = EmitNative<spv::OpGroupNonUniformBallot, IR::Type::U32x4>(state, scope, exec);
|
||||
if (ctx.half != 0) {
|
||||
lane = EmitAndConstant(state, lane, 31);
|
||||
}
|
||||
return EmitNative<spv::OpGroupNonUniformBallotBitExtract, IR::Type::U1>(state, scope, ballot,
|
||||
lane);
|
||||
}
|
||||
|
||||
uint32_t EmitDppWriteCondition(ValueEmitContext& ctx, const IR::DppMoveFlags& flags,
|
||||
uint32_t exec) {
|
||||
auto& state = ctx.state;
|
||||
@@ -135,9 +148,24 @@ uint32_t EmitDppWriteCondition(ValueEmitContext& ctx, const IR::DppMoveFlags& fl
|
||||
// Only row shifts (0x101-0x10f, 0x111-0x11f) can vacate a lane; other controls always read one.
|
||||
const bool row_shift = flags.control > 0x100u && flags.control < 0x120u &&
|
||||
flags.control != 0x110u;
|
||||
if (!flags.bound_control && row_shift) {
|
||||
if (flags.bound_control || (flags.fetch_inactive && !row_shift)) {
|
||||
return writable;
|
||||
}
|
||||
// Without BOUND_CTRL, a vacated source or, without FI, an EXEC-disabled one disables the write.
|
||||
const auto target = EmitDppTargetLane(state, flags);
|
||||
if (row_shift) {
|
||||
writable = Binary(state, spv::OpLogicalAnd, TypeBool(state), writable, target.valid);
|
||||
}
|
||||
if (!flags.fetch_inactive) {
|
||||
// Vacated lanes target outside their row; wrapping keeps the ballot index in range.
|
||||
auto source = target.lane;
|
||||
if (row_shift) {
|
||||
lane = lane != 0 ? lane : EmitSubgroupLocalInvocationId(state);
|
||||
source = EmitNative<spv::OpBitFieldInsert, IR::Type::U32>(
|
||||
state, lane, source, ConstantU32(state, 0), ConstantU32(state, 4));
|
||||
}
|
||||
writable = Binary(state, spv::OpLogicalAnd, TypeBool(state), writable,
|
||||
EmitDppTargetLane(state, flags).valid);
|
||||
EmitSourceLaneActive(ctx, exec, source));
|
||||
}
|
||||
return writable;
|
||||
}
|
||||
@@ -264,9 +292,10 @@ uint32_t ExportRawComponent(ValueEmitContext& ctx, uint32_t vector, uint32_t com
|
||||
}
|
||||
|
||||
uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo& exp,
|
||||
bool uint_output) {
|
||||
auto& state = ctx.state;
|
||||
if (exp.compr && !uint_output) {
|
||||
uint32_t vector_type) {
|
||||
auto& state = ctx.state;
|
||||
const bool integer_output = vector_type != TypeF32Vector(state, 4);
|
||||
if (exp.compr && !integer_output) {
|
||||
auto unpack = GLSLstd450UnpackHalf2x16;
|
||||
switch (MrtOutputMode(state, exp)) {
|
||||
case 5: unpack = GLSLstd450UnpackUnorm2x16; break;
|
||||
@@ -303,9 +332,11 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
|
||||
ConstantU32(state, 0),
|
||||
ConstantU32(state, 0),
|
||||
ConstantU32(state, 0),
|
||||
ConstantU32(state, uint_output ? 1u : 0x3f800000u),
|
||||
ConstantU32(state, integer_output ? 1u : 0x3f800000u),
|
||||
};
|
||||
if (exp.compr) {
|
||||
const auto extract = vector_type == TypeU32Vector(state, 4) ? spv::OpBitFieldUExtract
|
||||
: spv::OpBitFieldSExtract;
|
||||
for (uint32_t pair = 0; pair < 2u; pair++) {
|
||||
if ((exp.en & (3u << (pair * 2u))) == 0u) {
|
||||
continue;
|
||||
@@ -317,8 +348,8 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
|
||||
continue;
|
||||
}
|
||||
raw[component] = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpBitFieldUExtract, TypeU32(state), raw[component],
|
||||
packed, ConstantU32(state, lane * 16u),
|
||||
state.builder.AddFunction(extract, TypeU32(state), raw[component], packed,
|
||||
ConstantU32(state, lane * 16u),
|
||||
ConstantU32(state, 16));
|
||||
}
|
||||
}
|
||||
@@ -333,12 +364,10 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
|
||||
state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), data,
|
||||
raw[0], raw[1], raw[2], raw[3]);
|
||||
}
|
||||
if (uint_output) {
|
||||
if (vector_type == TypeU32Vector(state, 4)) {
|
||||
return data;
|
||||
}
|
||||
const auto vector = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpBitcast, TypeF32Vector(state, 4), vector, data);
|
||||
return vector;
|
||||
return Unary(state, spv::OpBitcast, vector_type, data);
|
||||
}
|
||||
|
||||
uint32_t DistancePointer(EmitterState& state, IR::StageOutputKind kind, uint32_t index) {
|
||||
@@ -536,11 +565,11 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
if (state.program.stage != ShaderType::Mesh && variable == 0) {
|
||||
return;
|
||||
}
|
||||
const bool uint_output = MrtOutputMode(state, exp) == 7u;
|
||||
const auto vector_type = uint_output ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4);
|
||||
auto value = ExportVector(ctx, data, exp, uint_output);
|
||||
const auto vector_type = MrtOutputMode(state, exp) != 0u ? MrtOutputType(state, exp.index)
|
||||
: TypeF32Vector(state, 4);
|
||||
auto value = ExportVector(ctx, data, exp, vector_type);
|
||||
if (state.program.stage == ShaderType::Pixel && exp.kind == IR::ExportTargetKind::Mrt &&
|
||||
exp.index == 0 && !uint_output &&
|
||||
exp.index == 0 && vector_type == TypeF32Vector(state, 4) &&
|
||||
state.input_info.pixel->alpha_blend_source != ShaderAlphaBlendSource::None) {
|
||||
// Broadcast logical alpha before swizzling the primary output.
|
||||
const auto blend_output =
|
||||
@@ -684,9 +713,16 @@ void EmitBarrier(EmitterState& state) {
|
||||
return;
|
||||
}
|
||||
const auto memory_scope = tessellation ? spv::ScopeInvocation : spv::ScopeWorkgroup;
|
||||
const auto memory = state.lds_storage_class == spv::StorageClassStorageBuffer
|
||||
? spv::MemorySemanticsUniformMemoryMask
|
||||
: spv::MemorySemanticsWorkgroupMemoryMask;
|
||||
auto memory = state.lds_storage_class == spv::StorageClassStorageBuffer
|
||||
? spv::MemorySemanticsUniformMemoryMask
|
||||
: spv::MemorySemanticsWorkgroupMemoryMask;
|
||||
// Shaders read other waves' buffer and image stores with plain loads right after S_BARRIER.
|
||||
const auto written = [](const auto& resource) { return resource.written; };
|
||||
if (std::ranges::any_of(state.program.info.buffers, written) ||
|
||||
std::ranges::any_of(state.program.info.images, written)) {
|
||||
memory = memory | spv::MemorySemanticsUniformMemoryMask |
|
||||
spv::MemorySemanticsImageMemoryMask;
|
||||
}
|
||||
const auto semantics = tessellation ? spv::MemorySemanticsMaskNone
|
||||
: spv::MemorySemanticsAcquireReleaseMask | memory;
|
||||
state.builder.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup),
|
||||
@@ -784,6 +820,13 @@ uint32_t EmitBallot(ValueEmitContext& ctx, IR::Value predicate) {
|
||||
return ctx.Ballot(predicate);
|
||||
}
|
||||
|
||||
uint32_t EmitLiveMask(EmitterState& state) {
|
||||
const auto helper =
|
||||
EmitNative<spv::OpLoad, IR::Type::U1>(state, state.helper_invocation_variable);
|
||||
return EmitNative<spv::OpGroupNonUniformBallot, IR::Type::U32x4>(
|
||||
state, ConstantU32(state, spv::ScopeSubgroup), EmitLogicalNotBool(state, helper));
|
||||
}
|
||||
|
||||
uint32_t EmitReadFirstLane(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
const auto ballot = ctx.Ballot(inst.Arg(1));
|
||||
const auto lane = ctx.FirstLane(ballot);
|
||||
@@ -794,18 +837,28 @@ uint32_t EmitReadLane(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
return ctx.Shuffle(inst, 0, ctx.Arg(inst, 1));
|
||||
}
|
||||
|
||||
uint32_t EmitSubgroupUMinU32(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
template <spv::Op Reduce, auto Combine>
|
||||
static uint32_t EmitSubgroupReduceU32(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
auto& state = ctx.state;
|
||||
auto value = ctx.Arg(inst, 0);
|
||||
if (ctx.other_half != nullptr) {
|
||||
value = EmitUMin32(state, ctx.HalfArg(inst, 0, 0), ctx.HalfArg(inst, 0, 1));
|
||||
value = Combine(state, ctx.HalfArg(inst, 0, 0), ctx.HalfArg(inst, 0, 1));
|
||||
}
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpGroupNonUniformUMin, TypeU32(state), result,
|
||||
ConstantU32(state, spv::ScopeSubgroup), spv::GroupOperationReduce, value);
|
||||
state.builder.AddFunction(Reduce, TypeU32(state), result,
|
||||
ConstantU32(state, spv::ScopeSubgroup), spv::GroupOperationReduce,
|
||||
value);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t EmitSubgroupUMinU32(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
return EmitSubgroupReduceU32<spv::OpGroupNonUniformUMin, EmitUMin32>(ctx, inst);
|
||||
}
|
||||
|
||||
uint32_t EmitSubgroupUMaxU32(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
return EmitSubgroupReduceU32<spv::OpGroupNonUniformUMax, EmitUMax32>(ctx, inst);
|
||||
}
|
||||
|
||||
uint32_t EmitWriteLane(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
auto& state = ctx.state;
|
||||
const auto hit = state.builder.AllocateId();
|
||||
@@ -816,27 +869,23 @@ uint32_t EmitWriteLane(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
}
|
||||
|
||||
uint32_t EmitPermlane16U32(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
auto& state = ctx.state;
|
||||
const auto flags = inst.Flags<IR::PermlaneFlags>();
|
||||
const auto subid = EmitSubgroupLocalInvocationId(state);
|
||||
const auto row = state.builder.AllocateId();
|
||||
const auto row_value = state.builder.AllocateId();
|
||||
const auto lane = state.builder.AllocateId();
|
||||
const auto lane8 = state.builder.AllocateId();
|
||||
const auto shift = state.builder.AllocateId();
|
||||
const auto upper = state.builder.AllocateId();
|
||||
const auto selected = state.builder.AllocateId();
|
||||
const auto shifted = state.builder.AllocateId();
|
||||
const auto index = state.builder.AllocateId();
|
||||
const auto target = state.builder.AllocateId();
|
||||
auto& state = ctx.state;
|
||||
const auto flags = inst.Flags<IR::PermlaneFlags>();
|
||||
const auto subid = EmitSubgroupLocalInvocationId(state);
|
||||
const auto row = state.builder.AllocateId();
|
||||
const auto lane = state.builder.AllocateId();
|
||||
const auto lane8 = state.builder.AllocateId();
|
||||
const auto shift = state.builder.AllocateId();
|
||||
const auto upper = state.builder.AllocateId();
|
||||
const auto selected = state.builder.AllocateId();
|
||||
const auto shifted = state.builder.AllocateId();
|
||||
const auto index = state.builder.AllocateId();
|
||||
const auto target = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), row, subid,
|
||||
ConstantU32(state, 0xfffffff0u));
|
||||
if (flags.x16) {
|
||||
state.builder.AddFunction(spv::OpBitwiseXor, TypeU32(state), row_value, row,
|
||||
ConstantU32(state, 16));
|
||||
} else {
|
||||
state.builder.AddFunction(spv::OpCopyObject, TypeU32(state), row_value, row);
|
||||
}
|
||||
const auto row_value =
|
||||
flags.x16 ? Binary(state, spv::OpBitwiseXor, TypeU32(state), row, ConstantU32(state, 16))
|
||||
: row;
|
||||
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), lane, subid,
|
||||
ConstantU32(state, 15));
|
||||
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), lane8, lane,
|
||||
@@ -852,14 +901,11 @@ uint32_t EmitPermlane16U32(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
ConstantU32(state, 15));
|
||||
state.builder.AddFunction(spv::OpBitwiseOr, TypeU32(state), target, row_value, index);
|
||||
const auto shuffled = ctx.Shuffle(inst, 0, target);
|
||||
uint32_t result = shuffled;
|
||||
if (!flags.fetch_inactive) {
|
||||
const auto source_exec = ctx.Shuffle(inst, 3, target);
|
||||
result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), result, source_exec, shuffled,
|
||||
ConstantU32(state, 0));
|
||||
if (flags.fetch_inactive) {
|
||||
return shuffled;
|
||||
}
|
||||
return result;
|
||||
return Select(state, TypeU32(state), EmitSourceLaneActive(ctx, ctx.Arg(inst, 3), target),
|
||||
shuffled, ctx.Arg(inst, 4));
|
||||
}
|
||||
|
||||
uint32_t EmitGetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
|
||||
@@ -38,7 +38,8 @@ bool HasFlag(const IR::MemoryInfo& mem, uint32_t flag) {
|
||||
|
||||
uint32_t AddressU32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address,
|
||||
uint32_t component) {
|
||||
const auto layout = Decoder::ImageAddressComponentLayout(mem.image_sample_flags, component);
|
||||
const auto layout = Decoder::ImageAddressComponentLayout(mem.image_sample_flags,
|
||||
mem.image_dimension, component);
|
||||
const auto packed = layout.bit_offset / 32u;
|
||||
if (packed >= address.NumArgs()) return ConstantU32(ctx.state, 0);
|
||||
auto value = ctx.Def(address.Arg(packed));
|
||||
@@ -55,10 +56,11 @@ uint32_t AddressU32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::
|
||||
|
||||
uint32_t AddressF32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address,
|
||||
uint32_t component) {
|
||||
const auto value = AddressU32(ctx, mem, address, component);
|
||||
return Decoder::ImageAddressComponentLayout(mem.image_sample_flags, component).bit_width == 16u
|
||||
? EmitF16BitsToF32(ctx.state, value)
|
||||
: Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), value);
|
||||
const auto value = AddressU32(ctx, mem, address, component);
|
||||
const auto layout = Decoder::ImageAddressComponentLayout(mem.image_sample_flags,
|
||||
mem.image_dimension, component);
|
||||
return layout.bit_width == 16u ? EmitF16BitsToF32(ctx.state, value)
|
||||
: Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), value);
|
||||
}
|
||||
|
||||
ImageSampleLayout Layout(const IR::MemoryInfo& mem) {
|
||||
@@ -77,6 +79,7 @@ ImageSampleLayout Layout(const IR::MemoryInfo& mem) {
|
||||
layout.coord = cursor;
|
||||
cursor += info.coordinate_components;
|
||||
if (HasFlag(mem, Decoder::ImageSampleFlagLod)) layout.lod = cursor++;
|
||||
if (HasFlag(mem, Decoder::ImageSampleFlagLodClamp)) layout.clamp = cursor++;
|
||||
return layout;
|
||||
}
|
||||
|
||||
@@ -810,6 +813,25 @@ uint32_t EmitImageResult(ValueEmitContext& ctx, const IR::Inst& inst, const Emit
|
||||
return mixed ? value : result_bits(mem.resource, value);
|
||||
}
|
||||
|
||||
// Ordinals outside the table select candidates[0]. Past ordinal 0, a consecutive table is an
|
||||
// offset of ordinal - 1, which wraps at 0 so that one unsigned compare checks both ends.
|
||||
uint32_t SamplerArrayIndex(EmitterState& state, uint32_t ordinal,
|
||||
const std::vector<uint32_t>& candidates) {
|
||||
const auto count = static_cast<uint32_t>(candidates.size());
|
||||
const auto gap = std::adjacent_find(candidates.begin() + 1, candidates.end(),
|
||||
[](uint32_t lhs, uint32_t rhs) { return rhs != lhs + 1u; });
|
||||
if (count == 1u || gap != candidates.end()) {
|
||||
return EmitIndexSwitch(state, ordinal, count, TypeU32(state),
|
||||
[&](uint32_t item) { return ConstantU32(state, candidates[item]); });
|
||||
}
|
||||
const auto offset = Binary(state, spv::OpISub, TypeU32(state), ordinal, ConstantU32(state, 1u));
|
||||
const auto in_table = Binary(state, spv::OpULessThan, TypeBool(state), offset,
|
||||
ConstantU32(state, count - 1u));
|
||||
const auto index = Binary(state, spv::OpIAdd, TypeU32(state), offset,
|
||||
ConstantU32(state, candidates[1]));
|
||||
return Select(state, TypeU32(state), in_table, index, ConstantU32(state, candidates[0]));
|
||||
}
|
||||
|
||||
struct ImageSampler {
|
||||
uint32_t id = 0;
|
||||
bool dynamic = false;
|
||||
@@ -845,9 +867,7 @@ std::array<ImageSampler, 2> LoadImageSamplers(ValueEmitContext& ctx, const IR::I
|
||||
selected = EmitIndirectResourceIndex(state, selected,
|
||||
samplers[index].indirect_mapping_offset,
|
||||
samplers[index].indirect_search_iterations, 0u);
|
||||
array_index = EmitIndexSwitch(
|
||||
state, selected, static_cast<uint32_t>(candidates.size()),
|
||||
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, candidates[ordinal]); });
|
||||
array_index = SamplerArrayIndex(state, selected, candidates);
|
||||
}
|
||||
sampler = {LoadSamplerDescriptor(state, index, array_index), array_index != 0u};
|
||||
};
|
||||
@@ -1069,7 +1089,12 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
dref_value = AddressF32(ctx, mem, *address, layout.dref);
|
||||
}
|
||||
}
|
||||
std::array<uint32_t, 4> operands {};
|
||||
// _cl clamps the computed LOD from below.
|
||||
uint32_t min_lod = 0;
|
||||
if (layout.clamp != NoImageComponent) {
|
||||
min_lod = AddressF32(ctx, mem, *address, layout.clamp);
|
||||
}
|
||||
std::array<uint32_t, 5> operands {};
|
||||
size_t operand_count = 0;
|
||||
if (HasFlag(mem, Decoder::ImageSampleFlagDerivative)) {
|
||||
operands[0] = spv::ImageOperandsGradMask;
|
||||
@@ -1081,6 +1106,11 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
if (HasFlag(mem, Decoder::ImageSampleFlagLod) && layout.lod != NoImageComponent) {
|
||||
lod = AddressF32(ctx, mem, *address, layout.lod);
|
||||
}
|
||||
// SPIR-V takes MinLod only with implicit LOD or Grad.
|
||||
if (min_lod != 0) {
|
||||
lod = EmitGlsl<GLSLstd450FMax, IR::Type::F32>(state, lod, min_lod);
|
||||
min_lod = 0;
|
||||
}
|
||||
operands[0] = spv::ImageOperandsLodMask;
|
||||
operands[1] = lod;
|
||||
operand_count = 2;
|
||||
@@ -1104,6 +1134,13 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
sample_operands[sample_operand_count++] = PackedOffset(
|
||||
ctx, offset_value, candidate.dimension, sample_operands[0]);
|
||||
}
|
||||
// MinLod follows the offset in operand order.
|
||||
if (min_lod != 0) {
|
||||
state.builder.RequireCapability(spv::CapabilityMinLod);
|
||||
sample_operand_count = std::max(size_t{1}, sample_operand_count);
|
||||
sample_operands[0] |= spv::ImageOperandsMinLodMask;
|
||||
sample_operands[sample_operand_count++] = min_lod;
|
||||
}
|
||||
const auto coord =
|
||||
CoordF32(ctx, mem, *address, layout.coord,
|
||||
ImageDimensionInfoFor(candidate.dimension).coordinate_components,
|
||||
|
||||
@@ -56,6 +56,7 @@ EMIT_NATIVE(BitFieldSExtract, OpBitFieldSExtract, U32, uint32_t, uint32_t, uint3
|
||||
EMIT_NATIVE(SelectU1, OpSelect, U1, uint32_t, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(SelectU32, OpSelect, U32, uint32_t, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(SelectF32, OpSelect, F32, uint32_t, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(SelectF64, OpSelect, F64, uint32_t, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(IAdd32, OpIAdd, U32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(ISub32, OpISub, U32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(IMul32, OpIMul, U32, uint32_t, uint32_t)
|
||||
@@ -229,10 +230,12 @@ uint32_t EmitDppUpdateU32(ValueEmitContext& ctx, const IR::Inst& in
|
||||
uint32_t EmitWqmU64(EmitterState& state, uint32_t value);
|
||||
uint32_t EmitLaneId(EmitterState& state);
|
||||
uint32_t EmitBallot(ValueEmitContext& ctx, IR::Value predicate);
|
||||
uint32_t EmitLiveMask(EmitterState& state);
|
||||
uint32_t EmitConditionRef(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
uint32_t EmitReadFirstLane(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
uint32_t EmitReadLane(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
uint32_t EmitSubgroupUMinU32(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
uint32_t EmitSubgroupUMaxU32(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
uint32_t EmitWriteLane(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
uint32_t EmitPermlane16U32(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
uint32_t EmitGetAttribute(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
@@ -288,6 +291,7 @@ inline constexpr auto EmitBufferAtomicIAdd32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicISub32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicSMin32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicUMin32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicUMin64 = EmitBufferAtomic64;
|
||||
inline constexpr auto EmitBufferAtomicSMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicUMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicAnd32 = EmitAtomic32;
|
||||
|
||||
@@ -112,6 +112,7 @@ struct EmitterState {
|
||||
uint32_t lane_half = 0;
|
||||
std::array<BufferDefinition, 4> storage_buffers {};
|
||||
std::vector<uint32_t> memory_byte_offsets;
|
||||
std::vector<uint32_t> memory_byte_sizes;
|
||||
uint32_t bda_pagetable_variable = 0;
|
||||
uint32_t fault_buffer_variable = 0;
|
||||
uint32_t bda_pointer_function = 0;
|
||||
@@ -145,6 +146,7 @@ struct EmitterState {
|
||||
const IR::Block* current_block = nullptr;
|
||||
uint32_t pixel_valid_mask_variable = 0;
|
||||
uint32_t subgroup_local_invocation_id_variable = 0;
|
||||
uint32_t helper_invocation_variable = 0;
|
||||
uint32_t per_vertex_variable = 0;
|
||||
uint32_t point_size_variable = 0;
|
||||
uint32_t clip_distance_variable = 0;
|
||||
@@ -282,6 +284,7 @@ struct ImageSampleLayout {
|
||||
uint32_t bias = NoImageComponent;
|
||||
uint32_t coord = 0;
|
||||
uint32_t lod = NoImageComponent;
|
||||
uint32_t clamp = NoImageComponent;
|
||||
uint32_t grad_x = NoImageComponent;
|
||||
uint32_t grad_y = NoImageComponent;
|
||||
};
|
||||
@@ -305,6 +308,7 @@ uint32_t VertexParameterScalarType(EmitterState& state, VertexInputScalarKind ki
|
||||
|
||||
|
||||
uint32_t OutputVariableForExport(const EmitterState& state, const IR::ExportInfo& exp);
|
||||
uint32_t MrtOutputType(EmitterState& state, uint32_t index);
|
||||
|
||||
uint32_t ConstantU32(EmitterState& state, uint32_t value);
|
||||
|
||||
@@ -414,7 +418,7 @@ Prospero::BufferFormat StorageBufferFormat(const EmitterState& state, const IR::
|
||||
|
||||
uint32_t SharedIndirectBufferResource(const EmitterState& state, uint32_t resource);
|
||||
|
||||
void EmitMemoryOffsets(EmitterState& state);
|
||||
void EmitMemoryBindings(EmitterState& state);
|
||||
|
||||
uint32_t LdsDwordCount(const EmitterState& state);
|
||||
void EnsureLdsStorage(EmitterState& state);
|
||||
@@ -531,6 +535,7 @@ inline constexpr auto EmitFNegateValue = EmitNative<spv::OpFNegate, IR::Type::F3
|
||||
inline constexpr auto EmitFAbsValue = EmitGlsl<GLSLstd450FAbs, IR::Type::F32, uint32_t>;
|
||||
|
||||
uint32_t EmitF16BitsToF32(EmitterState& state, uint32_t bits);
|
||||
uint32_t EmitF32BitsToF16RtzBits(EmitterState& state, uint32_t bits);
|
||||
|
||||
void EmitProgram(EmitterState& state);
|
||||
|
||||
|
||||
@@ -1541,16 +1541,16 @@ uint32_t EmitAppendConsume(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
const auto index =
|
||||
Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2));
|
||||
const auto access = PrepareMemoryResourceAccess(state, mem);
|
||||
const auto exec = ctx.Arg(inst, 1);
|
||||
const auto ballot = ctx.Ballot(inst.Arg(1));
|
||||
const auto low = state.builder.AllocateId();
|
||||
const auto high = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), low, ballot, 0);
|
||||
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), high, ballot, 1);
|
||||
// Elect and count from the guest EXEC words, which V_MBCNT also ranks lanes by.
|
||||
const auto low = ctx.Arg(inst, 1);
|
||||
const auto high = ctx.Arg(inst, 2);
|
||||
const auto count = Binary(state, spv::OpIAdd, TypeU32(state),
|
||||
Unary(state, spv::OpBitCount, TypeU32(state), low),
|
||||
Unary(state, spv::OpBitCount, TypeU32(state), high));
|
||||
const auto first = ctx.FirstLane(ballot);
|
||||
const auto mask = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), mask, low, high,
|
||||
ConstantU32(state, 0), ConstantU32(state, 0));
|
||||
const auto first = ctx.FirstLane(mask);
|
||||
const auto source_lane =
|
||||
state.lane_count == 2
|
||||
? Binary(state, spv::OpBitwiseAnd, TypeU32(state), first, ConstantU32(state, 31))
|
||||
@@ -1564,9 +1564,7 @@ uint32_t EmitAppendConsume(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
const auto condition = AndCondition(
|
||||
state, is_first,
|
||||
AndCondition(state,
|
||||
state.lane_count == 2 ? Binary(state, spv::OpINotEqual, TypeBool(state), count,
|
||||
ConstantU32(state, 0))
|
||||
: exec,
|
||||
Binary(state, spv::OpINotEqual, TypeBool(state), count, ConstantU32(state, 0)),
|
||||
bounds));
|
||||
const auto atomic = EmitValueOrZeroIfCondition(state, condition, [&]() {
|
||||
const auto value = state.builder.AllocateId();
|
||||
|
||||
@@ -154,9 +154,10 @@ uint32_t SharedIndirectBufferResource(const EmitterState& state, uint32_t resour
|
||||
return representative;
|
||||
}
|
||||
|
||||
void EmitMemoryOffsets(EmitterState& state) {
|
||||
void EmitMemoryBindings(EmitterState& state) {
|
||||
const auto count = state.program.bindings.descriptor_counts[static_cast<size_t>(IR::DescriptorBindingKind::Buffers)];
|
||||
state.memory_byte_offsets.resize(count);
|
||||
state.memory_byte_sizes.resize(count);
|
||||
std::vector<bool> dynamic_offsets(count);
|
||||
for (uint32_t resource = 0; resource < state.program.info.buffers.size(); ++resource) {
|
||||
if (SharedIndirectBufferResource(state, resource) == UINT32_MAX) continue;
|
||||
@@ -174,6 +175,8 @@ void EmitMemoryOffsets(EmitterState& state) {
|
||||
if (dynamic_offsets[i + lane]) continue;
|
||||
state.memory_byte_offsets[i + lane] = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
|
||||
state, word, ConstantU32(state, lane * 8u), ConstantU32(state, 8u));
|
||||
state.memory_byte_sizes[i + lane] =
|
||||
EmitShaderDataDwordLoad(state, state.program.bindings.MemorySizeDword() + i + lane);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,22 +260,28 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
|
||||
state.builder.RequireCapability(spv::CapabilityShaderNonUniform);
|
||||
state.builder.RequireCapability(spv::CapabilityStorageBufferArrayNonUniformIndexing);
|
||||
state.builder.AddAnnotation(spv::OpDecorate, access.object_pointer, spv::DecorationNonUniform);
|
||||
const auto offsets_start = state.program.bindings.memory_offset_dword +
|
||||
(state.program.bindings.UsesPushData() ? state.program.bindings.push_data_start_dword : 0u);
|
||||
const auto& bindings = state.program.bindings;
|
||||
const auto data_start = bindings.UsesPushData() ? bindings.push_data_start_dword : 0u;
|
||||
const auto word = EmitShaderDataDwordLoadAt(state, EmitAddU32(
|
||||
state, ConstantU32(state, offsets_start),
|
||||
state, ConstantU32(state, bindings.memory_offset_dword + data_start),
|
||||
Binary(state, spv::OpShiftRightLogical, TypeU32(state), array_index, ConstantU32(state, 2))));
|
||||
const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state), array_index, ConstantU32(state, 3)),
|
||||
ConstantU32(state, 3));
|
||||
access.byte_offset = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
|
||||
state, word, shift, ConstantU32(state, 8));
|
||||
access.length = EmitShaderDataDwordLoadAt(state, EmitAddU32(
|
||||
state, ConstantU32(state, bindings.MemorySizeDword() + data_start), array_index));
|
||||
} else {
|
||||
access.byte_offset = state.memory_byte_offsets[static_index];
|
||||
access.length = state.memory_byte_sizes[static_index];
|
||||
}
|
||||
// storage_buffers[i] views the binding as 1 << i byte elements.
|
||||
const auto shift = static_cast<uint32_t>(&buffer - state.storage_buffers.data());
|
||||
if (shift != 0u) {
|
||||
access.length = Binary(state, spv::OpShiftRightLogical, TypeU32(state), access.length,
|
||||
ConstantU32(state, shift));
|
||||
}
|
||||
access.length = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpArrayLength, TypeU32(state), access.length,
|
||||
access.object_pointer, 0);
|
||||
return access;
|
||||
}
|
||||
|
||||
@@ -472,6 +481,17 @@ uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::Buffer
|
||||
UnormRounding rounding) {
|
||||
EXIT_IF(rounding != UnormRounding::RoundEven && info.type != Format::ComponentType::Unorm);
|
||||
const auto bits = info.component_bits[component];
|
||||
if (info.type == Format::ComponentType::Float && bits < 16u) {
|
||||
// Truncate to f16, whose bias the unsigned 11/10-bit floats share, and drop mantissa
|
||||
// bits; negatives and NaN, whose f16 bits lie above +inf, store zero.
|
||||
const auto half = data_bits == 16u ? EmitAndConstant(state, data, 0xffffu)
|
||||
: EmitF32BitsToF16RtzBits(state, data);
|
||||
return Select(state, TypeU32(state),
|
||||
EmitCompareU32Constant(state, spv::OpUGreaterThan, half, 0x7c00u),
|
||||
ConstantU32(state, 0),
|
||||
EmitBinaryU32(state, spv::OpShiftRightLogical, half,
|
||||
ConstantU32(state, 15u - bits)));
|
||||
}
|
||||
if (data_bits == 16u) {
|
||||
if (info.type == Format::ComponentType::Uint) {
|
||||
data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
|
||||
@@ -565,6 +585,7 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
||||
case IR::ValueOpcode::ImageAtomicUMin32:
|
||||
case IR::ValueOpcode::ImageAtomicUMin64:
|
||||
case IR::ValueOpcode::BufferAtomicUMin32:
|
||||
case IR::ValueOpcode::BufferAtomicUMin64:
|
||||
case IR::ValueOpcode::SharedAtomicUMin32: return spv::OpAtomicUMin;
|
||||
case IR::ValueOpcode::ImageAtomicSMax32:
|
||||
case IR::ValueOpcode::BufferAtomicSMax32:
|
||||
|
||||
@@ -476,6 +476,8 @@ uint32_t BuiltInForInput(IR::StageInputKind kind) {
|
||||
case IR::StageInputKind::LocalInvocationId: return spv::BuiltInLocalInvocationId;
|
||||
case IR::StageInputKind::LocalInvocationIndex: return spv::BuiltInLocalInvocationIndex;
|
||||
case IR::StageInputKind::GlobalInvocationId: return spv::BuiltInGlobalInvocationId;
|
||||
case IR::StageInputKind::BaseVertex: return spv::BuiltInBaseVertex;
|
||||
case IR::StageInputKind::BaseInstance: return spv::BuiltInBaseInstance;
|
||||
default: return UINT32_MAX;
|
||||
}
|
||||
}
|
||||
@@ -526,7 +528,9 @@ void DefineInputs(EmitterState& state) {
|
||||
case IR::StageInputKind::PrimitiveId:
|
||||
case IR::StageInputKind::InstanceIndex:
|
||||
case IR::StageInputKind::Layer:
|
||||
case IR::StageInputKind::SampleId: type = TypeI32(state); break;
|
||||
case IR::StageInputKind::SampleId:
|
||||
case IR::StageInputKind::BaseVertex:
|
||||
case IR::StageInputKind::BaseInstance: type = TypeI32(state); break;
|
||||
case IR::StageInputKind::WorkgroupId:
|
||||
case IR::StageInputKind::NumWorkgroups:
|
||||
case IR::StageInputKind::LocalInvocationId:
|
||||
@@ -595,6 +599,13 @@ void DefineInputs(EmitterState& state) {
|
||||
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationFlat);
|
||||
}
|
||||
}
|
||||
if (state.program.info.helper_invocation) {
|
||||
const auto variable = DefineInterfaceVariable(state, TypeBool(state), spv::StorageClassInput,
|
||||
"gl_HelperInvocation");
|
||||
state.helper_invocation_variable = variable;
|
||||
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBuiltIn,
|
||||
spv::BuiltInHelperInvocation);
|
||||
}
|
||||
}
|
||||
|
||||
void DefineOutputs(EmitterState& state) {
|
||||
@@ -691,13 +702,15 @@ void DefineOutputs(EmitterState& state) {
|
||||
break;
|
||||
case IR::StageOutputKind::Parameter:
|
||||
case IR::StageOutputKind::Mrt: {
|
||||
const bool uint_output =
|
||||
binding.kind == IR::StageOutputKind::Mrt &&
|
||||
state.program.stage == ShaderType::Pixel &&
|
||||
binding.index < std::size(state.input_info.pixel->target_output_mode) &&
|
||||
(state.input_info.pixel->target_output_mode[binding.index] == 7u ||
|
||||
(state.input_info.pixel->target_packed_unorm_mask & (1u << binding.index)) != 0u);
|
||||
const auto type = uint_output ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4);
|
||||
auto type = TypeF32Vector(state, 4);
|
||||
if (binding.kind == IR::StageOutputKind::Mrt &&
|
||||
state.program.stage == ShaderType::Pixel && binding.index < 8u) {
|
||||
// Packed UNorm stores its encoded bits.
|
||||
const auto bit = 1u << binding.index;
|
||||
type = (state.input_info.pixel->target_packed_unorm_mask & bit) != 0u
|
||||
? TypeU32Vector(state, 4)
|
||||
: MrtOutputType(state, binding.index);
|
||||
}
|
||||
binding.variable_id = DefineInterfaceVariable(state, type, spv::StorageClassOutput,
|
||||
binding.debug_name.c_str());
|
||||
const bool dual_source = binding.kind == IR::StageOutputKind::Mrt &&
|
||||
@@ -793,6 +806,10 @@ void DefineModule(EmitterState& state) {
|
||||
if (InputVariableForKind(state, IR::StageInputKind::SampleId) != 0) {
|
||||
state.builder.RequireCapability(spv::CapabilitySampleRateShading);
|
||||
}
|
||||
if (InputVariableForKind(state, IR::StageInputKind::BaseVertex) != 0 ||
|
||||
InputVariableForKind(state, IR::StageInputKind::BaseInstance) != 0) {
|
||||
state.builder.RequireCapability(spv::CapabilityDrawParameters);
|
||||
}
|
||||
if (state.program.info.image_gather_extended) {
|
||||
state.builder.RequireCapability(spv::CapabilityImageGatherExtended);
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ void EmitDirectInstruction(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
const auto op = inst.GetOpcode();
|
||||
if (ctx.half != 0 &&
|
||||
(op == Op::Ballot || op == Op::ReadFirstLane || op == Op::SubgroupUMinU32 ||
|
||||
op == Op::ReadConstBuffer || op == Op::ReadRealtimeClock ||
|
||||
op == Op::SubgroupUMaxU32 || op == Op::ReadConstBuffer || op == Op::ReadRealtimeClock ||
|
||||
(op == Op::LoadAddressU32 && ctx.Memory(inst).kind == IR::ResourceKind::ScalarAddress))) {
|
||||
// Whole-wave results, clocks and SMEM loads (SGPR operands, no EXEC) match in both halves.
|
||||
ctx.Define(inst, ctx.other_half->Result(inst));
|
||||
@@ -746,7 +746,7 @@ void EmitProgram(EmitterState& state) {
|
||||
state.lds_base_dwords = EmitBinaryU32(state, spv::OpIMul, index,
|
||||
ConstantU32(state, LdsDwordCount(state)));
|
||||
}
|
||||
EmitMemoryOffsets(state);
|
||||
EmitMemoryBindings(state);
|
||||
if (program.blocks.empty()) {
|
||||
EmitReturn(ctx);
|
||||
} else if (state.program.dispatcher_fallback) {
|
||||
|
||||
@@ -1710,21 +1710,25 @@ Graph Structurize(const Graph& graph) {
|
||||
return GotoStructurizer(graph).Run();
|
||||
}
|
||||
|
||||
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code) {
|
||||
// Vector comparisons and carry results can write a scalar pair in wave64.
|
||||
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code, uint32_t wave_size) {
|
||||
// A vector lane mask (comparison or carry) spans wave_size / 32 SGPRs; a lane read writes one.
|
||||
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) {
|
||||
const bool lane_read =
|
||||
inst.opcode == Opcode::V_READLANE_B32 || inst.opcode == Opcode::V_READFIRSTLANE_B32;
|
||||
const uint32_t size = lane_read ? 1u : wave_size / 32u;
|
||||
uint32_t destination = 0;
|
||||
for (const auto* operand: {&inst.dst, &inst.dst2})
|
||||
if (ScalarOperandCode(*operand, destination) && code >= destination && code <= destination + 1u)
|
||||
if (ScalarOperandCode(*operand, destination) && code >= destination && code < destination + size)
|
||||
return true;
|
||||
}
|
||||
return InstructionWritesScalarCode(inst, code);
|
||||
}
|
||||
|
||||
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
|
||||
uint32_t before, uint32_t code, std::vector<uint32_t>& crossed_calls) {
|
||||
uint32_t before, uint32_t code, uint32_t wave_size,
|
||||
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;
|
||||
@@ -1743,11 +1747,11 @@ uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& grap
|
||||
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));
|
||||
EXIT_NOT_IMPLEMENTED(MayWriteScalarRegister(inst, code, wave_size));
|
||||
crossed_calls.push_back(i);
|
||||
continue;
|
||||
}
|
||||
if (MayWriteScalarRegister(inst, code)) { index = i; break; }
|
||||
if (MayWriteScalarRegister(inst, code, wave_size)) { index = i; break; }
|
||||
}
|
||||
if (index != UINT32_MAX || block->id == graph.entry_block) {
|
||||
EXIT_IF(definition && *definition != index);
|
||||
|
||||
@@ -113,9 +113,10 @@ struct Graph {
|
||||
};
|
||||
|
||||
Graph BuildGraph(const Decoder::Program& program);
|
||||
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code);
|
||||
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code, uint32_t wave_size);
|
||||
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
|
||||
uint32_t before, uint32_t code, std::vector<uint32_t>& crossed_calls);
|
||||
uint32_t before, uint32_t code, uint32_t wave_size,
|
||||
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);
|
||||
|
||||
@@ -14,12 +14,6 @@ struct MimgSampleInfo {
|
||||
uint32_t flags = 0;
|
||||
};
|
||||
|
||||
struct MimgGatherInfo {
|
||||
uint32_t encoding = 0;
|
||||
Opcode decoded = Opcode::UNSUPPORTED;
|
||||
uint32_t flags = 0;
|
||||
};
|
||||
|
||||
struct MimgAtomicInfo {
|
||||
uint32_t encoding = 0;
|
||||
Opcode decoded = Opcode::UNSUPPORTED;
|
||||
@@ -73,7 +67,7 @@ constexpr uint32_t ImageSampleAddressComponents(uint32_t flags, ImageDimension d
|
||||
if ((flags & ImageSampleFlagBias) != 0) {
|
||||
components++;
|
||||
}
|
||||
if ((flags & ImageSampleFlagLod) != 0) {
|
||||
if ((flags & (ImageSampleFlagLod | ImageSampleFlagLodClamp)) != 0) {
|
||||
components++;
|
||||
}
|
||||
if ((flags & ImageSampleFlagDerivative) != 0) {
|
||||
@@ -181,23 +175,72 @@ constexpr MimgSampleInfo MIMG_SAMPLE_OPCODE_LIST[] = {
|
||||
ImageSampleFlagOffset | ImageSampleFlagAdjust},
|
||||
};
|
||||
|
||||
constexpr MimgGatherInfo MIMG_GATHER_OPCODE_LIST[] = {
|
||||
{0x44u, Opcode::IMAGE_GATHER4_L, ImageSampleFlagLod},
|
||||
{0x47u, Opcode::IMAGE_GATHER4_LZ, ImageSampleFlagLevelZero},
|
||||
{0x48u, Opcode::IMAGE_GATHER4_C, ImageSampleFlagCompare},
|
||||
{0x4cu, Opcode::IMAGE_GATHER4_C_L, ImageSampleFlagCompare | ImageSampleFlagLod},
|
||||
{0x4fu, Opcode::IMAGE_GATHER4_C_LZ,
|
||||
ImageSampleFlagCompare | ImageSampleFlagLevelZero},
|
||||
{0x57u, Opcode::IMAGE_GATHER4_LZ_O,
|
||||
ImageSampleFlagLevelZero | ImageSampleFlagOffset},
|
||||
{0x58u, Opcode::IMAGE_GATHER4_C_O,
|
||||
ImageSampleFlagCompare | ImageSampleFlagOffset},
|
||||
{0x5fu, Opcode::IMAGE_GATHER4_C_LZ_O,
|
||||
constexpr MimgSampleInfo MIMG_GATHER_OPCODE_LIST[] = {
|
||||
{0x40u, "image_gather4", 0},
|
||||
{0x41u, "image_gather4_cl", ImageSampleFlagLodClamp},
|
||||
{0x44u, "image_gather4_l", ImageSampleFlagLod},
|
||||
{0x45u, "image_gather4_b", ImageSampleFlagBias},
|
||||
{0x46u, "image_gather4_b_cl", ImageSampleFlagBias | ImageSampleFlagLodClamp},
|
||||
{0x47u, "image_gather4_lz", ImageSampleFlagLevelZero},
|
||||
{0x48u, "image_gather4_c", ImageSampleFlagCompare},
|
||||
{0x49u, "image_gather4_c_cl", ImageSampleFlagCompare | ImageSampleFlagLodClamp},
|
||||
{0x4cu, "image_gather4_c_l", ImageSampleFlagCompare | ImageSampleFlagLod},
|
||||
{0x4du, "image_gather4_c_b", ImageSampleFlagCompare | ImageSampleFlagBias},
|
||||
{0x4eu, "image_gather4_c_b_cl",
|
||||
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagLodClamp},
|
||||
{0x4fu, "image_gather4_c_lz", ImageSampleFlagCompare | ImageSampleFlagLevelZero},
|
||||
{0x50u, "image_gather4_o", ImageSampleFlagOffset},
|
||||
{0x51u, "image_gather4_cl_o", ImageSampleFlagLodClamp | ImageSampleFlagOffset},
|
||||
{0x54u, "image_gather4_l_o", ImageSampleFlagLod | ImageSampleFlagOffset},
|
||||
{0x55u, "image_gather4_b_o", ImageSampleFlagBias | ImageSampleFlagOffset},
|
||||
{0x56u, "image_gather4_b_cl_o",
|
||||
ImageSampleFlagBias | ImageSampleFlagLodClamp | ImageSampleFlagOffset},
|
||||
{0x57u, "image_gather4_lz_o", ImageSampleFlagLevelZero | ImageSampleFlagOffset},
|
||||
{0x58u, "image_gather4_c_o", ImageSampleFlagCompare | ImageSampleFlagOffset},
|
||||
{0x59u, "image_gather4_c_cl_o",
|
||||
ImageSampleFlagCompare | ImageSampleFlagLodClamp | ImageSampleFlagOffset},
|
||||
{0x5cu, "image_gather4_c_l_o",
|
||||
ImageSampleFlagCompare | ImageSampleFlagLod | ImageSampleFlagOffset},
|
||||
{0x5du, "image_gather4_c_b_o",
|
||||
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagOffset},
|
||||
{0x5eu, "image_gather4_c_b_cl_o",
|
||||
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagLodClamp |
|
||||
ImageSampleFlagOffset},
|
||||
{0x5fu, "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,
|
||||
{0x61u, "image_gather4h", ImageSampleFlagGatherHorizontal},
|
||||
{0xc0u, "image_gather4_a", ImageSampleFlagAdjust},
|
||||
{0xc1u, "image_gather4_cl_a", ImageSampleFlagLodClamp | ImageSampleFlagAdjust},
|
||||
{0xc5u, "image_gather4_b_a", ImageSampleFlagBias | ImageSampleFlagAdjust},
|
||||
{0xc6u, "image_gather4_b_cl_a",
|
||||
ImageSampleFlagBias | ImageSampleFlagLodClamp | ImageSampleFlagAdjust},
|
||||
{0xc8u, "image_gather4_c_a", ImageSampleFlagCompare | ImageSampleFlagAdjust},
|
||||
{0xc9u, "image_gather4_c_cl_a",
|
||||
ImageSampleFlagCompare | ImageSampleFlagLodClamp | ImageSampleFlagAdjust},
|
||||
{0xcdu, "image_gather4_c_b_a",
|
||||
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagAdjust},
|
||||
{0xceu, "image_gather4_c_b_cl_a",
|
||||
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagLodClamp |
|
||||
ImageSampleFlagAdjust},
|
||||
{0xd0u, "image_gather4_o_a", ImageSampleFlagOffset | ImageSampleFlagAdjust},
|
||||
{0xd1u, "image_gather4_cl_o_a",
|
||||
ImageSampleFlagLodClamp | ImageSampleFlagOffset | ImageSampleFlagAdjust},
|
||||
{0xd5u, "image_gather4_b_o_a",
|
||||
ImageSampleFlagBias | ImageSampleFlagOffset | ImageSampleFlagAdjust},
|
||||
{0xd6u, "image_gather4_b_cl_o_a",
|
||||
ImageSampleFlagBias | ImageSampleFlagLodClamp | ImageSampleFlagOffset |
|
||||
ImageSampleFlagAdjust},
|
||||
{0xd8u, "image_gather4_c_o_a",
|
||||
ImageSampleFlagCompare | ImageSampleFlagOffset | ImageSampleFlagAdjust},
|
||||
{0xd9u, "image_gather4_c_cl_o_a",
|
||||
ImageSampleFlagCompare | ImageSampleFlagLodClamp | ImageSampleFlagOffset |
|
||||
ImageSampleFlagAdjust},
|
||||
{0xddu, "image_gather4_c_b_o_a",
|
||||
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagOffset |
|
||||
ImageSampleFlagAdjust},
|
||||
{0xdeu, "image_gather4_c_b_cl_o_a",
|
||||
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagLodClamp |
|
||||
ImageSampleFlagOffset | ImageSampleFlagAdjust},
|
||||
};
|
||||
|
||||
constexpr MimgAtomicInfo MIMG_ATOMIC_OPCODE_LIST[] = {
|
||||
@@ -219,13 +262,13 @@ constexpr auto MIMG_SAMPLE_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_SAMPLE_OPCO
|
||||
constexpr auto MIMG_GATHER_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_GATHER_OPCODE_LIST);
|
||||
constexpr auto MIMG_ATOMIC_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_ATOMIC_OPCODE_LIST);
|
||||
|
||||
Opcode DecodeMimgOpcode(uint32_t opcode, const MimgSampleInfo* sample, const MimgGatherInfo* gather,
|
||||
Opcode DecodeMimgOpcode(uint32_t opcode, const MimgSampleInfo* sample, const MimgSampleInfo* gather,
|
||||
const MimgAtomicInfo* atomic) {
|
||||
if (sample != nullptr) {
|
||||
return Opcode::IMAGE_SAMPLE;
|
||||
}
|
||||
if (gather != nullptr) {
|
||||
return gather->decoded;
|
||||
return Opcode::IMAGE_GATHER4;
|
||||
}
|
||||
if (atomic != nullptr) {
|
||||
return atomic->decoded;
|
||||
@@ -244,24 +287,10 @@ Opcode DecodeMimgOpcode(uint32_t opcode, const MimgSampleInfo* sample, const Mim
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t DecodeMimgSampleFlags(const MimgSampleInfo* sample, const MimgGatherInfo* gather) {
|
||||
if (sample != nullptr) {
|
||||
return sample->flags;
|
||||
}
|
||||
if (gather != nullptr) {
|
||||
return gather->flags;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t DecodeMimgAddressComponents(uint32_t opcode, ImageDimension dimension,
|
||||
const MimgSampleInfo* sample, const MimgGatherInfo* gather,
|
||||
const MimgAtomicInfo* atomic) {
|
||||
if (sample != nullptr) {
|
||||
return ImageSampleAddressComponents(sample->flags, dimension);
|
||||
}
|
||||
if (gather != nullptr) {
|
||||
return ImageSampleAddressComponents(gather->flags, dimension);
|
||||
const MimgSampleInfo* sampled, const MimgAtomicInfo* atomic) {
|
||||
if (sampled != nullptr) {
|
||||
return ImageSampleAddressComponents(sampled->flags, dimension);
|
||||
}
|
||||
if (atomic != nullptr) {
|
||||
return ImageCoordComponents(dimension);
|
||||
@@ -290,33 +319,31 @@ uint32_t CountDmaskComponents(uint32_t dmask) {
|
||||
|
||||
} // namespace
|
||||
|
||||
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, uint32_t component) {
|
||||
const auto width = [flags](uint32_t index) {
|
||||
if ((flags & ImageSampleFlagA16) == 0u) return 32u;
|
||||
uint32_t cursor = 0;
|
||||
if ((flags & ImageSampleFlagOffset) != 0u) {
|
||||
if (index == cursor++) return 32u;
|
||||
}
|
||||
if ((flags & ImageSampleFlagBias) != 0u) {
|
||||
if (index == cursor++) return 16u;
|
||||
}
|
||||
if ((flags & ImageSampleFlagCompare) != 0u && index == cursor) return 32u;
|
||||
return 16u;
|
||||
};
|
||||
uint32_t offset = 0;
|
||||
for (uint32_t index = 0; index < component; index++) {
|
||||
const auto bits = width(index);
|
||||
if (bits == 32u) offset = (offset + 31u) & ~31u;
|
||||
offset += bits;
|
||||
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, ImageDimension dimension,
|
||||
uint32_t component) {
|
||||
if ((flags & ImageSampleFlagA16) == 0u) return {component * 32u, 32u};
|
||||
// A16 offsets, bias and PCF reference each take a whole VGPR, the bias as a half in its low
|
||||
// 16 bits. The other components pack two per VGPR, except that each 1D derivative and the last
|
||||
// two 3D ones take the low half of their own VGPR.
|
||||
const bool has_bias = (flags & ImageSampleFlagBias) != 0u;
|
||||
const uint32_t bias_index = (flags & ImageSampleFlagOffset) != 0u ? 1u : 0u;
|
||||
const uint32_t whole_vgprs = std::popcount(
|
||||
flags & (ImageSampleFlagOffset | ImageSampleFlagBias | ImageSampleFlagCompare));
|
||||
if (component < whole_vgprs) {
|
||||
return {component * 32u, has_bias && component == bias_index ? 16u : 32u};
|
||||
}
|
||||
const auto bits = width(component);
|
||||
if (bits == 32u) offset = (offset + 31u) & ~31u;
|
||||
return {offset, bits};
|
||||
const uint32_t axes =
|
||||
(flags & ImageSampleFlagDerivative) != 0u ? ImageGradientComponents(dimension) : 0u;
|
||||
const uint32_t unpaired = axes % 2u * 2u;
|
||||
const uint32_t paired = axes * 2u - unpaired;
|
||||
const uint32_t index = component - whole_vgprs;
|
||||
const uint32_t half = index + std::clamp(index, paired, paired + unpaired) - paired;
|
||||
return {whole_vgprs * 32u + half * 16u, 16u};
|
||||
}
|
||||
|
||||
uint32_t ImageAddressDwordCount(uint32_t flags, uint32_t components) {
|
||||
uint32_t ImageAddressDwordCount(uint32_t flags, ImageDimension dimension, uint32_t components) {
|
||||
if (components == 0u) return 0u;
|
||||
const auto last = ImageAddressComponentLayout(flags, components - 1u);
|
||||
const auto last = ImageAddressComponentLayout(flags, dimension, components - 1u);
|
||||
return (last.bit_offset + last.bit_width + 31u) / 32u;
|
||||
}
|
||||
|
||||
@@ -329,16 +356,17 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
const auto dimension = DecodeImageDimension((word0 >> 3u) & 0x7u);
|
||||
const uint32_t word_count = 2u + nsa_dwords;
|
||||
|
||||
const uint32_t vdata = (word1 >> 8u) & 0xffu;
|
||||
const uint32_t vaddr = word1 & 0xffu;
|
||||
const uint32_t srsrc = (word1 >> 16u) & 0x1fu;
|
||||
const uint32_t ssamp = (word1 >> 21u) & 0x1fu;
|
||||
const bool r128 = ((word0 >> 15u) & 0x1u) != 0u;
|
||||
const bool a16 = ((word1 >> 30u) & 0x1u) != 0u;
|
||||
const bool d16 = ((word1 >> 31u) & 0x1u) != 0u;
|
||||
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
|
||||
const auto* gather = Detail::FindOpcode(MIMG_GATHER_OPS, opcode);
|
||||
const auto* atomic = Detail::FindOpcode(MIMG_ATOMIC_OPS, opcode);
|
||||
const uint32_t vdata = (word1 >> 8u) & 0xffu;
|
||||
const uint32_t vaddr = word1 & 0xffu;
|
||||
const uint32_t srsrc = (word1 >> 16u) & 0x1fu;
|
||||
const uint32_t ssamp = (word1 >> 21u) & 0x1fu;
|
||||
const bool r128 = ((word0 >> 15u) & 0x1u) != 0u;
|
||||
const bool a16 = ((word1 >> 30u) & 0x1u) != 0u;
|
||||
const bool d16 = ((word1 >> 31u) & 0x1u) != 0u;
|
||||
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
|
||||
const auto* gather = Detail::FindOpcode(MIMG_GATHER_OPS, opcode);
|
||||
const auto* atomic = Detail::FindOpcode(MIMG_ATOMIC_OPS, opcode);
|
||||
const auto* sampled = sample != nullptr ? sample : gather;
|
||||
|
||||
inst.pc = pc;
|
||||
inst.word_count = word_count;
|
||||
@@ -351,11 +379,11 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
inst.data_dwords = d16 ? (inst.data_components + 1u) / 2u : inst.data_components;
|
||||
inst.glc = ((word0 >> 13u) & 1u) != 0;
|
||||
inst.slc = ((word0 >> 25u) & 1u) != 0;
|
||||
inst.image_sample_flags = DecodeMimgSampleFlags(sample, gather);
|
||||
inst.image_sample_flags = sampled != nullptr ? sampled->flags : 0u;
|
||||
if (a16) {
|
||||
inst.image_sample_flags |= ImageSampleFlagA16;
|
||||
}
|
||||
if ((sample != nullptr || gather != nullptr) && (word0 & (1u << 12u)) != 0u) {
|
||||
if (sampled != nullptr && (word0 & (1u << 12u)) != 0u) {
|
||||
inst.image_sample_flags |= ImageSampleFlagUnnormalized;
|
||||
}
|
||||
inst.image_dimension = dimension;
|
||||
@@ -364,8 +392,7 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
for (uint32_t i = 0; i < nsa_dwords * 4u; i++) {
|
||||
inst.image_nsa_addr[i] = (code[word_index + 2u + i / 4u] >> ((i % 4u) * 8u)) & 0xffu;
|
||||
}
|
||||
inst.image_address_components =
|
||||
DecodeMimgAddressComponents(opcode, dimension, sample, gather, atomic);
|
||||
inst.image_address_components = DecodeMimgAddressComponents(opcode, dimension, sampled, atomic);
|
||||
SetRawWords(inst, code, word_index, word_count);
|
||||
|
||||
if (inst.opcode == Opcode::UNSUPPORTED) {
|
||||
@@ -389,8 +416,8 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool supports_d16 = sample != nullptr || gather != nullptr || opcode == 0x00u ||
|
||||
opcode == 0x01u || opcode == 0x08u || opcode == 0x09u;
|
||||
const bool supports_d16 = sampled != nullptr || opcode == 0x00u || opcode == 0x01u ||
|
||||
opcode == 0x08u || opcode == 0x09u;
|
||||
if (d16 && !supports_d16) {
|
||||
SetUnsupported(inst, Family::MIMG, opcode, "MIMG opcode does not support D16 data");
|
||||
}
|
||||
@@ -407,9 +434,10 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
inst.src_count = 3;
|
||||
}
|
||||
|
||||
const char* MimgSampleOpcodeName(uint32_t opcode) {
|
||||
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
|
||||
return sample != nullptr ? sample->name : nullptr;
|
||||
const char* MimgSampledOpcodeName(uint32_t opcode) {
|
||||
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
|
||||
const auto* sampled = sample != nullptr ? sample : Detail::FindOpcode(MIMG_GATHER_OPS, opcode);
|
||||
return sampled != nullptr ? sampled->name : nullptr;
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::Decoder
|
||||
@@ -8,15 +8,16 @@ namespace Libs::Graphics::ShaderRecompiler::Decoder {
|
||||
void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
|
||||
Instruction& inst);
|
||||
|
||||
const char* MimgSampleOpcodeName(uint32_t opcode);
|
||||
const char* MimgSampledOpcodeName(uint32_t opcode);
|
||||
|
||||
struct ImageAddressComponent {
|
||||
uint32_t bit_offset;
|
||||
uint32_t bit_width;
|
||||
};
|
||||
|
||||
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, uint32_t component);
|
||||
uint32_t ImageAddressDwordCount(uint32_t flags, uint32_t components);
|
||||
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, ImageDimension dimension,
|
||||
uint32_t component);
|
||||
uint32_t ImageAddressDwordCount(uint32_t flags, ImageDimension dimension, uint32_t components);
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::Decoder
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ constexpr MemoryOpcodeInfo SMEM_OPCODE_LIST[] = {
|
||||
{0x04u, Opcode::S_LOAD_DWORDX16, 16, 32}, {0x08u, Opcode::S_BUFFER_LOAD_DWORD, 1, 32},
|
||||
{0x09u, Opcode::S_BUFFER_LOAD_DWORDX2, 2, 32}, {0x0au, Opcode::S_BUFFER_LOAD_DWORDX4, 4, 32},
|
||||
{0x0bu, Opcode::S_BUFFER_LOAD_DWORDX8, 8, 32}, {0x0cu, Opcode::S_BUFFER_LOAD_DWORDX16, 16, 32},
|
||||
{0x25u, Opcode::S_MEMREALTIME, 2, 32},
|
||||
{0x20u, Opcode::S_DCACHE_INV, 0, 0}, {0x25u, Opcode::S_MEMREALTIME, 2, 32},
|
||||
};
|
||||
|
||||
constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
|
||||
@@ -69,6 +69,7 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
|
||||
{0x40u, Opcode::BUFFER_ATOMIC_FMAX, 1, 32},
|
||||
{0x50u, Opcode::BUFFER_ATOMIC_SWAP_X2, 2, 32},
|
||||
{0x52u, Opcode::BUFFER_ATOMIC_ADD_X2, 2, 32},
|
||||
{0x56u, Opcode::BUFFER_ATOMIC_UMIN_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},
|
||||
@@ -95,14 +96,14 @@ constexpr MemoryOpcodeInfo MTBUF_OPCODE_LIST[] = {
|
||||
};
|
||||
|
||||
constexpr MemoryOpcodeInfo FLAT_OPCODE_LIST[] = {
|
||||
{0x08u, Opcode::FLAT_LOAD_UBYTE, 1, 8}, {0x09u, Opcode::FLAT_LOAD_SBYTE, 1, 8, true},
|
||||
{0x0au, Opcode::FLAT_LOAD_USHORT, 1, 16}, {0x0bu, Opcode::FLAT_LOAD_SSHORT, 1, 16, true},
|
||||
{0x0cu, Opcode::FLAT_LOAD_DWORD, 1, 32}, {0x0du, Opcode::FLAT_LOAD_DWORDX2, 2, 32},
|
||||
{0x0eu, Opcode::FLAT_LOAD_DWORDX4, 4, 32}, {0x0fu, Opcode::FLAT_LOAD_DWORDX3, 3, 32},
|
||||
{0x18u, Opcode::FLAT_STORE_BYTE, 1, 8}, {0x1au, Opcode::FLAT_STORE_SHORT, 1, 16},
|
||||
{0x1cu, Opcode::FLAT_STORE_DWORD, 1, 32}, {0x1du, Opcode::FLAT_STORE_DWORDX2, 2, 32},
|
||||
{0x1eu, Opcode::FLAT_STORE_DWORDX4, 4, 32}, {0x1fu, Opcode::FLAT_STORE_DWORDX3, 3, 32},
|
||||
{0x24u, Opcode::FLAT_LOAD_SHORT_D16, 1, 16},
|
||||
{0x08u, Opcode::FLAT_LOAD_UBYTE, 1, 8}, {0x09u, Opcode::FLAT_LOAD_SBYTE, 1, 8, true},
|
||||
{0x0au, Opcode::FLAT_LOAD_USHORT, 1, 16}, {0x0bu, Opcode::FLAT_LOAD_SSHORT, 1, 16, true},
|
||||
{0x0cu, Opcode::FLAT_LOAD_DWORD, 1, 32}, {0x0du, Opcode::FLAT_LOAD_DWORDX2, 2, 32},
|
||||
{0x0eu, Opcode::FLAT_LOAD_DWORDX4, 4, 32}, {0x0fu, Opcode::FLAT_LOAD_DWORDX3, 3, 32},
|
||||
{0x18u, Opcode::FLAT_STORE_BYTE, 1, 8}, {0x1au, Opcode::FLAT_STORE_SHORT, 1, 16},
|
||||
{0x1cu, Opcode::FLAT_STORE_DWORD, 1, 32}, {0x1du, Opcode::FLAT_STORE_DWORDX2, 2, 32},
|
||||
{0x1eu, Opcode::FLAT_STORE_DWORDX4, 4, 32}, {0x1fu, Opcode::FLAT_STORE_DWORDX3, 3, 32},
|
||||
{0x24u, Opcode::FLAT_LOAD_SHORT_D16, 1, 16}, {0x25u, Opcode::FLAT_LOAD_SHORT_D16_HI, 1, 16},
|
||||
};
|
||||
|
||||
constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
|
||||
@@ -285,6 +286,9 @@ void DecodeSmem(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
if (inst.opcode == Opcode::UNSUPPORTED) {
|
||||
SetUnsupported(inst, Family::SMEM, opcode, "SMEM opcode is not implemented");
|
||||
}
|
||||
if (inst.opcode == Opcode::S_DCACHE_INV) {
|
||||
return;
|
||||
}
|
||||
|
||||
DecodeScalarDestination(sdst, pc, inst.dst);
|
||||
if (inst.opcode == Opcode::S_MEMREALTIME) {
|
||||
@@ -419,9 +423,10 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
}
|
||||
|
||||
DecodeVectorGpr(IsFlatStoreOpcode(inst.opcode) ? data : vdst, inst.dst);
|
||||
if (inst.opcode == Opcode::FLAT_LOAD_SHORT_D16) {
|
||||
// D16 loads reuse partial destinations to preserve the untouched high half.
|
||||
inst.dst.sdwa_sel = 4u;
|
||||
if (inst.opcode == Opcode::FLAT_LOAD_SHORT_D16 ||
|
||||
inst.opcode == Opcode::FLAT_LOAD_SHORT_D16_HI) {
|
||||
// D16 loads reuse partial destinations to preserve the untouched half.
|
||||
inst.dst.sdwa_sel = inst.opcode == Opcode::FLAT_LOAD_SHORT_D16_HI ? 5u : 4u;
|
||||
}
|
||||
DecodeVectorGpr(addr, inst.src0);
|
||||
inst.src_count = 1;
|
||||
|
||||
@@ -123,10 +123,9 @@ std::string ImageSampleFlagsToString(uint32_t flags) {
|
||||
}
|
||||
|
||||
std::string FormatMimg(const Instruction& inst) {
|
||||
const char* sample_name =
|
||||
inst.opcode == Opcode::IMAGE_SAMPLE ? MimgSampleOpcodeName(inst.opcode_id) : nullptr;
|
||||
const char* sampled_name = MimgSampledOpcodeName(inst.opcode_id);
|
||||
const std::string_view name =
|
||||
sample_name != nullptr ? sample_name : magic_enum::enum_name(inst.opcode);
|
||||
sampled_name != nullptr ? sampled_name : magic_enum::enum_name(inst.opcode);
|
||||
std::string text =
|
||||
fmt::format("0x{:08x}: {} {}, {}, {}, {} ; dmask=0x{:x} image_dim={}", inst.pc, name,
|
||||
OperandToString(inst.dst).c_str(), OperandToString(inst.src0).c_str(),
|
||||
@@ -138,22 +137,10 @@ std::string FormatMimg(const Instruction& inst) {
|
||||
if (inst.image_r128) {
|
||||
text += " r128=1";
|
||||
}
|
||||
switch (inst.opcode) {
|
||||
case Opcode::IMAGE_SAMPLE:
|
||||
case Opcode::IMAGE_GATHER4_L:
|
||||
case Opcode::IMAGE_GATHER4_LZ:
|
||||
case Opcode::IMAGE_GATHER4_C:
|
||||
case Opcode::IMAGE_GATHER4_C_L:
|
||||
case Opcode::IMAGE_GATHER4_C_LZ:
|
||||
case Opcode::IMAGE_GATHER4_LZ_O:
|
||||
case Opcode::IMAGE_GATHER4_C_O:
|
||||
case Opcode::IMAGE_GATHER4_C_LZ_O:
|
||||
case Opcode::IMAGE_GATHER4H:
|
||||
text += fmt::format(" sample_flags={} addr_components={}",
|
||||
ImageSampleFlagsToString(inst.image_sample_flags).c_str(),
|
||||
inst.image_address_components);
|
||||
break;
|
||||
default: break;
|
||||
if (sampled_name != nullptr) {
|
||||
text += fmt::format(" sample_flags={} addr_components={}",
|
||||
ImageSampleFlagsToString(inst.image_sample_flags).c_str(),
|
||||
inst.image_address_components);
|
||||
}
|
||||
if (inst.image_nsa_dwords != 0) {
|
||||
text += fmt::format(" nsa_dwords={} nsa_addr=", inst.image_nsa_dwords);
|
||||
@@ -258,7 +245,9 @@ void DecodeScalarSource(uint32_t code, uint32_t pc, Operand& operand) {
|
||||
case 126u: operand.kind = OperandKind::ExecLo; return;
|
||||
case 127u: operand.kind = OperandKind::ExecHi; return;
|
||||
case 235u: operand.kind = OperandKind::SharedBase; return;
|
||||
case 236u: operand.kind = OperandKind::SharedLimit; return;
|
||||
case 237u: operand.kind = OperandKind::PrivateBase; return;
|
||||
case 238u: operand.kind = OperandKind::PrivateLimit; return;
|
||||
case 239u: operand.kind = OperandKind::PopsExitingWaveId; return;
|
||||
case 248u:
|
||||
operand.kind = OperandKind::FloatInlineConstant;
|
||||
@@ -459,7 +448,9 @@ std::string OperandToString(const Operand& operand) {
|
||||
case OperandKind::M0: text = "m0"; break;
|
||||
case OperandKind::PopsExitingWaveId: text = "pops_exiting_wave_id"; break;
|
||||
case OperandKind::SharedBase: text = "shared_base"; break;
|
||||
case OperandKind::SharedLimit: text = "shared_limit"; break;
|
||||
case OperandKind::PrivateBase: text = "private_base"; break;
|
||||
case OperandKind::PrivateLimit: text = "private_limit"; break;
|
||||
case OperandKind::Null: text = "null"; break;
|
||||
default: text = "unknown"; break;
|
||||
}
|
||||
@@ -566,6 +557,7 @@ 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::S_DCACHE_INV:
|
||||
case Opcode::BUFFER_GL0_INV:
|
||||
case Opcode::BUFFER_GL1_INV:
|
||||
return fmt::format("0x{:08x}: {}", inst.pc, magic_enum::enum_name(inst.opcode));
|
||||
@@ -612,15 +604,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::IMAGE_LOAD_MIP:
|
||||
case Opcode::IMAGE_GET_RESINFO:
|
||||
case Opcode::IMAGE_GET_LOD:
|
||||
case Opcode::IMAGE_GATHER4_L:
|
||||
case Opcode::IMAGE_GATHER4_LZ:
|
||||
case Opcode::IMAGE_GATHER4_C:
|
||||
case Opcode::IMAGE_GATHER4_C_L:
|
||||
case Opcode::IMAGE_GATHER4_C_LZ:
|
||||
case Opcode::IMAGE_GATHER4_LZ_O:
|
||||
case Opcode::IMAGE_GATHER4_C_O:
|
||||
case Opcode::IMAGE_GATHER4_C_LZ_O:
|
||||
case Opcode::IMAGE_GATHER4H: return WithUnsupportedReason(inst, FormatMimg(inst));
|
||||
case Opcode::IMAGE_GATHER4: return WithUnsupportedReason(inst, FormatMimg(inst));
|
||||
case Opcode::S_LOAD_DWORD:
|
||||
case Opcode::S_LOAD_DWORDX2:
|
||||
case Opcode::S_LOAD_DWORDX4:
|
||||
@@ -681,6 +665,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::BUFFER_ATOMIC_SUB:
|
||||
case Opcode::BUFFER_ATOMIC_SMIN:
|
||||
case Opcode::BUFFER_ATOMIC_UMIN:
|
||||
case Opcode::BUFFER_ATOMIC_UMIN_X2:
|
||||
case Opcode::BUFFER_ATOMIC_SMAX:
|
||||
case Opcode::BUFFER_ATOMIC_UMAX:
|
||||
case Opcode::BUFFER_ATOMIC_AND:
|
||||
@@ -697,6 +682,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::FLAT_LOAD_USHORT:
|
||||
case Opcode::FLAT_LOAD_SSHORT:
|
||||
case Opcode::FLAT_LOAD_SHORT_D16:
|
||||
case Opcode::FLAT_LOAD_SHORT_D16_HI:
|
||||
case Opcode::FLAT_LOAD_DWORD:
|
||||
case Opcode::FLAT_LOAD_DWORDX2:
|
||||
case Opcode::FLAT_LOAD_DWORDX3:
|
||||
|
||||
@@ -544,6 +544,7 @@ enum class Opcode {
|
||||
S_BUFFER_LOAD_DWORDX8,
|
||||
S_BUFFER_LOAD_DWORDX16,
|
||||
S_MEMREALTIME,
|
||||
S_DCACHE_INV,
|
||||
BUFFER_LOAD_FORMAT_X,
|
||||
BUFFER_LOAD_FORMAT_XY,
|
||||
BUFFER_LOAD_FORMAT_XYZ,
|
||||
@@ -596,6 +597,7 @@ enum class Opcode {
|
||||
BUFFER_ATOMIC_SUB,
|
||||
BUFFER_ATOMIC_SMIN,
|
||||
BUFFER_ATOMIC_UMIN,
|
||||
BUFFER_ATOMIC_UMIN_X2,
|
||||
BUFFER_ATOMIC_SMAX,
|
||||
BUFFER_ATOMIC_UMAX,
|
||||
BUFFER_ATOMIC_AND,
|
||||
@@ -612,6 +614,7 @@ enum class Opcode {
|
||||
FLAT_LOAD_USHORT,
|
||||
FLAT_LOAD_SSHORT,
|
||||
FLAT_LOAD_SHORT_D16,
|
||||
FLAT_LOAD_SHORT_D16_HI,
|
||||
FLAT_LOAD_DWORD,
|
||||
FLAT_LOAD_DWORDX2,
|
||||
FLAT_LOAD_DWORDX3,
|
||||
@@ -707,15 +710,7 @@ enum class Opcode {
|
||||
IMAGE_ATOMIC_FMIN,
|
||||
IMAGE_ATOMIC_FMAX,
|
||||
IMAGE_SAMPLE,
|
||||
IMAGE_GATHER4_L,
|
||||
IMAGE_GATHER4_LZ,
|
||||
IMAGE_GATHER4_C,
|
||||
IMAGE_GATHER4_C_L,
|
||||
IMAGE_GATHER4_C_LZ,
|
||||
IMAGE_GATHER4_LZ_O,
|
||||
IMAGE_GATHER4_C_O,
|
||||
IMAGE_GATHER4_C_LZ_O,
|
||||
IMAGE_GATHER4H,
|
||||
IMAGE_GATHER4,
|
||||
V_INTERP_P1_F32,
|
||||
V_INTERP_P2_F32,
|
||||
V_INTERP_MOV_F32,
|
||||
@@ -766,7 +761,9 @@ enum class OperandKind {
|
||||
M0,
|
||||
PopsExitingWaveId,
|
||||
SharedBase,
|
||||
SharedLimit,
|
||||
PrivateBase,
|
||||
PrivateLimit,
|
||||
Null,
|
||||
Vgpr,
|
||||
};
|
||||
|
||||
@@ -5,30 +5,20 @@
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::Frontend {
|
||||
|
||||
void Translator::EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar,
|
||||
bool cmpx) {
|
||||
void Translator::EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar) {
|
||||
if (scalar) {
|
||||
ir.SetScc(value);
|
||||
return;
|
||||
}
|
||||
const auto masked = ir.LogicalAnd(ir.GetExec(), value);
|
||||
if (cmpx) {
|
||||
const auto mask = BallotMask(masked);
|
||||
ir.SetExec(masked);
|
||||
ir.SetExecLo(mask[0]);
|
||||
ir.SetExecHi(mask[1]);
|
||||
return;
|
||||
}
|
||||
WriteMask(inst.dst, masked);
|
||||
WriteVectorMask(inst.dst, value);
|
||||
}
|
||||
|
||||
void Translator::EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar,
|
||||
bool cmpx) {
|
||||
EmitCompareResult(inst, IR::U1(IR::Value(value)), scalar, cmpx);
|
||||
void Translator::EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar) {
|
||||
EmitCompareResult(inst, IR::U1(IR::Value(value)), scalar);
|
||||
}
|
||||
|
||||
void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
|
||||
IR::Type type, bool scalar, bool cmpx, bool swap_operands) {
|
||||
IR::Type type, bool scalar, bool swap_operands) {
|
||||
// The equality compares share their IR opcode with the unsigned ones.
|
||||
const bool signed_64 = opcode == IR::ValueOpcode::SLessThan64 ||
|
||||
opcode == IR::ValueOpcode::SLessThanEqual64 ||
|
||||
@@ -49,18 +39,18 @@ void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueO
|
||||
if (swap_operands) {
|
||||
std::swap(lhs, rhs);
|
||||
}
|
||||
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), scalar, cmpx);
|
||||
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), scalar);
|
||||
}
|
||||
|
||||
void Translator::EmitInteger16Compare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
|
||||
bool signed_value, bool cmpx) {
|
||||
bool signed_value) {
|
||||
const auto lhs = ReadU16AsU32(inst.src0, signed_value);
|
||||
const auto rhs = ReadU16AsU32(inst.src1, signed_value);
|
||||
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), false, cmpx);
|
||||
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), false);
|
||||
}
|
||||
|
||||
void Translator::EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
|
||||
bool half, bool cmpx) {
|
||||
bool half) {
|
||||
const auto type = IR::ArgTypeOf(opcode, 0);
|
||||
const auto lhs =
|
||||
half ? IR::Value(ReadF16AsF32(inst.src0)) : ReadOperand(inst.src0, type);
|
||||
@@ -68,10 +58,10 @@ void Translator::EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpc
|
||||
half ? IR::Value(ReadF16AsF32(inst.src1)) : ReadOperand(inst.src1, type);
|
||||
const IR::FPCompareFlags flags{
|
||||
.flush_input_denorms = !half && type == IR::Type::F32 && flush_f32_inputs};
|
||||
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs}, flags)), false, cmpx);
|
||||
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs}, flags)), false);
|
||||
}
|
||||
|
||||
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx,
|
||||
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered,
|
||||
bool is64) {
|
||||
const auto type = is64 ? IR::Type::F64 : IR::Type::F32;
|
||||
const auto is_nan_op = is64 ? IR::ValueOpcode::FPIsNan64 : IR::ValueOpcode::FPIsNan32;
|
||||
@@ -79,16 +69,16 @@ void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool
|
||||
const auto rhs = ReadOperand(inst.src1, type);
|
||||
const auto unordered = ir.LogicalOr(IR::U1(ir.Emit(is_nan_op, {lhs})),
|
||||
IR::U1(ir.Emit(is_nan_op, {rhs})));
|
||||
EmitCompareResult(inst, ordered ? ir.LogicalNot(unordered) : unordered, false, cmpx);
|
||||
EmitCompareResult(inst, ordered ? ir.LogicalNot(unordered) : unordered, false);
|
||||
}
|
||||
|
||||
void Translator::EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half) {
|
||||
void Translator::EmitFloatClassCompare(const Decoder::Instruction& inst, bool half) {
|
||||
const auto value = half ? IR::Value(Read16LaneBits(inst.src0, false))
|
||||
: ReadOperand(inst.src0, IR::Type::F32);
|
||||
const auto mask = half ? IR::Value(Read16LaneBits(inst.src1, false))
|
||||
: ReadOperand(inst.src1, IR::Type::U32);
|
||||
const auto opcode = half ? IR::ValueOpcode::FPCmpClass16 : IR::ValueOpcode::FPCmpClass32;
|
||||
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {value, mask})), false, cmpx);
|
||||
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {value, mask})), false);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::Frontend
|
||||
@@ -51,33 +51,33 @@ void Translator::S_SUBVECTOR_LOOP(const Decoder::Instruction& inst, bool begin)
|
||||
|
||||
void Translator::S_SAVEEXEC(const Decoder::Instruction& inst, IR::ValueOpcode operation,
|
||||
bool negate_exec, bool negate_source, bool write_64) {
|
||||
if (!write_64) {
|
||||
// Read the encoded scalar word and preserve EXEC_HI, including in wave32.
|
||||
const auto old = ir.GetExecLo();
|
||||
const auto src = ReadU32(inst.src0);
|
||||
const auto combine = [&](IR::U32 old, IR::U32 src) {
|
||||
const auto lhs = negate_exec ? ir.BitwiseNot(old) : old;
|
||||
const auto rhs = negate_source ? ir.BitwiseNot(src) : src;
|
||||
IR::U32 result;
|
||||
switch (operation) {
|
||||
case IR::ValueOpcode::LogicalAnd: result = ir.BitwiseAnd(lhs, rhs); break;
|
||||
case IR::ValueOpcode::LogicalOr: result = ir.BitwiseOr(lhs, rhs); break;
|
||||
default: EXIT("unsupported SAVEEXEC operation");
|
||||
}
|
||||
return operation == IR::ValueOpcode::LogicalAnd ? ir.BitwiseAnd(lhs, rhs)
|
||||
: ir.BitwiseOr(lhs, rhs);
|
||||
};
|
||||
if (!write_64) {
|
||||
// Read the encoded scalar word and preserve EXEC_HI, including in wave32.
|
||||
const auto old = ir.GetExecLo();
|
||||
const auto result = combine(old, ReadU32(inst.src0));
|
||||
WriteRawU32(inst.dst, old);
|
||||
WriteRawU32(ConditionOperand(Decoder::OperandKind::ExecLo), result);
|
||||
ir.SetScc(ir.INotEqual(result, IR::U32(IR::Value(0u))));
|
||||
return;
|
||||
}
|
||||
const auto old = ir.GetExec();
|
||||
const auto src = ReadMask(inst.src0);
|
||||
const auto lhs = negate_exec ? ir.LogicalNot(old) : old;
|
||||
const auto rhs = negate_source ? ir.LogicalNot(src) : src;
|
||||
const auto result = IR::U1(ir.Emit(operation, {lhs, rhs}));
|
||||
WriteMask(inst.dst, old, true);
|
||||
const auto mask = BallotMask(result);
|
||||
ir.SetExec(result);
|
||||
ir.SetExecLo(mask[0]);
|
||||
ir.SetExecHi(mask[1]);
|
||||
// EXEC words combine bitwise; the per-lane EXEC combines the per-lane masks, so helper lanes
|
||||
// stay enabled.
|
||||
const auto old = ir.GetExec();
|
||||
const auto src = ReadMask(inst.src0);
|
||||
const auto lhs = negate_exec ? ir.LogicalNot(old) : old;
|
||||
const auto rhs = negate_source ? ir.LogicalNot(src) : src;
|
||||
const auto src_mask = ReadU32Pair(inst.src0);
|
||||
const std::array old_mask {ir.GetExecLo(), ir.GetExecHi()};
|
||||
const std::array mask {combine(old_mask[0], src_mask[0]), combine(old_mask[1], src_mask[1])};
|
||||
WriteMask(inst.dst, old, old_mask, true);
|
||||
WriteMask(ConditionOperand(Decoder::OperandKind::ExecLo), IR::U1(ir.Emit(operation, {lhs, rhs})),
|
||||
mask);
|
||||
ir.SetScc(ir.INotEqual(ir.BitwiseOr(mask[0], mask[1]), IR::U32(IR::Value(0u))));
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ void Translator::ADD_U32(const Decoder::Instruction& inst, bool vector, bool use
|
||||
}
|
||||
WriteOperand(DestinationOperand(inst), result);
|
||||
if (vector) {
|
||||
WriteMask(inst.dst2, ir.LogicalAnd(ir.GetExec(), carry));
|
||||
WriteVectorMask(inst.dst2, carry);
|
||||
} else {
|
||||
ir.SetScc(carry);
|
||||
}
|
||||
@@ -108,7 +108,7 @@ void Translator::SUB_U32(const Decoder::Instruction& inst, bool vector, bool rev
|
||||
const auto borrow = ir.UGreaterThan(rhs, lhs);
|
||||
WriteOperand(DestinationOperand(inst), result);
|
||||
if (vector) {
|
||||
WriteMask(inst.dst2, ir.LogicalAnd(ir.GetExec(), borrow));
|
||||
WriteVectorMask(inst.dst2, borrow);
|
||||
} else {
|
||||
ir.SetScc(borrow);
|
||||
}
|
||||
@@ -126,7 +126,7 @@ void Translator::SUBB_U32(const Decoder::Instruction& inst, bool vector, bool re
|
||||
const auto borrow = ir.LogicalOr(borrow0, borrow1);
|
||||
WriteOperand(DestinationOperand(inst), result);
|
||||
if (vector) {
|
||||
WriteMask(inst.dst2, ir.LogicalAnd(ir.GetExec(), borrow));
|
||||
WriteVectorMask(inst.dst2, borrow);
|
||||
} else {
|
||||
ir.SetScc(borrow);
|
||||
}
|
||||
@@ -465,9 +465,13 @@ void Translator::V_PERMLANE16_B32(const Decoder::Instruction& inst, bool x16) {
|
||||
.fetch_inactive = inst.dst.op_sel,
|
||||
.bound_control = inst.dst.op_sel_hi,
|
||||
};
|
||||
const auto result =
|
||||
ir.Emit(IR::ValueOpcode::Permlane16U32,
|
||||
{ReadU32(inst.src0), ReadU32(inst.src1), ReadU32(inst.src2), ir.GetExec()}, flags);
|
||||
// An EXEC-disabled source lane without FI disables the write unless BOUND_CTRL reads zero.
|
||||
const auto inactive = flags.fetch_inactive || flags.bound_control
|
||||
? IR::U32(IR::Value(0u))
|
||||
: ir.GetVectorReg(static_cast<IR::VectorReg>(inst.dst.reg));
|
||||
const auto result = ir.Emit(
|
||||
IR::ValueOpcode::Permlane16U32,
|
||||
{ReadU32(inst.src0), ReadU32(inst.src1), ReadU32(inst.src2), ir.GetExec(), inactive}, flags);
|
||||
auto dst = DestinationOperand(inst);
|
||||
dst.op_sel = false;
|
||||
dst.op_sel_hi = false;
|
||||
|
||||
@@ -49,18 +49,11 @@ void Translator::V_CVT_F16_16(const Decoder::Instruction& inst, bool signed_valu
|
||||
}
|
||||
|
||||
void Translator::V_CVT_16_F16(const Decoder::Instruction& inst, bool signed_value) {
|
||||
const auto value = ReadF16AsF32(inst.src0);
|
||||
// The 32-bit conversion already saturates and maps NaN to zero.
|
||||
if (signed_value) {
|
||||
const auto converted = ir.Emit(IR::ValueOpcode::ConvertS32F32, {value});
|
||||
const auto low = ir.Emit(IR::ValueOpcode::SMax32, {converted, IR::Value(0xffff8000u)});
|
||||
Write16Bits(DestinationOperand(inst),
|
||||
IR::U32(ir.Emit(IR::ValueOpcode::SMin32, {low, IR::Value(0x7fffu)})));
|
||||
return;
|
||||
}
|
||||
const auto converted = ir.Emit(IR::ValueOpcode::ConvertU32F32, {value});
|
||||
Write16Bits(DestinationOperand(inst),
|
||||
IR::U32(ir.Emit(IR::ValueOpcode::UMin32, {converted, IR::Value(0xffffu)})));
|
||||
const auto converted = IR::U32(
|
||||
ir.Emit(signed_value ? IR::ValueOpcode::ConvertS32F32 : IR::ValueOpcode::ConvertU32F32,
|
||||
{ReadF16AsF32(inst.src0)}));
|
||||
Write16Bits(DestinationOperand(inst), Saturate16(converted, signed_value));
|
||||
}
|
||||
|
||||
void Translator::V_CVT_RPI_I32_F32(const Decoder::Instruction& inst) {
|
||||
@@ -130,14 +123,30 @@ void Translator::V_CVT_PK_U8_F32(const Decoder::Instruction& inst) {
|
||||
{ReadU32(SourceAt(inst, 2)), byte, shift, IR::Value(8u)}));
|
||||
}
|
||||
|
||||
void Translator::V_CVT_PK_16_32(const Decoder::Instruction& inst, bool signed_value) {
|
||||
WriteOperand(DestinationOperand(inst),
|
||||
PackU16Lanes(Saturate16(ReadU32(inst.src0), signed_value),
|
||||
Saturate16(ReadU32(inst.src1), signed_value)));
|
||||
}
|
||||
|
||||
void Translator::V_PACK_B32_F16(const Decoder::Instruction& inst) {
|
||||
WriteOperand(DestinationOperand(inst),
|
||||
PackU16Lanes(Read16LaneBits(inst.src0, false), Read16LaneBits(inst.src1, false)));
|
||||
}
|
||||
|
||||
IR::U32 Translator::PackU16Lanes(IR::U32 low, IR::U32 high) {
|
||||
// A zero high lane zero-extends; descriptor tracking bounds a mask, not a bitfield insert.
|
||||
if (high == IR::Value(0u)) return ir.BitwiseAnd(low, IR::U32(IR::Value(0xffffu)));
|
||||
return IR::U32(
|
||||
ir.Emit(IR::ValueOpcode::BitFieldInsert, {low, high, IR::Value(16u), IR::Value(16u)}));
|
||||
}
|
||||
|
||||
IR::U32 Translator::Saturate16(IR::U32 value, bool signed_value) {
|
||||
if (!signed_value) {
|
||||
return IR::U32(ir.Emit(IR::ValueOpcode::UMin32, {value, IR::Value(0xffffu)}));
|
||||
}
|
||||
const auto low = ir.Emit(IR::ValueOpcode::SMax32, {value, IR::Value(0xffff8000u)});
|
||||
return IR::U32(ir.Emit(IR::ValueOpcode::SMin32, {low, IR::Value(0x7fffu)}));
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::Frontend
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "graphics/shader/recompiler/frontend/translate/Translator.h"
|
||||
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <utility>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::Frontend {
|
||||
@@ -118,6 +119,33 @@ void Translator::FloatUnary(const Decoder::Instruction& inst, IR::ValueOpcode op
|
||||
WriteOperand(DestinationOperand(inst), ir.Emit(opcode, {ReadOperand(inst.src0, type)}));
|
||||
}
|
||||
|
||||
void Translator::V_FRACT(const Decoder::Instruction& inst, IR::Type type) {
|
||||
// x - floor(x) rounds to 1.0 for a tiny negative x; v_fract returns the largest value below
|
||||
// one instead. The ordered compare lets NaN through.
|
||||
const auto destination = DestinationOperand(inst);
|
||||
if (type == IR::Type::F64) {
|
||||
const auto result = ir.Emit(IR::ValueOpcode::FPFract64, {ReadOperand(inst.src0, type)});
|
||||
const auto below_one = ir.Emit(IR::ValueOpcode::BitCastF64U64,
|
||||
{IR::Value(uint64_t {0x3fefffffffffffff})});
|
||||
const auto above = ir.Emit(IR::ValueOpcode::FPOrdGreaterThan64, {result, below_one});
|
||||
WriteOperand(destination, ir.Emit(IR::ValueOpcode::SelectF64, {above, below_one, result}));
|
||||
return;
|
||||
}
|
||||
const bool half = type == IR::Type::F16;
|
||||
const auto source = half ? ReadF16AsF32(inst.src0) : IR::F32(ReadOperand(inst.src0, type));
|
||||
const auto result = IR::F32(ir.Emit(IR::ValueOpcode::FPFract32, {source}));
|
||||
// Clamp in the destination precision: 0x3f7fe000 is the half 0x3bff.
|
||||
const auto below_one =
|
||||
IR::F32(IR::Value::F32(std::bit_cast<float>(half ? 0x3f7fe000u : 0x3f7fffffu)));
|
||||
const auto above = IR::U1(ir.Emit(IR::ValueOpcode::FPOrdGreaterThan32, {result, below_one}));
|
||||
const auto clamped = SelectF32(above, below_one, result);
|
||||
if (half) {
|
||||
WriteF16(destination, clamped);
|
||||
} else {
|
||||
WriteOperand(destination, clamped);
|
||||
}
|
||||
}
|
||||
|
||||
void Translator::FloatBinary(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
|
||||
bool reverse) {
|
||||
std::array<IR::Value, 2> args;
|
||||
|
||||
@@ -25,8 +25,7 @@ void Translator::V_MAD_I16(const Decoder::Instruction& inst) {
|
||||
const auto rhs = ReadU16AsU32(inst.src1, true);
|
||||
auto result = ir.IAdd(ir.IMul(lhs, rhs), ReadU16AsU32(inst.src2, true));
|
||||
if (inst.dst.clamp) {
|
||||
result = IR::U32(ir.Emit(IR::ValueOpcode::SMax32, {result, IR::Value(0xffff8000u)}));
|
||||
result = IR::U32(ir.Emit(IR::ValueOpcode::SMin32, {result, IR::Value(0x7fffu)}));
|
||||
result = Saturate16(result, true);
|
||||
}
|
||||
Write16Bits(DestinationOperand(inst), result);
|
||||
}
|
||||
@@ -108,19 +107,7 @@ void Translator::S_U64_MASK(const Decoder::Instruction& inst, IR::ValueOpcode lo
|
||||
default: return false;
|
||||
}
|
||||
};
|
||||
if (is_exec_or_vcc(inst.dst) || is_exec_or_vcc(inst.src0) ||
|
||||
(inst.src_count > 1u && is_exec_or_vcc(inst.src1))) {
|
||||
const auto mask = WriteMask(inst.dst, invocation_result, true);
|
||||
ir.SetScc(ir.INotEqual(ir.BitwiseOr(mask[0], mask[1]), IR::U32(IR::Value(0u))));
|
||||
return;
|
||||
}
|
||||
|
||||
// Descriptor tracking reasons about each scalar word independently.
|
||||
const auto lhs = ReadU32Pair(inst.src0);
|
||||
auto mask_valid = ReadMaskValid(inst.src0);
|
||||
if (inst.src_count > 1u) {
|
||||
mask_valid = ir.LogicalAnd(mask_valid, ReadMaskValid(inst.src1));
|
||||
}
|
||||
const auto lhs = ReadU32Pair(inst.src0);
|
||||
std::array<IR::U32, 2> result;
|
||||
if (unary) {
|
||||
result = {ir.BitwiseNot(lhs[0]), ir.BitwiseNot(lhs[1])};
|
||||
@@ -133,11 +120,21 @@ void Translator::S_U64_MASK(const Decoder::Instruction& inst, IR::ValueOpcode lo
|
||||
result[component] = negate_result ? ir.BitwiseNot(IR::U32(value)) : IR::U32(value);
|
||||
}
|
||||
}
|
||||
WriteU32Pair(inst.dst, result);
|
||||
if (inst.dst.kind == Decoder::OperandKind::Sgpr) {
|
||||
const auto dst = static_cast<IR::ScalarReg>(inst.dst.reg);
|
||||
ir.SetThreadBitScalarReg(dst, invocation_result);
|
||||
ir.SetScalarMaskTag(dst, mask_valid);
|
||||
if (is_exec_or_vcc(inst.dst) || is_exec_or_vcc(inst.src0) ||
|
||||
(inst.src_count > 1u && is_exec_or_vcc(inst.src1))) {
|
||||
WriteMask(inst.dst, invocation_result, result, true);
|
||||
} else {
|
||||
// Descriptor tracking reasons about each scalar word independently.
|
||||
auto mask_valid = ReadMaskValid(inst.src0);
|
||||
if (inst.src_count > 1u) {
|
||||
mask_valid = ir.LogicalAnd(mask_valid, ReadMaskValid(inst.src1));
|
||||
}
|
||||
WriteU32Pair(inst.dst, result);
|
||||
if (inst.dst.kind == Decoder::OperandKind::Sgpr) {
|
||||
const auto dst = static_cast<IR::ScalarReg>(inst.dst.reg);
|
||||
ir.SetThreadBitScalarReg(dst, invocation_result);
|
||||
ir.SetScalarMaskTag(dst, mask_valid);
|
||||
}
|
||||
}
|
||||
ir.SetScc(ir.INotEqual(ir.BitwiseOr(result[0], result[1]), IR::U32(IR::Value(0u))));
|
||||
}
|
||||
@@ -293,7 +290,8 @@ void Translator::V_MAD_U64_U32(const Decoder::Instruction& inst) {
|
||||
if (inst.dst2.kind != Decoder::OperandKind::Null &&
|
||||
inst.dst2.kind != Decoder::OperandKind::Unknown) {
|
||||
// The 32x32 product fits in 64 bits, so only the addition can carry.
|
||||
WriteMask(inst.dst2, IR::U1(ir.Emit(IR::ValueOpcode::ULessThan64, {result, product})));
|
||||
const auto carry = IR::U1(ir.Emit(IR::ValueOpcode::ULessThan64, {result, product}));
|
||||
WriteVectorMask(inst.dst2, carry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,7 +471,7 @@ void Translator::S_BITCMP_B32(const Decoder::Instruction& inst, bool expected) {
|
||||
const auto offset = ir.BitwiseAnd(ReadU32(inst.src1), IR::U32(IR::Value(31u)));
|
||||
const auto bit =
|
||||
IR::U32(ir.Emit(IR::ValueOpcode::BitFieldUExtract, {value, offset, IR::Value(1u)}));
|
||||
WriteCompareResult(inst.dst, ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
|
||||
ir.SetScc(ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
|
||||
}
|
||||
|
||||
void Translator::S_BITCMP_B64(const Decoder::Instruction& inst, bool expected) {
|
||||
@@ -484,7 +482,7 @@ void Translator::S_BITCMP_B64(const Decoder::Instruction& inst, bool expected) {
|
||||
ir.Select(ir.ULessThan(offset, IR::U32(IR::Value(32u))), value[0], value[1]);
|
||||
const auto bit =
|
||||
IR::U32(ir.Emit(IR::ValueOpcode::BitFieldUExtract, {word, word_bit, IR::Value(1u)}));
|
||||
WriteCompareResult(inst.dst, ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
|
||||
ir.SetScc(ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
|
||||
}
|
||||
|
||||
void Translator::V_ALIGN_B32(const Decoder::Instruction& inst, bool byte_offset) {
|
||||
|
||||
@@ -322,8 +322,8 @@ IR::Value Translator::MakeImageAddress(const Decoder::Instruction& inst,
|
||||
const Decoder::Operand& base) {
|
||||
std::array<IR::Value, 13> components {};
|
||||
components.fill(IR::Value(0u));
|
||||
const auto count =
|
||||
Decoder::ImageAddressDwordCount(inst.image_sample_flags, inst.image_address_components);
|
||||
const auto count = Decoder::ImageAddressDwordCount(
|
||||
inst.image_sample_flags, inst.image_dimension, inst.image_address_components);
|
||||
EXIT_IF(count > components.size());
|
||||
const auto nsa_components =
|
||||
std::min(inst.image_nsa_dwords * 4u, Decoder::MaxImageNsaAddressComponents);
|
||||
@@ -863,9 +863,10 @@ void Translator::DS_WRITE2(const Decoder::Instruction& inst) {
|
||||
|
||||
void Translator::DS_APPEND_CONSUME(const Decoder::Instruction& inst, IR::ValueOpcode opcode) {
|
||||
const auto memory = MemoryInfoFromDecoded(inst);
|
||||
WriteOperand(inst.dst, ir.Emit(opcode,
|
||||
{ReadU32(MemorySourceAt(inst, 0)), ir.GetExec()},
|
||||
AddMemoryInfo(memory, inst.pc)));
|
||||
const auto exec = ExecMask();
|
||||
WriteOperand(inst.dst,
|
||||
ir.Emit(opcode, {ReadU32(MemorySourceAt(inst, 0)), exec[0], exec[1], ir.GetExec()},
|
||||
AddMemoryInfo(memory, inst.pc)));
|
||||
}
|
||||
|
||||
void Translator::DS_ADDTID(const Decoder::Instruction& inst, bool write) {
|
||||
@@ -901,10 +902,7 @@ void Translator::DS_ORDERED_COUNT(const Decoder::Instruction& inst) {
|
||||
const auto packer = ir.BitwiseAnd(m0, IR::U32(IR::Value(0xfu)));
|
||||
address = ir.IAdd(address, ir.ShiftLeftLogical(packer, IR::U32(IR::Value(2u))));
|
||||
}
|
||||
const auto exec = ir.GetExec();
|
||||
const auto ballot = ir.Emit(IR::ValueOpcode::Ballot, {exec});
|
||||
const auto low = ir.CompositeExtract(ballot, 0u);
|
||||
const auto high = ir.CompositeExtract(ballot, 1u);
|
||||
const auto [low, high] = ExecMask();
|
||||
const auto active = ir.INotEqual(ir.BitwiseOr(low, high), IR::U32(IR::Value(0u)));
|
||||
const auto first = ir.Select(
|
||||
ir.INotEqual(low, IR::U32(IR::Value(0u))),
|
||||
@@ -943,6 +941,8 @@ void Translator::DS_PERMUTE(const Decoder::Instruction& inst, bool backward) {
|
||||
|
||||
void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
switch (inst.opcode) {
|
||||
// Translated scalar loads have no cache of their own to invalidate.
|
||||
case Decoder::Opcode::S_DCACHE_INV: return;
|
||||
case Decoder::Opcode::BUFFER_GL0_INV:
|
||||
case Decoder::Opcode::BUFFER_GL1_INV:
|
||||
ir.Emit(IR::ValueOpcode::DataCacheInvalidate);
|
||||
@@ -1031,6 +1031,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicSMin32);
|
||||
case Decoder::Opcode::BUFFER_ATOMIC_UMIN:
|
||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicUMin32);
|
||||
case Decoder::Opcode::BUFFER_ATOMIC_UMIN_X2:
|
||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicUMin64);
|
||||
case Decoder::Opcode::BUFFER_ATOMIC_SMAX:
|
||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicSMax32);
|
||||
case Decoder::Opcode::BUFFER_ATOMIC_UMAX:
|
||||
@@ -1148,6 +1150,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
case Decoder::Opcode::FLAT_LOAD_USHORT:
|
||||
case Decoder::Opcode::FLAT_LOAD_SSHORT:
|
||||
case Decoder::Opcode::FLAT_LOAD_SHORT_D16:
|
||||
case Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI:
|
||||
case Decoder::Opcode::FLAT_LOAD_DWORD:
|
||||
case Decoder::Opcode::FLAT_LOAD_DWORDX2:
|
||||
case Decoder::Opcode::FLAT_LOAD_DWORDX3:
|
||||
@@ -1167,15 +1170,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
case Decoder::Opcode::IMAGE_STORE:
|
||||
case Decoder::Opcode::IMAGE_STORE_MIP: return IMAGE_STORE(inst);
|
||||
case Decoder::Opcode::IMAGE_SAMPLE: return IMAGE_SAMPLE(inst);
|
||||
case Decoder::Opcode::IMAGE_GATHER4_L:
|
||||
case Decoder::Opcode::IMAGE_GATHER4_LZ:
|
||||
case Decoder::Opcode::IMAGE_GATHER4_C:
|
||||
case Decoder::Opcode::IMAGE_GATHER4_C_L:
|
||||
case Decoder::Opcode::IMAGE_GATHER4_C_LZ:
|
||||
case Decoder::Opcode::IMAGE_GATHER4_LZ_O:
|
||||
case Decoder::Opcode::IMAGE_GATHER4_C_O:
|
||||
case Decoder::Opcode::IMAGE_GATHER4_C_LZ_O:
|
||||
case Decoder::Opcode::IMAGE_GATHER4H: return IMAGE_GATHER(inst);
|
||||
case Decoder::Opcode::IMAGE_GATHER4: return IMAGE_GATHER(inst);
|
||||
|
||||
case Decoder::Opcode::DS_MIN_F32:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicFMin32, false);
|
||||
|
||||
@@ -73,43 +73,41 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
|
||||
|
||||
case O::S_CMP_EQ_U32:
|
||||
case O::S_CMP_EQ_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, true, false);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, true);
|
||||
return;
|
||||
case O::S_CMP_LG_U32:
|
||||
case O::S_CMP_LG_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, true, false);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, true);
|
||||
return;
|
||||
case O::S_CMP_GT_U32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, true, false);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, true);
|
||||
return;
|
||||
case O::S_CMP_GE_U32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, true,
|
||||
false);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, true);
|
||||
return;
|
||||
case O::S_CMP_LT_U32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, true, false);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, true);
|
||||
return;
|
||||
case O::S_CMP_LE_U32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, true, false);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, true);
|
||||
return;
|
||||
case O::S_CMP_GT_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, true, false);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, true);
|
||||
return;
|
||||
case O::S_CMP_GE_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, true,
|
||||
false);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, true);
|
||||
return;
|
||||
case O::S_CMP_LT_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, true, false);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, true);
|
||||
return;
|
||||
case O::S_CMP_LE_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, true, false);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, true);
|
||||
return;
|
||||
case O::S_CMP_EQ_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, true, false);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, true);
|
||||
return;
|
||||
case O::S_CMP_LG_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, true, false);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, true);
|
||||
return;
|
||||
|
||||
case O::S_AND_B64:
|
||||
|
||||
@@ -129,6 +129,20 @@ std::array<IR::U32, 2> Translator::BallotMask(IR::U1 value) {
|
||||
program.wave_size == 64u ? ir.CompositeExtract(mask, 1) : IR::U32(IR::Value(0u))};
|
||||
}
|
||||
|
||||
std::array<IR::U32, 2> Translator::ExecMask() {
|
||||
return {ir.GetExecLo(), program.wave_size == 64u ? ir.GetExecHi() : IR::U32(IR::Value(0u))};
|
||||
}
|
||||
|
||||
// Pixel EXEC words exclude the helper invocations that the per-lane EXEC keeps enabled.
|
||||
std::array<IR::U32, 2> Translator::VectorMask(IR::U1 value) {
|
||||
auto mask = BallotMask(value);
|
||||
if (program.stage == ShaderType::Pixel) {
|
||||
const auto exec = ExecMask();
|
||||
mask = {ir.BitwiseAnd(mask[0], exec[0]), ir.BitwiseAnd(mask[1], exec[1])};
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
IR::U32 Translator::ReadRawU32(const Decoder::Operand& operand) {
|
||||
switch (operand.kind) {
|
||||
case Decoder::OperandKind::LiteralConstant:
|
||||
@@ -138,6 +152,8 @@ IR::U32 Translator::ReadRawU32(const Decoder::Operand& operand) {
|
||||
case Decoder::OperandKind::SharedBase:
|
||||
case Decoder::OperandKind::PrivateBase:
|
||||
case Decoder::OperandKind::PopsExitingWaveId: return IR::U32(IR::Value(0u));
|
||||
case Decoder::OperandKind::SharedLimit:
|
||||
case Decoder::OperandKind::PrivateLimit: return IR::U32(IR::Value(0xffffffffu));
|
||||
case Decoder::OperandKind::Sgpr:
|
||||
return ir.GetScalarReg(static_cast<IR::ScalarReg>(operand.reg));
|
||||
case Decoder::OperandKind::Vgpr:
|
||||
@@ -388,21 +404,9 @@ void Translator::WriteOperand(const Decoder::Operand& operand, IR::Value value)
|
||||
switch (operand.kind) {
|
||||
case Decoder::OperandKind::Scc: ir.SetScc(IR::U1(value)); return;
|
||||
case Decoder::OperandKind::ExecLo:
|
||||
case Decoder::OperandKind::ExecHi: {
|
||||
const auto mask = BallotMask(IR::U1(value));
|
||||
ir.SetExec(IR::U1(value));
|
||||
ir.SetExecLo(mask[0]);
|
||||
ir.SetExecHi(mask[1]);
|
||||
return;
|
||||
}
|
||||
case Decoder::OperandKind::ExecHi:
|
||||
case Decoder::OperandKind::VccLo:
|
||||
case Decoder::OperandKind::VccHi: {
|
||||
const auto mask = BallotMask(IR::U1(value));
|
||||
ir.SetVcc(IR::U1(value));
|
||||
ir.SetVccLo(mask[0]);
|
||||
ir.SetVccHi(mask[1]);
|
||||
return;
|
||||
}
|
||||
case Decoder::OperandKind::VccHi: EXIT("lane masks are written through WriteMask");
|
||||
default:
|
||||
WriteRawU32(operand, ir.Select(IR::U1(value), IR::U32(IR::Value(1u)),
|
||||
IR::U32(IR::Value(0u))));
|
||||
@@ -471,17 +475,17 @@ IR::U32 Translator::ReadU32(const Decoder::Operand& operand) {
|
||||
}
|
||||
|
||||
std::array<IR::U32, 2> Translator::ReadU32Pair(const Decoder::Operand& operand) {
|
||||
if (operand.kind == Decoder::OperandKind::PrivateBase ||
|
||||
operand.kind == Decoder::OperandKind::SharedBase) {
|
||||
return {IR::U32(IR::Value(0u)), IR::U32(IR::Value(
|
||||
operand.kind == Decoder::OperandKind::PrivateBase ? Decoder::PrivateApertureHigh
|
||||
: Decoder::SharedApertureHigh))};
|
||||
}
|
||||
if (operand.kind == Decoder::OperandKind::ExecLo) {
|
||||
return {ir.GetExecLo(), ir.GetExecHi()};
|
||||
}
|
||||
if (operand.kind == Decoder::OperandKind::VccLo) {
|
||||
return {ir.GetVccLo(), ir.GetVccHi()};
|
||||
// Each aperture limit is its base plus 0xffffffff.
|
||||
switch (operand.kind) {
|
||||
case Decoder::OperandKind::SharedBase:
|
||||
case Decoder::OperandKind::SharedLimit:
|
||||
return {ReadRawU32(operand), IR::U32(IR::Value(Decoder::SharedApertureHigh))};
|
||||
case Decoder::OperandKind::PrivateBase:
|
||||
case Decoder::OperandKind::PrivateLimit:
|
||||
return {ReadRawU32(operand), IR::U32(IR::Value(Decoder::PrivateApertureHigh))};
|
||||
case Decoder::OperandKind::ExecLo: return {ir.GetExecLo(), ir.GetExecHi()};
|
||||
case Decoder::OperandKind::VccLo: return {ir.GetVccLo(), ir.GetVccHi()};
|
||||
default: break;
|
||||
}
|
||||
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
|
||||
const auto bits =
|
||||
@@ -639,7 +643,7 @@ IR::U1 Translator::ReadMask(const Decoder::Operand& operand) {
|
||||
}
|
||||
case Decoder::OperandKind::ExecLo:
|
||||
case Decoder::OperandKind::ExecHi: return ir.GetExec();
|
||||
case Decoder::OperandKind::VccLo:
|
||||
case Decoder::OperandKind::VccLo: return ir.GetVcc();
|
||||
case Decoder::OperandKind::VccHi:
|
||||
return program.wave_size == 32u
|
||||
? ThreadBit({ReadRawU32(operand), IR::U32(IR::Value(0u))})
|
||||
@@ -677,9 +681,8 @@ IR::U1 Translator::ReadMaskValid(const Decoder::Operand& operand) {
|
||||
}
|
||||
}
|
||||
|
||||
std::array<IR::U32, 2> Translator::WriteMask(const Decoder::Operand& operand, IR::U1 value,
|
||||
bool write_64) {
|
||||
const auto mask = BallotMask(value);
|
||||
void Translator::WriteMask(const Decoder::Operand& operand, IR::U1 value,
|
||||
const std::array<IR::U32, 2>& mask, bool write_64) {
|
||||
switch (operand.kind) {
|
||||
case Decoder::OperandKind::Sgpr: {
|
||||
const auto reg = static_cast<IR::ScalarReg>(operand.reg);
|
||||
@@ -700,43 +703,43 @@ std::array<IR::U32, 2> Translator::WriteMask(const Decoder::Operand& operand, IR
|
||||
ir.SetThreadBitScalarReg(high, IR::U1(IR::Value(false)));
|
||||
ir.SetScalarMaskTag(high, IR::U1(IR::Value(false)));
|
||||
}
|
||||
return mask;
|
||||
return;
|
||||
}
|
||||
case Decoder::OperandKind::ExecLo:
|
||||
case Decoder::OperandKind::ExecHi: {
|
||||
ir.SetExec(value);
|
||||
ir.SetExecLo(mask[0]);
|
||||
ir.SetExecHi(mask[1]);
|
||||
return mask;
|
||||
return;
|
||||
}
|
||||
case Decoder::OperandKind::VccLo:
|
||||
case Decoder::OperandKind::VccHi: {
|
||||
if (!write_64 && program.wave_size == 32u) {
|
||||
if (operand.kind == Decoder::OperandKind::VccHi && !write_64 &&
|
||||
program.wave_size == 32u) {
|
||||
WriteRawU32(operand, mask[0]);
|
||||
return mask;
|
||||
return;
|
||||
}
|
||||
ir.SetVcc(value);
|
||||
ir.SetVccLo(mask[0]);
|
||||
ir.SetVccHi(mask[1]);
|
||||
return mask;
|
||||
if (write_64 || program.wave_size == 64u) {
|
||||
ir.SetVccHi(mask[1]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case Decoder::OperandKind::Scc: ir.SetScc(value); return mask;
|
||||
case Decoder::OperandKind::Scc: ir.SetScc(value); return;
|
||||
default:
|
||||
WriteRawU32(operand, mask[0]);
|
||||
WriteRawU32(OffsetOperand(operand, 1), mask[1]);
|
||||
return mask;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Translator::WriteCompareResult(const Decoder::Operand& operand, IR::U1 value) {
|
||||
if (operand.kind == Decoder::OperandKind::Scc) {
|
||||
WriteOperand(operand, value);
|
||||
return;
|
||||
}
|
||||
void Translator::WriteVectorMask(const Decoder::Operand& operand, IR::U1 value) {
|
||||
// VALU instructions only write lanes enabled by the incoming EXEC mask. This is
|
||||
// especially important for V_CMPX: replacing EXEC with the ungated comparison would
|
||||
// reactivate lanes disabled by an enclosing divergent region.
|
||||
WriteMask(operand, ir.LogicalAnd(ir.GetExec(), value));
|
||||
const auto active = ir.LogicalAnd(ir.GetExec(), value);
|
||||
WriteMask(operand, active, VectorMask(active));
|
||||
}
|
||||
|
||||
void Translator::AddBranchCondition(const CFG::Graph& graph, const CFG::BasicBlock& source,
|
||||
@@ -1153,7 +1156,10 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
||||
}
|
||||
}
|
||||
entry_ir.SetExec(initial_exec);
|
||||
const auto initial_mask = entry_ir.Emit(IR::ValueOpcode::Ballot, {initial_exec});
|
||||
// Pixel EXEC words start with the live (non-helper) lanes; helpers keep the per-lane EXEC.
|
||||
const auto initial_mask = options.stage == ShaderType::Pixel
|
||||
? entry_ir.Emit(IR::ValueOpcode::LiveMask)
|
||||
: entry_ir.Emit(IR::ValueOpcode::Ballot, {initial_exec});
|
||||
const auto initial_lo = entry_ir.CompositeExtract(initial_mask, 0);
|
||||
const auto initial_hi = options.wave_size == 64u ? entry_ir.CompositeExtract(initial_mask, 1)
|
||||
: IR::U32(IR::Value(0u));
|
||||
@@ -1238,12 +1244,12 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
||||
entry_ir.BitwiseOr(wave_info, entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(
|
||||
primitive_count, u32(8)),
|
||||
vertex_count)));
|
||||
// Instance IDs exclude the start instance, which shaders add from user data.
|
||||
if (mesh.fast_launch) {
|
||||
// Fast launch broadcasts the subgroup's base vertex and instance to every lane.
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5), entry_ir.IAdd(draw(1), group));
|
||||
entry_ir.SetVectorReg(
|
||||
static_cast<IR::VectorReg>(6),
|
||||
entry_ir.IAdd(draw(2), builtin(IR::StageInputKind::WorkgroupId, 1)));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(6),
|
||||
builtin(IR::StageInputKind::WorkgroupId, 1));
|
||||
} else {
|
||||
// GS adjacency addresses local ES records in LDS. Fans retain the draw's
|
||||
// center in every subgroup; strip winding follows the global primitive.
|
||||
@@ -1271,14 +1277,14 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
||||
entry_ir.ShiftLeftLogical(second, u32(18))));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(1),
|
||||
entry_ir.ShiftLeftLogical(third, u32(2)));
|
||||
const auto index_bytes = draw(3);
|
||||
const auto index_bytes = draw(2);
|
||||
const auto indexed = entry_ir.INotEqual(index_bytes, u32(0));
|
||||
const auto index_low = draw(4);
|
||||
const auto index_low = draw(3);
|
||||
const auto byte_offset = entry_ir.IAdd(entry_ir.BitwiseAnd(index_low, u32(3)),
|
||||
entry_ir.IMul(input_vertex, index_bytes));
|
||||
const auto index_resource = entry_ir.Emit(
|
||||
IR::ValueOpcode::GetAddressResource,
|
||||
{entry_ir.BitwiseAnd(index_low, u32(~3u)), draw(5)});
|
||||
{entry_ir.BitwiseAnd(index_low, u32(~3u)), draw(4)});
|
||||
const auto memory_index = static_cast<uint32_t>(result.memory_info.size());
|
||||
result.memory_info.push_back({.kind = IR::ResourceKind::Global});
|
||||
const auto packed_index = entry_ir.Emit(
|
||||
@@ -1292,9 +1298,8 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
||||
entry_ir.IMul(index_bytes, u32(8))}));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
|
||||
entry_ir.IAdd(draw(1), entry_ir.Select(indexed, index, input_vertex)));
|
||||
entry_ir.SetVectorReg(
|
||||
static_cast<IR::VectorReg>(8),
|
||||
entry_ir.IAdd(draw(2), builtin(IR::StageInputKind::WorkgroupId, 1)));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(8),
|
||||
builtin(IR::StageInputKind::WorkgroupId, 1));
|
||||
}
|
||||
} else if (options.stage == ShaderType::Local) {
|
||||
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3), IR::U32(IR::Value(64u)));
|
||||
@@ -1384,10 +1389,27 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
||||
IR::U32(IR::Value(options.wave_size << 12u)));
|
||||
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3),
|
||||
entry_ir.BitwiseOr(vertex_count, IR::U32(IR::Value(1u << 28u))));
|
||||
// Vulkan adds an indirect record's bases to the indices; the hardware passes them
|
||||
// only in the user SGPRs the packet patches.
|
||||
const auto& offsets = options.input_info.vertex->indirect_offsets;
|
||||
const auto index = [&](uint8_t slot, IR::StageInputKind kind,
|
||||
IR::StageInputKind base_kind) {
|
||||
if (offsets == ShaderIndirectOffsets {}) {
|
||||
return builtin(kind);
|
||||
}
|
||||
const auto base = builtin(base_kind);
|
||||
if (slot < options.user_data_count) {
|
||||
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(options.user_data_base + slot),
|
||||
base);
|
||||
}
|
||||
return entry_ir.ISub(builtin(kind), base);
|
||||
};
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
|
||||
builtin(IR::StageInputKind::VertexIndex));
|
||||
index(offsets.vertex, IR::StageInputKind::VertexIndex,
|
||||
IR::StageInputKind::BaseVertex));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(8),
|
||||
builtin(IR::StageInputKind::InstanceIndex));
|
||||
index(offsets.instance, IR::StageInputKind::InstanceIndex,
|
||||
IR::StageInputKind::BaseInstance));
|
||||
}
|
||||
}
|
||||
const bool flush_f32_inputs = options.stage == ShaderType::Compute &&
|
||||
|
||||
@@ -27,6 +27,8 @@ private:
|
||||
Decoder::Operand ScalarDestinationOperand(const Decoder::Operand& operand, uint32_t offset);
|
||||
Decoder::Operand PlainOperand(const Decoder::Operand& operand);
|
||||
std::array<IR::U32, 2> BallotMask(IR::U1 value);
|
||||
std::array<IR::U32, 2> ExecMask();
|
||||
std::array<IR::U32, 2> VectorMask(IR::U1 value);
|
||||
IR::U32 ReadRawU32(const Decoder::Operand& operand);
|
||||
IR::U32 ReadScalarCode(uint32_t code);
|
||||
IR::U32 ApplyBitSourceModifiers(const Decoder::Operand& operand, IR::U32 value);
|
||||
@@ -52,9 +54,9 @@ private:
|
||||
IR::U32 ConditionBit(const Decoder::Operand& operand);
|
||||
IR::U1 ReadMask(const Decoder::Operand& operand);
|
||||
IR::U1 ReadMaskValid(const Decoder::Operand& operand);
|
||||
std::array<IR::U32, 2> WriteMask(const Decoder::Operand& operand, IR::U1 value,
|
||||
bool write_64 = false);
|
||||
void WriteCompareResult(const Decoder::Operand& operand, IR::U1 value);
|
||||
void WriteMask(const Decoder::Operand& operand, IR::U1 value,
|
||||
const std::array<IR::U32, 2>& mask, bool write_64 = false);
|
||||
void WriteVectorMask(const Decoder::Operand& operand, IR::U1 value);
|
||||
|
||||
IR::MemoryFlags AddMemoryInfo(const IR::MemoryInfo& memory, uint32_t pc);
|
||||
IR::ExportFlags AddExportInfo(const Decoder::Instruction& inst);
|
||||
@@ -114,17 +116,16 @@ private:
|
||||
|
||||
IR::F32 SelectF32(IR::U1 condition, IR::F32 true_value, IR::F32 false_value);
|
||||
IR::U32 PackU16Lanes(IR::U32 low, IR::U32 high);
|
||||
void EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar, bool cmpx);
|
||||
void EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar, bool cmpx);
|
||||
IR::U32 Saturate16(IR::U32 value, bool signed_value);
|
||||
void EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar);
|
||||
void EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar);
|
||||
void EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, IR::Type type,
|
||||
bool scalar, bool cmpx, bool swap_operands = false);
|
||||
bool scalar, bool swap_operands = false);
|
||||
void EmitInteger16Compare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
|
||||
bool signed_value, bool cmpx);
|
||||
void EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool half,
|
||||
bool cmpx);
|
||||
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx,
|
||||
bool is64 = false);
|
||||
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half = false);
|
||||
bool signed_value);
|
||||
void EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool half);
|
||||
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool is64 = false);
|
||||
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool half = false);
|
||||
void V_CVT_F32_UBYTE(const Decoder::Instruction& inst, uint32_t byte_index);
|
||||
void V_CVT_F32_U32(const Decoder::Instruction& inst);
|
||||
void V_CVT_F32_I32(const Decoder::Instruction& inst);
|
||||
@@ -141,6 +142,7 @@ private:
|
||||
void V_CVT_PKRTZ_F16_F32(const Decoder::Instruction& inst);
|
||||
void V_CVT_PKNORM_F32(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
|
||||
void V_CVT_PK_U8_F32(const Decoder::Instruction& inst);
|
||||
void V_CVT_PK_16_32(const Decoder::Instruction& inst, bool signed_value);
|
||||
void V_PACK_B32_F16(const Decoder::Instruction& inst);
|
||||
void PackedFloat16(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool accumulator,
|
||||
bool quiet_snan);
|
||||
@@ -151,6 +153,7 @@ private:
|
||||
void Float16Ternary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool accumulator,
|
||||
bool mix);
|
||||
void FloatUnary(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
|
||||
void V_FRACT(const Decoder::Instruction& inst, IR::Type type);
|
||||
void FloatBinary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool reverse);
|
||||
void FloatTernary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool accumulator,
|
||||
bool mix);
|
||||
|
||||
@@ -25,444 +25,310 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_PERMLANEX16_B32: V_PERMLANE16_B32(inst, true); return;
|
||||
|
||||
case O::V_CMP_F_I32:
|
||||
case O::V_CMP_F_U32: EmitCompareConstant(inst, false, false, false); return;
|
||||
case O::V_CMP_F_U32: EmitCompareConstant(inst, false, false); return;
|
||||
case O::V_CMP_T_I32:
|
||||
case O::V_CMP_T_U32: EmitCompareConstant(inst, true, false, false); return;
|
||||
case O::V_CMP_T_U32: EmitCompareConstant(inst, true, false); return;
|
||||
case O::V_CMP_EQ_U32:
|
||||
case O::V_CMP_EQ_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_EQ_U32:
|
||||
case O::V_CMPX_EQ_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, false);
|
||||
return;
|
||||
case O::V_CMP_NE_U32:
|
||||
case O::V_CMP_NE_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NE_U32:
|
||||
case O::V_CMPX_NE_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, false);
|
||||
return;
|
||||
case O::V_CMP_GT_U32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_GT_U32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, false);
|
||||
return;
|
||||
case O::V_CMP_GE_U32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, false,
|
||||
false);
|
||||
return;
|
||||
case O::V_CMPX_GE_U32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, false,
|
||||
true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, false);
|
||||
return;
|
||||
case O::V_CMP_LT_U32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LT_U32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, false);
|
||||
return;
|
||||
case O::V_CMP_LE_U32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, false,
|
||||
false);
|
||||
return;
|
||||
case O::V_CMPX_LE_U32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, false);
|
||||
return;
|
||||
case O::V_CMP_GT_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_GT_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, false);
|
||||
return;
|
||||
case O::V_CMP_GE_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, false,
|
||||
false);
|
||||
return;
|
||||
case O::V_CMPX_GE_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, false,
|
||||
true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, false);
|
||||
return;
|
||||
case O::V_CMP_LT_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LT_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, false);
|
||||
return;
|
||||
case O::V_CMP_LE_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false,
|
||||
false);
|
||||
return;
|
||||
case O::V_CMPX_LE_I32:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false);
|
||||
return;
|
||||
case O::V_CMP_F_I64:
|
||||
case O::V_CMP_F_U64: EmitCompareConstant(inst, false, false, false); return;
|
||||
case O::V_CMP_T_I64:
|
||||
case O::V_CMP_T_U64: EmitCompareConstant(inst, true, false, false); return;
|
||||
case O::V_CMP_F_U64:
|
||||
case O::V_CMPX_F_I64:
|
||||
case O::V_CMPX_F_U64: EmitCompareConstant(inst, false, false, true); return;
|
||||
case O::V_CMPX_F_U64: EmitCompareConstant(inst, false, false); return;
|
||||
case O::V_CMP_T_I64:
|
||||
case O::V_CMP_T_U64:
|
||||
case O::V_CMPX_T_I64:
|
||||
case O::V_CMPX_T_U64: EmitCompareConstant(inst, true, false, true); return;
|
||||
case O::V_CMPX_T_U64: EmitCompareConstant(inst, true, false); return;
|
||||
case O::V_CMP_EQ_I64:
|
||||
case O::V_CMP_EQ_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_EQ_I64:
|
||||
case O::V_CMPX_EQ_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false);
|
||||
return;
|
||||
case O::V_CMP_LT_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LT_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false);
|
||||
return;
|
||||
case O::V_CMP_GT_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_GT_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false);
|
||||
return;
|
||||
case O::V_CMP_NE_I64:
|
||||
case O::V_CMP_NE_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NE_I64:
|
||||
case O::V_CMPX_NE_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false);
|
||||
return;
|
||||
|
||||
case O::V_CMP_LT_I64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LT_I64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false);
|
||||
return;
|
||||
case O::V_CMP_LE_I64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LE_I64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false);
|
||||
return;
|
||||
// The IR has no signed 64-bit greater-than: a > b is b < a, and a >= b is b <= a.
|
||||
case O::V_CMP_GT_I64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, false,
|
||||
true);
|
||||
return;
|
||||
case O::V_CMPX_GT_I64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true,
|
||||
true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true);
|
||||
return;
|
||||
case O::V_CMP_GE_I64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, false,
|
||||
true);
|
||||
return;
|
||||
case O::V_CMPX_GE_I64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true,
|
||||
true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true);
|
||||
return;
|
||||
case O::V_CMP_LE_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LE_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false);
|
||||
return;
|
||||
case O::V_CMP_GE_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_GE_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, true);
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false);
|
||||
return;
|
||||
|
||||
case O::V_CMP_EQ_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false, false);
|
||||
case O::V_CMPX_EQ_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false);
|
||||
return;
|
||||
case O::V_CMP_EQ_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_EQ_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, true, true);
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, true);
|
||||
return;
|
||||
case O::V_CMP_NE_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NE_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false, true);
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false);
|
||||
return;
|
||||
case O::V_CMP_NE_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_NE_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true, true);
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true);
|
||||
return;
|
||||
case O::V_CMP_GT_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThan32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_GT_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThan32, false, true);
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThan32, false);
|
||||
return;
|
||||
case O::V_CMP_GE_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThanEqual32, false, false);
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThanEqual32, false);
|
||||
return;
|
||||
case O::V_CMP_LT_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThan32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LT_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThan32, false, true);
|
||||
return;
|
||||
case O::V_CMPX_EQ_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false, true);
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThan32, false);
|
||||
return;
|
||||
case O::V_CMP_LE_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThanEqual32, false, false);
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThanEqual32, false);
|
||||
return;
|
||||
case O::V_CMP_GT_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThan32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_GT_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThan32, true, true);
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThan32, true);
|
||||
return;
|
||||
case O::V_CMP_GE_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThanEqual32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_GE_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThanEqual32, true, true);
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThanEqual32, true);
|
||||
return;
|
||||
case O::V_CMP_LT_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThan32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_LT_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThan32, true, true);
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThan32, true);
|
||||
return;
|
||||
case O::V_CMP_LE_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThanEqual32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_LE_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThanEqual32, true, true);
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThanEqual32, true);
|
||||
return;
|
||||
|
||||
case O::V_CMP_F_F32: EmitCompareConstant(inst, false, false, false); return;
|
||||
case O::V_CMP_TRU_F32: EmitCompareConstant(inst, true, false, false); return;
|
||||
case O::V_CMP_F_F32: EmitCompareConstant(inst, false, false); return;
|
||||
case O::V_CMP_TRU_F32: EmitCompareConstant(inst, true, false); return;
|
||||
case O::V_CMP_EQ_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false, false);
|
||||
case O::V_CMPX_EQ_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false);
|
||||
return;
|
||||
case O::V_CMP_EQ_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false, false);
|
||||
case O::V_CMPX_EQ_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false);
|
||||
return;
|
||||
case O::V_CMP_LE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual64, false, true);
|
||||
return;
|
||||
case O::V_CMPX_GE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, true);
|
||||
return;
|
||||
case O::V_CMP_F_F64: EmitCompareConstant(inst, false, false, false); return;
|
||||
case O::V_CMP_TRU_F64: EmitCompareConstant(inst, true, false, false); return;
|
||||
case O::V_CMPX_F_F64: EmitCompareConstant(inst, false, false, true); return;
|
||||
case O::V_CMPX_TRU_F64: EmitCompareConstant(inst, true, false, true); return;
|
||||
case O::V_CMP_LT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false, true);
|
||||
return;
|
||||
case O::V_CMPX_EQ_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false, true);
|
||||
return;
|
||||
case O::V_CMP_GT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_GT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false, true);
|
||||
return;
|
||||
case O::V_CMP_LG_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LG_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual64, false);
|
||||
return;
|
||||
case O::V_CMP_GE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, false);
|
||||
case O::V_CMPX_GE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false);
|
||||
return;
|
||||
case O::V_CMP_F_F64:
|
||||
case O::V_CMPX_F_F64: EmitCompareConstant(inst, false, false); return;
|
||||
case O::V_CMP_TRU_F64:
|
||||
case O::V_CMPX_TRU_F64: EmitCompareConstant(inst, true, false); return;
|
||||
case O::V_CMP_LT_F64:
|
||||
case O::V_CMPX_LT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false);
|
||||
return;
|
||||
case O::V_CMP_GT_F64:
|
||||
case O::V_CMPX_GT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false);
|
||||
return;
|
||||
case O::V_CMP_LG_F64:
|
||||
case O::V_CMPX_LG_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false);
|
||||
return;
|
||||
case O::V_CMP_NLG_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NLG_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false);
|
||||
return;
|
||||
case O::V_CMP_NEQ_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NEQ_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false);
|
||||
return;
|
||||
case O::V_CMP_NLE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NLE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false);
|
||||
return;
|
||||
case O::V_CMP_NLT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NLT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false);
|
||||
return;
|
||||
case O::V_CMP_NGE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NGE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false);
|
||||
return;
|
||||
case O::V_CMP_NGT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NGT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false, true);
|
||||
return;
|
||||
case O::V_CMP_O_F64: EmitFloatOrderedCompare(inst, true, false, true); return;
|
||||
case O::V_CMP_U_F64: EmitFloatOrderedCompare(inst, false, false, true); return;
|
||||
case O::V_CMPX_O_F64: EmitFloatOrderedCompare(inst, true, true, true); return;
|
||||
case O::V_CMPX_U_F64: EmitFloatOrderedCompare(inst, false, true, true); return;
|
||||
case O::V_CMPX_EQ_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false);
|
||||
return;
|
||||
case O::V_CMP_O_F64:
|
||||
case O::V_CMPX_O_F64: EmitFloatOrderedCompare(inst, true, true); return;
|
||||
case O::V_CMP_U_F64:
|
||||
case O::V_CMPX_U_F64: EmitFloatOrderedCompare(inst, false, true); return;
|
||||
case O::V_CMP_LG_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LG_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, false);
|
||||
return;
|
||||
case O::V_CMP_GT_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_GT_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, false);
|
||||
return;
|
||||
case O::V_CMP_GE_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_GE_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, false);
|
||||
return;
|
||||
case O::V_CMP_LT_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LT_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, false);
|
||||
return;
|
||||
case O::V_CMP_LE_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LE_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, false);
|
||||
return;
|
||||
case O::V_CMP_NLG_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NLG_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual32, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual32, false);
|
||||
return;
|
||||
case O::V_CMP_NEQ_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NEQ_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, false);
|
||||
return;
|
||||
case O::V_CMP_NLE_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NLE_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, false);
|
||||
return;
|
||||
case O::V_CMP_NLT_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NLT_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, false);
|
||||
return;
|
||||
case O::V_CMP_NGE_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NGE_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, false);
|
||||
return;
|
||||
case O::V_CMP_NGT_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NGT_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, false, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, false);
|
||||
return;
|
||||
case O::V_CMP_EQ_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_EQ_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, true, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, true);
|
||||
return;
|
||||
case O::V_CMP_LG_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, true, false);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, true);
|
||||
return;
|
||||
case O::V_CMP_GT_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_GT_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, true, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, true);
|
||||
return;
|
||||
case O::V_CMP_GE_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_GE_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, true, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, true);
|
||||
return;
|
||||
case O::V_CMP_LT_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_LT_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, true, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, true);
|
||||
return;
|
||||
case O::V_CMP_LE_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_LE_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, true, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, true);
|
||||
return;
|
||||
case O::V_CMP_NGE_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, true, false);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, true);
|
||||
return;
|
||||
case O::V_CMP_NGT_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_NGT_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true);
|
||||
return;
|
||||
case O::V_CMPX_NLE_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, true, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, true);
|
||||
return;
|
||||
case O::V_CMP_NEQ_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_NEQ_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, true, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, true);
|
||||
return;
|
||||
case O::V_CMP_NLT_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_NLT_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, true, true);
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, true);
|
||||
return;
|
||||
case O::V_CMP_O_F32: EmitFloatOrderedCompare(inst, true, false); return;
|
||||
case O::V_CMP_U_F32: EmitFloatOrderedCompare(inst, false, false); return;
|
||||
case O::V_CMPX_O_F32: EmitFloatOrderedCompare(inst, true, true); return;
|
||||
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false, true); return;
|
||||
case O::V_CMP_CLASS_F32: EmitFloatClassCompare(inst, false); return;
|
||||
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst, true); return;
|
||||
case O::V_CMP_CLASS_F16: EmitFloatClassCompare(inst, false, true); return;
|
||||
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true, true); return;
|
||||
case O::V_CMP_O_F32:
|
||||
case O::V_CMPX_O_F32: EmitFloatOrderedCompare(inst, true); return;
|
||||
case O::V_CMP_U_F32:
|
||||
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false); return;
|
||||
case O::V_CMP_CLASS_F32:
|
||||
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst); return;
|
||||
case O::V_CMP_CLASS_F16:
|
||||
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true); return;
|
||||
|
||||
case O::V_CVT_F32_UBYTE0: V_CVT_F32_UBYTE(inst, 0); return;
|
||||
case O::V_CVT_F32_UBYTE1: V_CVT_F32_UBYTE(inst, 1); return;
|
||||
@@ -495,8 +361,8 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
return;
|
||||
case O::V_CVT_PK_U8_F32: V_CVT_PK_U8_F32(inst); return;
|
||||
case O::V_PACK_B32_F16: V_PACK_B32_F16(inst); return;
|
||||
case O::V_CVT_PK_U16_U32:
|
||||
case O::V_CVT_PK_I16_I32: return PackB16(inst, false, false);
|
||||
case O::V_CVT_PK_U16_U32: return V_CVT_PK_16_32(inst, false);
|
||||
case O::V_CVT_PK_I16_I32: return V_CVT_PK_16_32(inst, true);
|
||||
|
||||
case O::V_LSHLREV_B16:
|
||||
return Integer16Shift(inst, IR::ValueOpcode::ShiftLeftLogical32, false);
|
||||
@@ -563,7 +429,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_CEIL_F16: return Float16Unary(inst, IR::ValueOpcode::FPCeil32, false);
|
||||
case O::V_TRUNC_F16: return Float16Unary(inst, IR::ValueOpcode::FPTrunc32, false);
|
||||
case O::V_RNDNE_F16: return Float16Unary(inst, IR::ValueOpcode::FPRoundEven32, false);
|
||||
case O::V_FRACT_F16: return Float16Unary(inst, IR::ValueOpcode::FPFract32, false);
|
||||
case O::V_FRACT_F16: return V_FRACT(inst, IR::Type::F16);
|
||||
case O::V_SIN_F16: return Float16Trig(inst, IR::ValueOpcode::FPSin);
|
||||
case O::V_COS_F16: return Float16Trig(inst, IR::ValueOpcode::FPCos);
|
||||
case O::V_MIN3_F16: return Float16Ternary(inst, IR::ValueOpcode::FPMinTri32, false, false);
|
||||
@@ -574,8 +440,8 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_RCP_F32: return FloatUnary(inst, IR::ValueOpcode::FPRecip32);
|
||||
case O::V_RCP_F64: return FloatUnary(inst, IR::ValueOpcode::FPRecip64);
|
||||
case O::V_RCP_IFLAG_F32: return FloatUnary(inst, IR::ValueOpcode::FPRecipIFlag32);
|
||||
case O::V_FRACT_F32: return FloatUnary(inst, IR::ValueOpcode::FPFract32);
|
||||
case O::V_FRACT_F64: return FloatUnary(inst, IR::ValueOpcode::FPFract64);
|
||||
case O::V_FRACT_F32: return V_FRACT(inst, IR::Type::F32);
|
||||
case O::V_FRACT_F64: return V_FRACT(inst, IR::Type::F64);
|
||||
case O::V_TRUNC_F32: return FloatUnary(inst, IR::ValueOpcode::FPTrunc32);
|
||||
case O::V_TRUNC_F64: return FloatUnary(inst, IR::ValueOpcode::FPTrunc64);
|
||||
case O::V_CEIL_F32: return FloatUnary(inst, IR::ValueOpcode::FPCeil32);
|
||||
|
||||
@@ -118,15 +118,14 @@ ResourcePlan& ResourcePlan::operator=(ResourcePlan&& other) noexcept {
|
||||
}
|
||||
|
||||
Program::~Program() {
|
||||
// Planning expressions can refer to block values but outlive block storage in the base class.
|
||||
// Planning expressions can refer to block values but outlive block storage in the base class,
|
||||
// and values cross block boundaries: detach everything before any storage is destroyed.
|
||||
for (auto& inst: value_storage) {
|
||||
inst.Invalidate();
|
||||
inst.DropArgs();
|
||||
}
|
||||
// Values may cross block boundaries. Detach all arguments before any block starts destroying
|
||||
// its instruction storage so reverse-use links always point to live definitions.
|
||||
for (auto* block: blocks) {
|
||||
for (auto& inst: *block) {
|
||||
inst.Invalidate();
|
||||
inst.DropArgs();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,6 +168,8 @@ bool EquivalentSelectorDomain(const ResourcePlan& program,
|
||||
const DescriptorSource::IndirectDescriptor& right) {
|
||||
return left.selector == right.selector && left.signed_count == right.signed_count &&
|
||||
left.workgroup_axis == right.workgroup_axis &&
|
||||
left.workgroup_multiplier == right.workgroup_multiplier &&
|
||||
left.workgroup_shift == right.workgroup_shift &&
|
||||
EquivalentValue(program, left.key_count, right.key_count) &&
|
||||
EquivalentValue(program, left.selector_first, right.selector_first) &&
|
||||
EquivalentValue(program, left.selector_mask, right.selector_mask);
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
#ifndef EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_SERIALIZE_H_
|
||||
#define EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_SERIALIZE_H_
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "graphics/shader/recompiler/backend/spirv/SpirvEmitter.h"
|
||||
#include "graphics/shader/recompiler/ir/ShaderIR.h"
|
||||
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
|
||||
#include "graphics/shader/rectListShader.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// Shader cache records. Io(io, value) appends value to a Writer or reads it back from a Reader
|
||||
// field by field, so padding never reaches the bytes; instructions travel as plan-local indices.
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
|
||||
struct Writer {
|
||||
std::vector<uint8_t> bytes;
|
||||
std::unordered_map<const Inst*, uint32_t> index; // Instructions that values may refer to.
|
||||
|
||||
void Raw(const void* data, size_t size) {
|
||||
const auto* begin = static_cast<const uint8_t*>(data);
|
||||
bytes.insert(bytes.end(), begin, begin + size);
|
||||
}
|
||||
};
|
||||
|
||||
struct Reader {
|
||||
std::span<const uint8_t> bytes;
|
||||
std::vector<Inst*> insts; // Instructions by index.
|
||||
bool ok = true; // False once a read ran past the end or was invalid.
|
||||
|
||||
void Raw(void* data, size_t size) {
|
||||
ok = ok && size <= bytes.size();
|
||||
if (!ok) return;
|
||||
std::copy_n(bytes.data(), size, static_cast<uint8_t*>(data));
|
||||
bytes = bytes.subspan(size);
|
||||
}
|
||||
// Every element takes at least one byte, which bounds counts read from damaged records.
|
||||
uint32_t Count(uint32_t count) {
|
||||
ok = ok && count <= bytes.size();
|
||||
return ok ? count : 0u;
|
||||
}
|
||||
};
|
||||
|
||||
template <class S> constexpr bool Reading = std::is_same_v<S, Reader>;
|
||||
template <class T, template <class...> class Of> constexpr bool IsA = false;
|
||||
template <template <class...> class Of, class... A> constexpr bool IsA<Of<A...>, Of> = true;
|
||||
template <class T, class U> concept Is = std::is_same_v<std::remove_const_t<T>, U>;
|
||||
|
||||
template <class S, class T> void Io(S& io, T& value);
|
||||
|
||||
template <class S, class... T> void IoAll(S& io, T&&... values) {
|
||||
(Io(io, values), ...);
|
||||
}
|
||||
|
||||
// The value's Type, Opaque for an instruction, then the instruction index or the zero-extended
|
||||
// immediate.
|
||||
inline void IoValue(Writer& writer, const Value& value) {
|
||||
auto type = Type::Opaque;
|
||||
uint64_t bits = 0;
|
||||
if (const auto* inst = value.TryInstruction()) {
|
||||
bits = writer.index.at(inst);
|
||||
} else {
|
||||
type = value.GetType();
|
||||
switch (type) {
|
||||
case Type::Void: break;
|
||||
case Type::ScalarReg: bits = RegIndex(value.ScalarRegister()); break;
|
||||
case Type::VectorReg: bits = RegIndex(value.VectorRegister()); break;
|
||||
case Type::U1: bits = value.U1(); break;
|
||||
case Type::U8: bits = value.U8(); break;
|
||||
case Type::U16: bits = value.U16(); break;
|
||||
case Type::U32: bits = value.U32(); break;
|
||||
case Type::U64: bits = value.U64(); break;
|
||||
case Type::F16: bits = value.F16Bits(); break;
|
||||
case Type::F32: bits = std::bit_cast<uint32_t>(value.F32Value()); break;
|
||||
default: EXIT("value type %u has no cache encoding\n", static_cast<uint32_t>(type));
|
||||
}
|
||||
}
|
||||
IoAll(writer, type, bits);
|
||||
}
|
||||
|
||||
inline void IoValue(Reader& reader, Value& value) {
|
||||
auto type = Type::Void;
|
||||
uint64_t bits = 0;
|
||||
IoAll(reader, type, bits);
|
||||
switch (type) {
|
||||
case Type::Void: value = {}; break;
|
||||
case Type::Opaque:
|
||||
reader.ok = reader.ok && bits < reader.insts.size();
|
||||
if (reader.ok) value = Value(reader.insts[bits]);
|
||||
break;
|
||||
case Type::ScalarReg: value = Value(static_cast<ScalarReg>(bits)); break;
|
||||
case Type::VectorReg: value = Value(static_cast<VectorReg>(bits)); break;
|
||||
case Type::U1: value = Value(bits != 0u); break;
|
||||
case Type::U8: value = Value(static_cast<uint8_t>(bits)); break;
|
||||
case Type::U16: value = Value(static_cast<uint16_t>(bits)); break;
|
||||
case Type::U32: value = Value(static_cast<uint32_t>(bits)); break;
|
||||
case Type::U64: value = Value(bits); break;
|
||||
case Type::F16: value = Value::F16(static_cast<uint16_t>(bits)); break;
|
||||
case Type::F32: value = Value::F32(std::bit_cast<float>(static_cast<uint32_t>(bits))); break;
|
||||
default: reader.ok = false; break;
|
||||
}
|
||||
}
|
||||
|
||||
// Scalars and standard containers; structs have their own overloads below.
|
||||
template <class S, class T> void Io(S& io, T& value) {
|
||||
using U = std::remove_const_t<T>;
|
||||
static_assert(!std::is_pointer_v<U>, "pointers do not persist");
|
||||
if constexpr (std::is_arithmetic_v<U> || std::is_enum_v<U>) {
|
||||
io.Raw(&value, sizeof(U));
|
||||
} else if constexpr (std::is_same_v<U, Value>) {
|
||||
IoValue(io, value);
|
||||
} else if constexpr (IsA<U, std::vector> || IsA<U, std::basic_string>) {
|
||||
auto count = static_cast<uint32_t>(value.size());
|
||||
Io(io, count);
|
||||
if constexpr (Reading<S>) value.resize(io.Count(count));
|
||||
using E = typename U::value_type;
|
||||
if constexpr (std::is_arithmetic_v<E> || std::is_enum_v<E>) {
|
||||
io.Raw(value.data(), value.size() * sizeof(E));
|
||||
} else {
|
||||
for (auto& element: value) Io(io, element);
|
||||
}
|
||||
} else if constexpr (IsA<U, std::optional>) {
|
||||
bool has = value.has_value();
|
||||
Io(io, has);
|
||||
if constexpr (Reading<S>) {
|
||||
if (has) value = typename U::value_type {}; // Not emplace(): clang rejects nested aggregates.
|
||||
else value.reset();
|
||||
}
|
||||
if (has) Io(io, *value);
|
||||
} else {
|
||||
static_assert(requires { std::tuple_size<U>::value; }, "struct without an Io overload");
|
||||
std::apply([&](auto&... elements) { IoAll(io, elements...); }, value); // std::array, std::pair
|
||||
}
|
||||
}
|
||||
|
||||
// Binds every field, so that a field added later fails to compile until it is listed.
|
||||
#define KYTY_IO_FIELDS(Struct, ...) \
|
||||
template <class S, Is<Struct> T> void Io(S& io, T& object) { \
|
||||
auto& [__VA_ARGS__] = object; \
|
||||
IoAll(io, __VA_ARGS__); \
|
||||
}
|
||||
KYTY_IO_FIELDS(MemoryInfo, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y)
|
||||
KYTY_IO_FIELDS(BufferResource, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)
|
||||
KYTY_IO_FIELDS(ImageResource, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w)
|
||||
KYTY_IO_FIELDS(SamplerResource, a, b, c, d, e, f, g, h, i, j, k)
|
||||
KYTY_IO_FIELDS(SampledResourcePair, a, b, c)
|
||||
KYTY_IO_FIELDS(StageInput, a, b, c, d, e)
|
||||
KYTY_IO_FIELDS(StageOutput, a, b, c, d)
|
||||
KYTY_IO_FIELDS(ShaderInfo, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z,
|
||||
A, B, C, D, E, F, G, H, I, J, K)
|
||||
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::SelectorRead::CountedRows, a, b)
|
||||
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::SelectorRead::MaskRanks, a, b, c)
|
||||
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::SelectorRead, a, b, c, d, e, f, g, h, i, j, k,
|
||||
l)
|
||||
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::PointerTable, a, b, c, d)
|
||||
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p,
|
||||
q, r)
|
||||
KYTY_IO_FIELDS(DescriptorSource, a, b, c)
|
||||
KYTY_IO_FIELDS(SrtRead, a, b)
|
||||
KYTY_IO_FIELDS(ResourceBlock, a, b, c, d, e)
|
||||
KYTY_IO_FIELDS(UniformFill, a, b, c, d, e, f)
|
||||
KYTY_IO_FIELDS(UniformFillPlan, a, b, c, d)
|
||||
KYTY_IO_FIELDS(ResourceSpecialization::Buffer, a, b, c, d, e, f, g, h)
|
||||
KYTY_IO_FIELDS(ResourceSpecialization::Image, a, b, c, d, e, f, g, h, i, j, k)
|
||||
KYTY_IO_FIELDS(ResourceSpecialization::Sampler, a, b, c)
|
||||
KYTY_IO_FIELDS(ResourceSpecialization, a, b, c)
|
||||
KYTY_IO_FIELDS(Bindings, a, b, c, d, e)
|
||||
KYTY_IO_FIELDS(CompiledShaderInfo, a, b, c, d, e, f, g, h, i, j)
|
||||
KYTY_IO_FIELDS(Spirv::Profile, a, b, c)
|
||||
KYTY_IO_FIELDS(ShaderTessellationInputInfo, a, b, c, d, e, f, g, h, i)
|
||||
KYTY_IO_FIELDS(RectListParameter, a, b, c)
|
||||
#undef KYTY_IO_FIELDS
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
||||
#endif /* EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_SERIALIZE_H_ */
|
||||
@@ -251,6 +251,8 @@ enum class StageInputKind {
|
||||
LocalInvocationId,
|
||||
LocalInvocationIndex,
|
||||
GlobalInvocationId,
|
||||
BaseVertex,
|
||||
BaseInstance,
|
||||
Parameter,
|
||||
};
|
||||
|
||||
@@ -365,7 +367,7 @@ static_assert(static_cast<uint32_t>(DescriptorBindingKind::Count) == 58u);
|
||||
|
||||
struct PushData {
|
||||
static constexpr uint32_t DwordCount = 32;
|
||||
static constexpr uint32_t MeshDrawDwordCount = 6;
|
||||
static constexpr uint32_t MeshDrawDwordCount = 5;
|
||||
static constexpr uint32_t NoStart = UINT32_MAX;
|
||||
std::array<uint32_t, DwordCount> dwords {};
|
||||
|
||||
@@ -495,10 +497,16 @@ struct Bindings {
|
||||
uint32_t dispatch_thread_dword = PushData::NoStart;
|
||||
uint32_t memory_offset_dword = 0;
|
||||
|
||||
[[nodiscard]] uint32_t ShaderDataDwords() const {
|
||||
// Packed 8-bit binding offsets, then each binding's bound range in bytes: OpArrayLength
|
||||
// reads 0 for ranges of 2 GiB or more on NVIDIA.
|
||||
[[nodiscard]] uint32_t MemorySizeDword() const {
|
||||
return memory_offset_dword +
|
||||
(descriptor_counts[static_cast<size_t>(DescriptorBindingKind::Buffers)] + 3u) / 4u;
|
||||
}
|
||||
[[nodiscard]] uint32_t ShaderDataDwords() const {
|
||||
return MemorySizeDword() +
|
||||
descriptor_counts[static_cast<size_t>(DescriptorBindingKind::Buffers)];
|
||||
}
|
||||
[[nodiscard]] bool UsesPushData() const {
|
||||
return push_data_start_dword != PushData::NoStart;
|
||||
}
|
||||
@@ -541,6 +549,7 @@ struct ShaderInfo {
|
||||
bool subgroup_shuffle = false;
|
||||
bool subgroup_arithmetic = false;
|
||||
bool subgroup_local_invocation_id = false;
|
||||
bool helper_invocation = false;
|
||||
bool compute_derivatives = false;
|
||||
bool image_gather_extended = false;
|
||||
bool function_lds = false;
|
||||
@@ -565,6 +574,13 @@ struct DescriptorSource {
|
||||
|
||||
bool operator==(const CountedRows& other) const = default;
|
||||
};
|
||||
struct MaskRanks {
|
||||
uint32_t source = UINT32_MAX;
|
||||
uint32_t stride = 0; // Byte spacing of the mask words its load can address.
|
||||
uint32_t offset = 0;
|
||||
|
||||
bool operator==(const MaskRanks& other) const = default;
|
||||
};
|
||||
uint32_t source = UINT32_MAX;
|
||||
uint32_t stride = 0;
|
||||
uint32_t offset = 0;
|
||||
@@ -572,10 +588,12 @@ struct DescriptorSource {
|
||||
uint32_t bit_offset = 0;
|
||||
uint32_t bit_mask = UINT32_MAX;
|
||||
bool global = false;
|
||||
// Four DWORD bitmap words per record; set-bit ranks subtract from the last key.
|
||||
// Four or eight DWORD bitmap words per record; set ranks subtract from the last key.
|
||||
bool bitset = false;
|
||||
std::optional<CountedRows> rows;
|
||||
std::vector<std::pair<uint32_t, uint32_t>> byte_ranges;
|
||||
std::optional<uint32_t> excluded; // A guard keeps this key from the read.
|
||||
std::optional<MaskRanks> ranks; // Records are the bits some mask word sets.
|
||||
|
||||
bool operator==(const SelectorRead& other) const = default;
|
||||
};
|
||||
@@ -599,8 +617,12 @@ struct DescriptorSource {
|
||||
bool indexed = false;
|
||||
bool signed_count = false;
|
||||
uint32_t workgroup_axis = UINT32_MAX;
|
||||
// Nonzero shift: workgroup keys are (selector_first + ID) * multiplier >> shift.
|
||||
uint32_t workgroup_multiplier = 0;
|
||||
uint32_t workgroup_shift = 0;
|
||||
Value key_count;
|
||||
// First selector index, or first descriptor key for a direct or bitmap range.
|
||||
// First selector index, workgroup key base, or first descriptor key for a direct or bitmap
|
||||
// range.
|
||||
Value selector_first;
|
||||
Value selector_mask;
|
||||
std::vector<uint32_t> sources;
|
||||
@@ -627,6 +649,7 @@ struct ResourceBlock {
|
||||
Value condition;
|
||||
std::vector<uint32_t> successors;
|
||||
std::vector<uint32_t> sources;
|
||||
std::vector<uint32_t> images;
|
||||
std::vector<uint32_t> srt_reads;
|
||||
};
|
||||
|
||||
@@ -655,7 +678,6 @@ struct UniformFillPlan {
|
||||
// condition and fill values without translated blocks, plus reusable evaluation scratch.
|
||||
struct ResourcePlan {
|
||||
struct SelectorKeys {
|
||||
uint32_t source = UINT32_MAX;
|
||||
std::vector<uint32_t> values;
|
||||
bool resolved = false;
|
||||
};
|
||||
|
||||
@@ -25,22 +25,6 @@ Value Value::F32(float value) {
|
||||
return Value(Type::F32, std::bit_cast<uint32_t>(value));
|
||||
}
|
||||
|
||||
bool Value::IsEmpty() const {
|
||||
return type == Type::Void;
|
||||
}
|
||||
|
||||
bool Value::IsImmediate() const {
|
||||
return type != Type::Opaque;
|
||||
}
|
||||
|
||||
bool Value::IsIdentity() const {
|
||||
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Identity;
|
||||
}
|
||||
|
||||
bool Value::IsPhi() const {
|
||||
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Phi;
|
||||
}
|
||||
|
||||
Type Value::GetType() const {
|
||||
if (IsPhi()) {
|
||||
return inst->Flags<Type>();
|
||||
@@ -56,19 +40,6 @@ Inst* Value::Instruction() const {
|
||||
return inst;
|
||||
}
|
||||
|
||||
Inst* Value::TryInstruction() const {
|
||||
return type == Type::Opaque ? inst : nullptr;
|
||||
}
|
||||
|
||||
Inst* Value::ResolveInstruction() const {
|
||||
EXIT_IF(type != Type::Opaque);
|
||||
return IsIdentity() ? inst->Arg(0).ResolveInstruction() : inst;
|
||||
}
|
||||
|
||||
Value Value::Resolve() const {
|
||||
return IsIdentity() ? inst->Arg(0).Resolve() : *this;
|
||||
}
|
||||
|
||||
ScalarReg Value::ScalarRegister() const {
|
||||
EXIT_IF(type != Type::ScalarReg);
|
||||
return scalar_reg;
|
||||
@@ -94,11 +65,6 @@ uint16_t Value::U16() const {
|
||||
return imm_u16;
|
||||
}
|
||||
|
||||
uint32_t Value::U32() const {
|
||||
EXIT_IF(type != Type::U32);
|
||||
return imm_u32;
|
||||
}
|
||||
|
||||
uint64_t Value::U64() const {
|
||||
EXIT_IF(type != Type::U64);
|
||||
return imm_u64;
|
||||
@@ -152,10 +118,6 @@ Inst::~Inst() {
|
||||
ClearArgs();
|
||||
}
|
||||
|
||||
ValueOpcode Inst::GetOpcode() const {
|
||||
return opcode;
|
||||
}
|
||||
|
||||
Type Inst::GetType() const {
|
||||
if (opcode == ValueOpcode::Phi) {
|
||||
return static_cast<Type>(flags);
|
||||
@@ -178,51 +140,25 @@ size_t Inst::UseCount() const {
|
||||
return uses.size();
|
||||
}
|
||||
|
||||
size_t Inst::NumArgs() const {
|
||||
return num_args == PhiArity ? phi_args.size() : num_args;
|
||||
}
|
||||
|
||||
size_t Inst::NumPhiBlocks() const {
|
||||
return num_args == PhiArity ? phi_args.size() : 0;
|
||||
}
|
||||
|
||||
Value Inst::Arg(size_t index) const {
|
||||
EXIT_IF(index >= NumArgs());
|
||||
if (num_args <= InlineArity) {
|
||||
return fixed_args[index];
|
||||
}
|
||||
return num_args == PhiArity ? phi_args[index].second : large_args[index];
|
||||
}
|
||||
|
||||
Block* Inst::PhiBlock(size_t index) const {
|
||||
EXIT_IF(opcode != ValueOpcode::Phi || index >= phi_args.size());
|
||||
return phi_args[index].first;
|
||||
}
|
||||
|
||||
Block* Inst::Parent() const {
|
||||
return parent;
|
||||
}
|
||||
|
||||
const std::vector<Use>& Inst::Uses() const {
|
||||
return uses;
|
||||
}
|
||||
|
||||
void Inst::SetParent(Block* block) {
|
||||
parent = block;
|
||||
}
|
||||
|
||||
void Inst::SetArg(size_t index, Value value) {
|
||||
const auto old = Arg(index);
|
||||
if (auto* old_inst = old.TryInstruction(); old_inst != nullptr) {
|
||||
auto& arg = ArgSlot(*this, index);
|
||||
if (auto* old_inst = arg.TryInstruction(); old_inst != nullptr) {
|
||||
RemoveUse(old_inst, index);
|
||||
}
|
||||
if (num_args <= InlineArity) {
|
||||
fixed_args[index] = value;
|
||||
} else if (num_args == PhiArity) {
|
||||
phi_args[index].second = value;
|
||||
} else {
|
||||
large_args[index] = value;
|
||||
}
|
||||
arg = value;
|
||||
if (auto* new_inst = value.TryInstruction(); new_inst != nullptr) {
|
||||
AddUse(new_inst, index);
|
||||
}
|
||||
@@ -238,11 +174,15 @@ void Inst::AddPhiOperand(Block* predecessor, Value value) {
|
||||
}
|
||||
|
||||
void Inst::ReplaceUsesWith(Value replacement, bool preserve) {
|
||||
const auto old_uses = uses;
|
||||
for (const auto& use: old_uses) {
|
||||
use.user->SetArg(use.operand, replacement);
|
||||
}
|
||||
// Detach the arguments first (a phi can use itself), then move every use over in order.
|
||||
Invalidate();
|
||||
const auto old_uses = std::exchange(uses, {});
|
||||
for (const auto& use: old_uses) {
|
||||
ArgSlot(*use.user, use.operand) = replacement;
|
||||
}
|
||||
if (auto* target = replacement.TryInstruction(); target != nullptr) {
|
||||
target->uses.insert(target->uses.end(), old_uses.begin(), old_uses.end());
|
||||
}
|
||||
if (preserve) {
|
||||
opcode = ValueOpcode::Identity;
|
||||
num_args = 1;
|
||||
@@ -256,9 +196,6 @@ void Inst::Invalidate() {
|
||||
}
|
||||
|
||||
void Inst::AddUse(Inst* used, size_t operand) {
|
||||
const auto found = std::ranges::find_if(
|
||||
used->uses, [&](const Use& use) { return use.user == this && use.operand == operand; });
|
||||
EXIT_IF(found != used->uses.end());
|
||||
used->uses.push_back({this, operand});
|
||||
}
|
||||
|
||||
@@ -275,6 +212,10 @@ void Inst::ClearArgs() {
|
||||
RemoveUse(value_inst, index);
|
||||
}
|
||||
}
|
||||
DropArgs();
|
||||
}
|
||||
|
||||
void Inst::DropArgs() {
|
||||
if (num_args == PhiArity) {
|
||||
std::destroy_at(&phi_args);
|
||||
std::construct_at(&fixed_args);
|
||||
|
||||
@@ -33,14 +33,14 @@ public:
|
||||
static Value F16(uint16_t bits);
|
||||
static Value F32(float value);
|
||||
|
||||
[[nodiscard]] bool IsEmpty() const;
|
||||
[[nodiscard]] bool IsImmediate() const;
|
||||
[[nodiscard]] bool IsEmpty() const { return type == Type::Void; }
|
||||
[[nodiscard]] bool IsImmediate() const { return type != Type::Opaque; }
|
||||
[[nodiscard]] bool IsIdentity() const;
|
||||
[[nodiscard]] bool IsPhi() const;
|
||||
[[nodiscard]] Type GetType() const;
|
||||
|
||||
[[nodiscard]] Inst* Instruction() const;
|
||||
[[nodiscard]] Inst* TryInstruction() const;
|
||||
[[nodiscard]] Inst* TryInstruction() const { return type == Type::Opaque ? inst : nullptr; }
|
||||
[[nodiscard]] Inst* ResolveInstruction() const;
|
||||
[[nodiscard]] Value Resolve() const;
|
||||
[[nodiscard]] ScalarReg ScalarRegister() const;
|
||||
@@ -112,7 +112,7 @@ public:
|
||||
Inst(Inst&&) = delete;
|
||||
Inst& operator=(Inst&&) = delete;
|
||||
|
||||
[[nodiscard]] ValueOpcode GetOpcode() const;
|
||||
[[nodiscard]] ValueOpcode GetOpcode() const { return opcode; }
|
||||
[[nodiscard]] Type GetType() const;
|
||||
[[nodiscard]] bool MayHaveSideEffects() const;
|
||||
[[nodiscard]] bool HasUses() const;
|
||||
@@ -121,8 +121,8 @@ public:
|
||||
[[nodiscard]] size_t NumPhiBlocks() const;
|
||||
[[nodiscard]] Value Arg(size_t index) const;
|
||||
[[nodiscard]] Block* PhiBlock(size_t index) const;
|
||||
[[nodiscard]] Block* Parent() const;
|
||||
[[nodiscard]] const std::vector<Use>& Uses() const;
|
||||
[[nodiscard]] Block* Parent() const { return parent; }
|
||||
[[nodiscard]] const std::vector<Use>& Uses() const { return uses; }
|
||||
// SRT walker output index, assigned by the resource plan that owns this instruction.
|
||||
[[nodiscard]] uint32_t WalkOutput() const { return walk_output; }
|
||||
void SetWalkOutput(uint32_t index) const { walk_output = index; }
|
||||
@@ -138,6 +138,8 @@ public:
|
||||
void AddPhiOperand(Block* predecessor, Value value);
|
||||
void ReplaceUsesWith(Value replacement, bool preserve = true);
|
||||
void Invalidate();
|
||||
// Drops the arguments without updating their use lists, for teardown that destroys both sides.
|
||||
void DropArgs();
|
||||
|
||||
template <typename T>
|
||||
requires(sizeof(T) <= sizeof(uint64_t) && std::is_trivially_copyable_v<T>)
|
||||
@@ -157,6 +159,8 @@ public:
|
||||
private:
|
||||
friend void EliminateDeadCode(const std::vector<Block*>& blocks);
|
||||
|
||||
template <typename Self>
|
||||
static auto& ArgSlot(Self& self, size_t index);
|
||||
void AddUse(Inst* used, size_t operand);
|
||||
void RemoveUse(Inst* used, size_t operand);
|
||||
void ClearArgs();
|
||||
@@ -181,4 +185,44 @@ private:
|
||||
|
||||
static_assert(sizeof(Inst) <= 120, "Inst operand and backend storage unintentionally increased");
|
||||
|
||||
// Called per operand by the IR passes and the resource walker; inline, as the build has no LTO.
|
||||
inline bool Value::IsIdentity() const {
|
||||
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Identity;
|
||||
}
|
||||
|
||||
inline bool Value::IsPhi() const {
|
||||
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Phi;
|
||||
}
|
||||
|
||||
inline Inst* Value::ResolveInstruction() const {
|
||||
EXIT_IF(type != Type::Opaque);
|
||||
return IsIdentity() ? inst->Arg(0).ResolveInstruction() : inst;
|
||||
}
|
||||
|
||||
inline Value Value::Resolve() const {
|
||||
return IsIdentity() ? inst->Arg(0).Resolve() : *this;
|
||||
}
|
||||
|
||||
inline uint32_t Value::U32() const {
|
||||
EXIT_IF(type != Type::U32);
|
||||
return imm_u32;
|
||||
}
|
||||
|
||||
inline size_t Inst::NumArgs() const {
|
||||
return num_args == PhiArity ? phi_args.size() : num_args;
|
||||
}
|
||||
|
||||
template <typename Self>
|
||||
auto& Inst::ArgSlot(Self& self, size_t index) {
|
||||
EXIT_IF(index >= self.NumArgs());
|
||||
if (self.num_args <= InlineArity) {
|
||||
return self.fixed_args[index];
|
||||
}
|
||||
return self.num_args == PhiArity ? self.phi_args[index].second : self.large_args[index];
|
||||
}
|
||||
|
||||
inline Value Inst::Arg(size_t index) const {
|
||||
return ArgSlot(*this, index);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
@@ -123,6 +123,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
|
||||
case ValueOpcode::BufferAtomicISub32:
|
||||
case ValueOpcode::BufferAtomicSMin32:
|
||||
case ValueOpcode::BufferAtomicUMin32:
|
||||
case ValueOpcode::BufferAtomicUMin64:
|
||||
case ValueOpcode::BufferAtomicSMax32:
|
||||
case ValueOpcode::BufferAtomicUMax32:
|
||||
case ValueOpcode::BufferAtomicAnd32:
|
||||
@@ -140,6 +141,7 @@ uint32_t BufferComponentCount(ValueOpcode opcode) {
|
||||
switch (opcode) {
|
||||
case ValueOpcode::BufferAtomicSwap64:
|
||||
case ValueOpcode::BufferAtomicIAdd64:
|
||||
case ValueOpcode::BufferAtomicUMin64:
|
||||
case ValueOpcode::BufferAtomicAnd64:
|
||||
case ValueOpcode::BufferAtomicOr64:
|
||||
case ValueOpcode::LoadBufferU32x2:
|
||||
|
||||
@@ -96,6 +96,7 @@ VALUE_OPCODE(WqmU64, U64, U64)
|
||||
// Core value operations used to expand architectural side effects.
|
||||
VALUE_OPCODE(SelectU1, U1, U1, U1, U1)
|
||||
VALUE_OPCODE(SelectF32, F32, U1, F32, F32)
|
||||
VALUE_OPCODE(SelectF64, F64, U1, F64, F64)
|
||||
VALUE_OPCODE(IAdd32, U32, U32, U32)
|
||||
VALUE_OPCODE(IAdd64, U64, U64, U64)
|
||||
VALUE_OPCODE(ISub32, U32, U32, U32)
|
||||
@@ -218,11 +219,13 @@ VALUE_OPCODE(FPFract64, F64, F64)
|
||||
|
||||
VALUE_OPCODE(LaneId, U32)
|
||||
VALUE_OPCODE(Ballot, U32x4, U1)
|
||||
VALUE_OPCODE(LiveMask, U32x4)
|
||||
VALUE_OPCODE(ReadFirstLane, U32, U32, U1)
|
||||
VALUE_OPCODE(ReadLane, U32, U32, U32)
|
||||
VALUE_OPCODE(SubgroupUMinU32, U32, U32)
|
||||
VALUE_OPCODE(SubgroupUMaxU32, U32, U32)
|
||||
VALUE_OPCODE(WriteLane, U32, U32, U32, U32)
|
||||
VALUE_OPCODE(Permlane16U32, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(Permlane16U32, U32, U32, U32, U32, U1, U32)
|
||||
VALUE_OPCODE(PermuteU32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BpermuteU32, U32, U32, U32, U1)
|
||||
|
||||
@@ -266,6 +269,7 @@ VALUE_OPCODE(BufferAtomicIAdd64, U64, BufferResource, U32, U32, U32, U64, U1)
|
||||
VALUE_OPCODE(BufferAtomicISub32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicSMin32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicUMin32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicUMin64, U64, BufferResource, U32, U32, U32, U64, U1)
|
||||
VALUE_OPCODE(BufferAtomicSMax32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicUMax32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicAnd32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
@@ -308,8 +312,8 @@ VALUE_OPCODE(SharedAtomicOr32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicOr64, Void, U32, U64, U1)
|
||||
VALUE_OPCODE(SharedAtomicXor32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicMaskedOr32, Void, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(DataAppend, U32, U32, U1)
|
||||
VALUE_OPCODE(DataConsume, U32, U32, U1)
|
||||
VALUE_OPCODE(DataAppend, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(DataConsume, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SwizzleU32, U32, U32, U32, U1)
|
||||
|
||||
VALUE_OPCODE(ImageQueryDimensions, U32x4, ImageResource, ImageAddress)
|
||||
|
||||
@@ -167,6 +167,16 @@ bool ReplaceBinaryIdentity(Inst& inst, Type type, uint64_t identity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether one operand is the other's negation.
|
||||
bool IsComplementPair(const Inst& inst, ValueOpcode negation) {
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg) {
|
||||
const auto* negate = Arg(inst, arg).TryInstruction();
|
||||
if (negate != nullptr && negate->GetOpcode() == negation &&
|
||||
Arg(*negate, 0) == Arg(inst, arg ^ 1u)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FoldSelect(Inst& inst) {
|
||||
const auto condition = Arg(inst, 0);
|
||||
const auto true_value = Arg(inst, 1);
|
||||
@@ -626,8 +636,9 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
||||
case ValueOpcode::BitwiseXor32:
|
||||
if (!FoldU32(inst, [opcode = inst.GetOpcode()](uint32_t a, uint32_t b) {
|
||||
return opcode == ValueOpcode::BitwiseOr32 ? a | b : a ^ b;
|
||||
})) {
|
||||
ReplaceBinaryIdentity(inst, Type::U32, 0u);
|
||||
}) && !ReplaceBinaryIdentity(inst, Type::U32, 0u) &&
|
||||
IsComplementPair(inst, ValueOpcode::BitwiseNot32)) {
|
||||
Replace(inst, Value(UINT32_MAX));
|
||||
}
|
||||
return;
|
||||
case ValueOpcode::BitwiseNot32: {
|
||||
@@ -802,6 +813,8 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
||||
Replace(inst, lhs.U1() ? lhs : rhs);
|
||||
} else if (IsImmediate(rhs, Type::U1)) {
|
||||
Replace(inst, rhs.U1() ? rhs : lhs);
|
||||
} else if (IsComplementPair(inst, ValueOpcode::LogicalNot)) {
|
||||
Replace(inst, Value(true));
|
||||
} else {
|
||||
const auto* left = lhs.TryInstruction();
|
||||
const auto* right = rhs.TryInstruction();
|
||||
@@ -892,8 +905,10 @@ private:
|
||||
if (inst.GetOpcode() != ValueOpcode::CompositeExtractU32x4 ||
|
||||
!Immediate(Arg(inst, 1), 0u)) return {};
|
||||
const auto* source = Arg(inst, 0).TryInstruction();
|
||||
return source != nullptr && source->GetOpcode() == ValueOpcode::Ballot
|
||||
? Arg(*source, 0) : Value {};
|
||||
if (source == nullptr) return {};
|
||||
// Helper lanes keep the per-lane EXEC, so the live mask projects to true.
|
||||
if (source->GetOpcode() == ValueOpcode::LiveMask) return Value(true);
|
||||
return source->GetOpcode() == ValueOpcode::Ballot ? Arg(*source, 0) : Value {};
|
||||
}
|
||||
|
||||
bool CanProject(Value value) {
|
||||
|
||||
@@ -99,89 +99,66 @@ Value GetRealValue(PhiMap& phi_map, Value source, uint32_t lane, uint32_t wave_s
|
||||
return Value(&phi);
|
||||
}
|
||||
|
||||
// A full-EXEC prefix scan with both half-wave endpoint reads is a reduction.
|
||||
// A full-EXEC prefix scan with both half-wave endpoint reads is a reduction. Wave64 first joins
|
||||
// the two rows of each half; a wave32 half is one row.
|
||||
// Fragment subgroups may have no physical invocation at either endpoint.
|
||||
class UMinReductionMatcher {
|
||||
class WaveReductionMatcher {
|
||||
public:
|
||||
explicit UMinReductionMatcher(Block* block): block(block) {}
|
||||
WaveReductionMatcher(const Block* block, ValueOpcode operation)
|
||||
: block(block), operation(operation) {}
|
||||
|
||||
Value Match(const Inst& minimum) {
|
||||
const auto* low = Get(minimum.Arg(0), ValueOpcode::ReadLane);
|
||||
const auto* high = Get(minimum.Arg(1), ValueOpcode::ReadLane);
|
||||
Value Match(const Inst& reduction, uint32_t wave_size) const {
|
||||
const auto* low = Get(reduction.Arg(0), ValueOpcode::ReadLane);
|
||||
const auto* high = Get(reduction.Arg(1), ValueOpcode::ReadLane);
|
||||
if (low == nullptr || high == nullptr || low->Arg(0).Resolve() != high->Arg(0).Resolve()) {
|
||||
return {};
|
||||
}
|
||||
const auto lane = [](Value value, uint32_t expected) {
|
||||
const auto lane = [&](Value value, uint32_t expected) {
|
||||
value = value.Resolve();
|
||||
return value.IsImmediate() && value.GetType() == Type::U32 &&
|
||||
value.U32() % 64u == expected;
|
||||
value.U32() % wave_size == expected;
|
||||
};
|
||||
if (!((lane(low->Arg(1), 31) && lane(high->Arg(1), 63)) ||
|
||||
(lane(low->Arg(1), 63) && lane(high->Arg(1), 31)))) {
|
||||
const auto half = wave_size / 2u;
|
||||
if (!((lane(low->Arg(1), half - 1u) && lane(high->Arg(1), wave_size - 1u)) ||
|
||||
(lane(low->Arg(1), wave_size - 1u) && lane(high->Arg(1), half - 1u)))) {
|
||||
return {};
|
||||
}
|
||||
const auto* joined = Get(Unwrap(low->Arg(0)), ValueOpcode::UMin32);
|
||||
if (wave_size == 32u) return MatchScan(low->Arg(0));
|
||||
const auto* joined = Get(low->Arg(0), operation);
|
||||
if (joined == nullptr) return {};
|
||||
for (size_t index = 0; index < 2; index++) {
|
||||
auto attempt = *this;
|
||||
const auto* cross = attempt.Get(attempt.Unwrap(joined->Arg(index)), ValueOpcode::Permlane16U32);
|
||||
const auto* cross = Get(joined->Arg(index), ValueOpcode::Permlane16U32);
|
||||
if (cross == nullptr) continue;
|
||||
const auto flags = cross->Flags<PermlaneFlags>();
|
||||
auto prefix = attempt.Unwrap(joined->Arg(1 - index));
|
||||
const auto flags = cross->Flags<PermlaneFlags>();
|
||||
const auto prefix = joined->Arg(1 - index).Resolve();
|
||||
if (!flags.x16 || flags.fetch_inactive || !flags.bound_control ||
|
||||
cross->Arg(1).Resolve() != Value(UINT32_MAX) ||
|
||||
cross->Arg(2).Resolve() != Value(UINT32_MAX) ||
|
||||
!attempt.FullExec(cross->Arg(3)) || attempt.Unwrap(cross->Arg(0)) != prefix) {
|
||||
cross->Arg(2).Resolve() != Value(UINT32_MAX) || !FullExec(cross->Arg(3)) ||
|
||||
cross->Arg(0).Resolve() != prefix) {
|
||||
continue;
|
||||
}
|
||||
for (const uint32_t shift: {8u, 4u, 2u, 1u}) {
|
||||
prefix = attempt.MatchRow(prefix, shift);
|
||||
}
|
||||
prefix = attempt.Unwrap(prefix);
|
||||
if (prefix.IsEmpty() || attempt.saved_exec.IsEmpty()) return prefix;
|
||||
const auto* seed = attempt.Get(prefix, ValueOpcode::SelectU32);
|
||||
if (seed != nullptr && seed->Arg(2).Resolve() == Value(UINT32_MAX)) {
|
||||
const auto predicate = seed->Arg(0).Resolve();
|
||||
const auto* subset = attempt.Get(predicate, ValueOpcode::LogicalAnd);
|
||||
if (predicate == attempt.saved_exec ||
|
||||
(subset != nullptr && (subset->Arg(0).Resolve() == attempt.saved_exec ||
|
||||
subset->Arg(1).Resolve() == attempt.saved_exec))) {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
if (const auto seed = MatchScan(prefix); !seed.IsEmpty()) return seed;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private:
|
||||
// The row scan's input.
|
||||
Value MatchScan(Value prefix) const {
|
||||
for (const uint32_t shift: {8u, 4u, 2u, 1u}) {
|
||||
prefix = MatchRow(prefix, shift);
|
||||
}
|
||||
return prefix;
|
||||
}
|
||||
|
||||
Inst* Get(Value value, ValueOpcode opcode) const {
|
||||
auto* inst = value.Resolve().TryInstruction();
|
||||
return inst != nullptr && inst->Parent() == block && inst->GetOpcode() == opcode
|
||||
? inst : nullptr;
|
||||
}
|
||||
|
||||
bool FullExec(Value predicate) {
|
||||
predicate = predicate.Resolve();
|
||||
if (predicate == Value(true)) return true;
|
||||
const auto* either = Get(predicate, ValueOpcode::LogicalOr);
|
||||
if (either == nullptr) return false;
|
||||
for (size_t index = 0; index < 2; index++) {
|
||||
const auto* negate = Get(either->Arg(index), ValueOpcode::LogicalNot);
|
||||
if (negate == nullptr || negate->Arg(0).Resolve() != either->Arg(1 - index).Resolve()) continue;
|
||||
const auto original = negate->Arg(0).Resolve();
|
||||
if (saved_exec.IsEmpty()) saved_exec = original;
|
||||
return saved_exec == original;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Value Unwrap(Value value) {
|
||||
while (const auto* select = Get(value, ValueOpcode::SelectU32)) {
|
||||
if (!FullExec(select->Arg(0))) break;
|
||||
value = select->Arg(1);
|
||||
}
|
||||
return value.Resolve();
|
||||
}
|
||||
// Constant propagation folds the x | ~x EXEC of s_orn2_saveexec to true.
|
||||
static bool FullExec(Value predicate) { return predicate.Resolve() == Value(true); }
|
||||
|
||||
static bool IsRowShift(const Inst& inst, uint32_t shift) {
|
||||
const auto flags = inst.Flags<DppMoveFlags>();
|
||||
@@ -189,22 +166,24 @@ private:
|
||||
flags.bank_mask == 0xf && !flags.fetch_inactive && !flags.bound_control && !flags.dpp8;
|
||||
}
|
||||
|
||||
Value MatchRow(Value value, uint32_t shift) {
|
||||
const auto* update = Get(Unwrap(value), ValueOpcode::DppUpdateU32);
|
||||
Value MatchRow(Value value, uint32_t shift) const {
|
||||
const auto* update = Get(value, ValueOpcode::DppUpdateU32);
|
||||
if (update == nullptr || !IsRowShift(*update, shift) || !FullExec(update->Arg(2))) return {};
|
||||
const auto* minimum = Get(update->Arg(0), ValueOpcode::UMin32);
|
||||
if (minimum == nullptr) return {};
|
||||
const auto* combined = Get(update->Arg(0), operation);
|
||||
if (combined == nullptr) return {};
|
||||
for (size_t index = 0; index < 2; index++) {
|
||||
const auto* move = Get(minimum->Arg(index), ValueOpcode::DppMoveU32);
|
||||
const auto* move = Get(combined->Arg(index), ValueOpcode::DppMoveU32);
|
||||
if (move == nullptr || !IsRowShift(*move, shift) || !FullExec(move->Arg(1))) continue;
|
||||
const auto previous = Unwrap(minimum->Arg(1 - index));
|
||||
if (Unwrap(move->Arg(0)) == previous && Unwrap(update->Arg(1)) == previous) return previous;
|
||||
const auto previous = combined->Arg(1 - index).Resolve();
|
||||
if (move->Arg(0).Resolve() == previous && update->Arg(1).Resolve() == previous) {
|
||||
return previous;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Block* block;
|
||||
Value saved_exec;
|
||||
const Block* block;
|
||||
ValueOpcode operation;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -217,18 +196,22 @@ ReadLaneStats EliminateReadLane(Program& program, uint32_t wave_size) {
|
||||
|
||||
for (auto* block: program.blocks) {
|
||||
for (auto& inst: *block) {
|
||||
if (wave_size == 64u && inst.GetOpcode() == ValueOpcode::UMin32) {
|
||||
const auto seed = UMinReductionMatcher(block).Match(inst);
|
||||
const auto opcode = inst.GetOpcode();
|
||||
if (opcode == ValueOpcode::UMin32 || opcode == ValueOpcode::UMax32) {
|
||||
const auto seed = WaveReductionMatcher(block, opcode).Match(inst, wave_size);
|
||||
if (!seed.IsEmpty()) {
|
||||
const auto position = std::find_if(block->begin(), block->end(),
|
||||
[&](const Inst& candidate) { return &candidate == &inst; });
|
||||
auto& reduction = *block->PrependNewInst(position, ValueOpcode::SubgroupUMinU32, {seed});
|
||||
const auto reduce = opcode == ValueOpcode::UMin32
|
||||
? ValueOpcode::SubgroupUMinU32
|
||||
: ValueOpcode::SubgroupUMaxU32;
|
||||
auto& reduction = *block->PrependNewInst(position, reduce, {seed});
|
||||
inst.ReplaceUsesWith(Value(&reduction));
|
||||
stats.rewritten_reductions++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inst.GetOpcode() != ValueOpcode::ReadLane) {
|
||||
if (opcode != ValueOpcode::ReadLane) {
|
||||
continue;
|
||||
}
|
||||
const auto selector = inst.Arg(1).Resolve();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "common/assert.h"
|
||||
#include "graphics/guest_gpu/gpu_format.h"
|
||||
#include "graphics/shader/recompiler/BufferFormat.h"
|
||||
#include "graphics/shader/recompiler/ir/Serialize.h"
|
||||
#include "graphics/shader/recompiler/ir/ShaderIR.h"
|
||||
#include "graphics/shader/shaderBindings.h"
|
||||
|
||||
@@ -330,6 +331,18 @@ bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::In
|
||||
keys.resize(count);
|
||||
return true;
|
||||
};
|
||||
// Reads the key of each record whose rank is set.
|
||||
const auto read_ranks = [&](uint64_t base, uint64_t size, uint64_t first, uint64_t step,
|
||||
uint32_t ranks) {
|
||||
keys.reserve(std::popcount(ranks));
|
||||
for (; ranks != 0u; ranks &= ranks - 1u) {
|
||||
const auto offset = first + std::countr_zero(ranks) * step;
|
||||
uint32_t key = 0;
|
||||
if (offset > UINT32_MAX || !read_selector(base, size, offset, {&key, 1})) return false;
|
||||
keys.push_back(key);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (selector->rows) {
|
||||
const auto& rows = *selector->rows;
|
||||
DescriptorValue count_value;
|
||||
@@ -376,19 +389,21 @@ bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::In
|
||||
}
|
||||
} else if (selector->bitset) {
|
||||
ShaderBufferResource material;
|
||||
std::array<uint32_t, 4> masks {};
|
||||
std::array<uint32_t, 8> storage {};
|
||||
const auto record_words = std::min<size_t>(selector->stride / 4u, storage.size());
|
||||
const auto masks = std::span(storage).first(record_words);
|
||||
uint32_t first = 0, count = 0;
|
||||
if (!DecodeBufferDescriptor(material_value, material) ||
|
||||
!WalkedValue(program, indirect.selector_first, first) ||
|
||||
!EvaluateIndirectCount(program, indirect, runtime, count) || count != masks.size() * 32u ||
|
||||
selector->stride != sizeof(masks) || selector->offset != 0u ||
|
||||
material.Type() != 0u || material.Stride() != sizeof(masks) ||
|
||||
selector->stride != masks.size_bytes() || selector->offset != 0u ||
|
||||
material.Type() != 0u || material.Stride() != selector->stride ||
|
||||
material.SwizzleEnabled() || material.AddTid() || material.OutOfBounds() != 0u ||
|
||||
(material.Base48() & 3u) != 0u) return false;
|
||||
if (material.Format() != Prospero::BufferFormat::kInvalid) {
|
||||
const auto words = material.GetSize() / sizeof(uint32_t);
|
||||
// Bitmap storage can grow with resolution while its candidate domain stays
|
||||
// at 128 keys. Bound scratch memory, preserving each coherent read's capture.
|
||||
// Bitmap storage can grow with resolution while its candidate domain stays at 32 keys
|
||||
// per record word. Bound scratch memory, preserving each coherent read's capture.
|
||||
for (uint64_t first_word = 0; first_word < words; first_word += keys.size()) {
|
||||
keys.resize(std::min<uint64_t>(words - first_word, MaxIndirectDescriptorProbes));
|
||||
if (!ReadScalarTable(material.Base48(), material.GetSize(), first_word * 4u,
|
||||
@@ -444,12 +459,35 @@ bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::In
|
||||
uint64_t {first} + count > material.NumRecords())) return false;
|
||||
const auto last = count != 0u ? uint64_t {first} + count - 1u : 0u;
|
||||
if (last > UINT32_MAX) return false;
|
||||
// Out-of-bounds scalar reads return zero.
|
||||
const auto end = count != 0u ? last * step + selector->offset + 4u : 0u;
|
||||
if (end > uint64_t {UINT32_MAX} + 1u ||
|
||||
(indirect.workgroup_axis == UINT32_MAX && end > size))
|
||||
return false;
|
||||
if (!read_keys(base, size, uint64_t {first} * step + selector->offset, step, count))
|
||||
if (end > uint64_t {UINT32_MAX} + 1u) return false;
|
||||
const auto start = uint64_t {first} * step + selector->offset;
|
||||
if (selector->ranks) {
|
||||
// A scan selects the ranks some word of its mask's address class sets.
|
||||
const auto& mask = *selector->ranks;
|
||||
DescriptorValue mask_value;
|
||||
ShaderBufferResource masks;
|
||||
if (count > 32u || !WalkedDescriptor(program, mask.source, mask_value) ||
|
||||
!DecodeBufferDescriptor(mask_value, masks)) return false;
|
||||
// Each chunk holds whole strides of the power-of-two class, or one of its words.
|
||||
const auto words = mask.stride / 4u;
|
||||
const auto chunk =
|
||||
words <= MaxIndirectDescriptorProbes ? MaxIndirectDescriptorProbes : 1u;
|
||||
uint32_t ranks = 0;
|
||||
for (auto at = uint64_t {mask.offset}; at + 4u <= masks.GetSize();
|
||||
at += uint64_t {std::max(chunk, words)} * 4u) {
|
||||
keys.resize(std::min<uint64_t>((masks.GetSize() - at) / 4u, chunk));
|
||||
if (!ReadScalarTable(masks.Base48(), masks.GetSize(), at, runtime, keys))
|
||||
return false;
|
||||
for (size_t word = 0; word < keys.size(); word += words) ranks |= keys[word];
|
||||
}
|
||||
keys.clear();
|
||||
if (count < 32u) ranks &= (1u << count) - 1u;
|
||||
if (!read_ranks(base, size, start, step, ranks)) return false;
|
||||
} else if (!read_keys(base, size, start, step, count)) {
|
||||
return false;
|
||||
}
|
||||
} else if (!indirect.selector_mask.IsEmpty()) {
|
||||
uint32_t mask = 0;
|
||||
uint32_t count = 0;
|
||||
@@ -461,18 +499,8 @@ bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::In
|
||||
if (count < 32u) mask &= (1u << count) - 1u;
|
||||
const auto material_base =
|
||||
(static_cast<uint64_t>(material_value.dwords[1]) << 32u) | material_value.dwords[0];
|
||||
keys.reserve(std::popcount(mask));
|
||||
while (mask != 0u) {
|
||||
const auto index = std::countr_zero(mask);
|
||||
const auto offset = static_cast<uint64_t>(selector->offset) +
|
||||
static_cast<uint64_t>(index) * selector->stride;
|
||||
if (offset > UINT32_MAX) return false;
|
||||
uint32_t key = 0;
|
||||
if (!read_selector(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
|
||||
{&key, 1})) return false;
|
||||
keys.push_back(key);
|
||||
mask &= mask - 1u;
|
||||
}
|
||||
if (!read_ranks(material_base, UINT64_MAX, selector->offset, selector->stride, mask))
|
||||
return false;
|
||||
} else if (selector->stride == 0u) {
|
||||
ShaderBufferResource material;
|
||||
if (!DecodeBufferDescriptor(material_value, material) ||
|
||||
@@ -502,6 +530,7 @@ bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::In
|
||||
if (selector->bit_offset != 0u || selector->bit_mask != UINT32_MAX || selector->maximum != UINT32_MAX)
|
||||
for (auto& key: keys)
|
||||
key = std::min((key >> selector->bit_offset) & selector->bit_mask, selector->maximum);
|
||||
if (selector->excluded) std::erase(keys, *selector->excluded);
|
||||
std::ranges::sort(keys);
|
||||
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
||||
}
|
||||
@@ -755,6 +784,12 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
std::iota(keys.begin(), keys.end(), first_key);
|
||||
if (!keys.empty() && keys.back() < keys.front())
|
||||
std::rotate(keys.begin(), keys.begin() + (UINT32_MAX - first_key + 1ull), keys.end());
|
||||
if (indirect.workgroup_shift != 0u) {
|
||||
for (auto& key: keys)
|
||||
key = static_cast<uint32_t>(uint64_t {key} * indirect.workgroup_multiplier >>
|
||||
indirect.workgroup_shift);
|
||||
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
||||
}
|
||||
} else {
|
||||
if (!indirect.selector_mask.IsEmpty() && table_value.dword_count != 2u) return false;
|
||||
if (!selector_keys->resolved) {
|
||||
@@ -1040,6 +1075,26 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
||||
return dimension == Decoder::ImageDimension::Dim1D ||
|
||||
dimension == Decoder::ImageDimension::Dim2D;
|
||||
};
|
||||
// The descriptor's type selects the address layout. A 2D or 3D candidate reads the leading
|
||||
// coordinates of an equal or larger dimension, but an array with more than one slice reads
|
||||
// the next as its slice. Derivative, LOD, clamp and mip operands shift with the dimension.
|
||||
const auto leading = [&](Decoder::ImageDimension dimension, const DescriptorValue& value) {
|
||||
const bool sliced =
|
||||
static_cast<Prospero::ImageType>((value.dwords[3] >> 28u) & 0xfu) ==
|
||||
Prospero::ImageType::kColor2DArray &&
|
||||
(value.dwords[4] & 0x1fffu) != ((value.dwords[4] >> 16u) & 0x1fffu);
|
||||
return !sliced &&
|
||||
(dimension == Decoder::ImageDimension::Dim2D || dimension == base.dimension);
|
||||
};
|
||||
const bool leading_coordinates = (base.dimension == Decoder::ImageDimension::Dim2D ||
|
||||
base.dimension == Decoder::ImageDimension::Dim3D) &&
|
||||
leading(image_class.dimension, snapshot.images[exemplar]) &&
|
||||
std::ranges::none_of(program.memory_info, [&](const MemoryInfo& memory) {
|
||||
return memory.kind == ResourceKind::Image && memory.resource == root_index &&
|
||||
(memory.image_has_mip || (memory.image_sample_flags &
|
||||
(Decoder::ImageSampleFlagLod | Decoder::ImageSampleFlagDerivative |
|
||||
Decoder::ImageSampleFlagLodClamp)) != 0u);
|
||||
});
|
||||
for (uint32_t candidate = 0; candidate < specialization.images.size(); candidate++) {
|
||||
auto& image = specialization.images[candidate];
|
||||
if (image.indirect_root != root_index) {
|
||||
@@ -1059,8 +1114,10 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
||||
base.dimension == Decoder::ImageDimension::Dim2D &&
|
||||
!image.cube && !image_class.cube && is_1d_or_2d(image.dimension) &&
|
||||
is_1d_or_2d(image_class.dimension);
|
||||
const bool prefix_coordinates =
|
||||
leading_coordinates && leading(image.dimension, snapshot.images[candidate]);
|
||||
if ((!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension)) &&
|
||||
!unfiltered_coordinates) ||
|
||||
!unfiltered_coordinates && !prefix_coordinates) ||
|
||||
image.conversion_format != image_class.conversion_format ||
|
||||
(!storage_swizzle && image.shader_swizzle != image_class.shader_swizzle)) {
|
||||
return SpecializationFail(
|
||||
@@ -1163,14 +1220,16 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, co
|
||||
block.sources.push_back(program.info.buffers.at(memory.resource).source);
|
||||
} else {
|
||||
block.sources.push_back(program.info.images.at(memory.resource).source);
|
||||
block.images.push_back(memory.resource);
|
||||
if (image.needs_sampler) {
|
||||
block.sources.push_back(program.info.samplers.at(memory.sampler).source);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::ranges::sort(block.sources);
|
||||
block.sources.erase(std::unique(block.sources.begin(), block.sources.end()),
|
||||
block.sources.end());
|
||||
for (auto* list: {&block.sources, &block.images}) {
|
||||
std::ranges::sort(*list);
|
||||
list->erase(std::unique(list->begin(), list->end()), list->end());
|
||||
}
|
||||
}
|
||||
for (auto& block: blocks) block.condition = predicate(block.condition);
|
||||
if (std::ranges::none_of(
|
||||
@@ -1311,6 +1370,17 @@ static UniformFillPlan AnalyzeUniformFill(const Program& program) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// State an extracted or loaded plan derives from its fields: one key set per selector group, which
|
||||
// extraction numbers densely in order of first use (false when they are not), and the walker.
|
||||
static bool DerivePlanState(ResourcePlan& plan) {
|
||||
for (const auto group: plan.selector_groups) {
|
||||
if (group == plan.selector_keys.size()) plan.selector_keys.emplace_back();
|
||||
if (group != UINT32_MAX && group >= plan.selector_keys.size()) return false;
|
||||
}
|
||||
if (plan.srt_plan_complete && plan.resource_tracking_complete) CompileSrtWalker(plan);
|
||||
return true;
|
||||
}
|
||||
|
||||
ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
ResourcePlan plan;
|
||||
plan.stage = program.stage;
|
||||
@@ -1391,17 +1461,18 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
}
|
||||
// Destination tables share keys when their complete selector domains agree.
|
||||
// Resolve those domains lazily at dispatch time, without repeated guest reads.
|
||||
std::vector<uint32_t> group_sources; // The first source of each group.
|
||||
plan.selector_groups.resize(plan.descriptor_sources.size(), UINT32_MAX);
|
||||
for (uint32_t source = 0; source < plan.descriptor_sources.size(); ++source) {
|
||||
const auto& descriptor = program.descriptor_sources[source].indirect_descriptor;
|
||||
if (!descriptor || !descriptor->selector) continue;
|
||||
const auto& a = *descriptor;
|
||||
uint32_t group = 0;
|
||||
for (; group < plan.selector_keys.size(); ++group) {
|
||||
const auto& b = *program.descriptor_sources[plan.selector_keys[group].source].indirect_descriptor;
|
||||
for (; group < group_sources.size(); ++group) {
|
||||
const auto& b = *program.descriptor_sources[group_sources[group]].indirect_descriptor;
|
||||
if (EquivalentSelectorDomain(program, a, b)) break;
|
||||
}
|
||||
if (group == plan.selector_keys.size()) plan.selector_keys.push_back({.source = source});
|
||||
if (group == group_sources.size()) group_sources.push_back(source);
|
||||
plan.selector_groups[source] = group;
|
||||
}
|
||||
plan.srt_reads.reserve(program.srt_reads.size());
|
||||
@@ -1491,7 +1562,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
capture_indirect_reads || capture_control_reads || !plan.source_reads.empty();
|
||||
if (capture_indirect_reads || !plan.source_reads.empty())
|
||||
plan.resource_tracking_complete &= !program.has_address_writes;
|
||||
if (plan.srt_plan_complete && plan.resource_tracking_complete) CompileSrtWalker(plan);
|
||||
DerivePlanState(plan);
|
||||
return plan;
|
||||
}
|
||||
|
||||
@@ -1632,7 +1703,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
}
|
||||
if (source->indirect_descriptor.has_value()) {
|
||||
snapshot.images[i] = {.dword_count = 8u};
|
||||
if (!WalkedActive(program, image.source)) {
|
||||
if (!WalkedActive(program, image.source) || !WalkedImageActive(program, i)) {
|
||||
continue;
|
||||
}
|
||||
const auto& indirect = *source->indirect_descriptor;
|
||||
@@ -1652,7 +1723,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
if (!evaluate(image.source, snapshot.images[i])) {
|
||||
return false;
|
||||
}
|
||||
if (!ValidImageDescriptor(snapshot.images[i], image.r128)) {
|
||||
// An image listed only by unreached blocks reads zero, not its source's descriptor.
|
||||
if (!WalkedImageActive(program, i) ||
|
||||
!ValidImageDescriptor(snapshot.images[i], image.r128)) {
|
||||
snapshot.images[i].dwords.fill(0);
|
||||
}
|
||||
}
|
||||
@@ -1698,8 +1771,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
}
|
||||
|
||||
uint32_t ImageHeapKind(const ImageResource& root, DescriptorValue& value) {
|
||||
// Mirrors BuildResourceSpecialization for sampled roots: entries it would reject, and
|
||||
// entries that need shader-side conversion, read as null instead of failing the draw.
|
||||
// Mirrors BuildResourceSpecialization for sampled roots: entries it would reject, entries of
|
||||
// another dimension and entries that need shader-side conversion read as null instead of
|
||||
// failing the draw.
|
||||
const auto format = static_cast<Prospero::BufferFormat>((value.dwords[1] >> 20u) & 0x1ffu);
|
||||
const auto tile = static_cast<Prospero::TileMode>((value.dwords[3] >> 20u) & 0x1fu);
|
||||
const auto numeric_class =
|
||||
@@ -1986,4 +2060,86 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
image_remap.Apply(images);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using ControlPhis = decltype(ResourcePlan::control_phi_predecessors);
|
||||
|
||||
// Instructions come first, so that any value can refer to them by index, then their arguments.
|
||||
// Plan Phis have no blocks; a control Phi lists the plan blocks of its arguments after them.
|
||||
void IoValues(Writer& writer, const std::list<Inst>& values, const ControlPhis& phis) {
|
||||
IoAll(writer, static_cast<uint32_t>(values.size()));
|
||||
for (const auto& inst: values) {
|
||||
writer.index.emplace(&inst, static_cast<uint32_t>(writer.index.size()));
|
||||
IoAll(writer, inst.GetOpcode(), inst.Flags<uint64_t>());
|
||||
}
|
||||
for (const auto& inst: values) {
|
||||
if (inst.GetOpcode() != ValueOpcode::Phi) {
|
||||
for (size_t index = 0; index < inst.NumArgs(); ++index) IoAll(writer, inst.Arg(index));
|
||||
continue;
|
||||
}
|
||||
std::vector<Value> args;
|
||||
for (size_t index = 0; index < inst.NumArgs(); ++index) {
|
||||
EXIT_IF(inst.PhiBlock(index) != nullptr);
|
||||
args.push_back(inst.Arg(index));
|
||||
}
|
||||
const auto found = phis.find(&inst);
|
||||
IoAll(writer, args, found != phis.end() ? found->second : std::vector<uint32_t> {});
|
||||
}
|
||||
}
|
||||
|
||||
void IoValues(Reader& reader, std::list<Inst>& values, ControlPhis& phis) {
|
||||
uint32_t count = 0;
|
||||
Io(reader, count);
|
||||
count = reader.Count(count);
|
||||
for (uint32_t index = 0; index < count && reader.ok; ++index) {
|
||||
ValueOpcode opcode {};
|
||||
uint64_t flags = 0;
|
||||
IoAll(reader, opcode, flags);
|
||||
reader.ok = reader.ok && opcode < ValueOpcode::Count;
|
||||
if (reader.ok) reader.insts.push_back(&values.emplace_back(opcode, flags));
|
||||
}
|
||||
for (auto* inst: reader.insts) {
|
||||
if (inst->GetOpcode() != ValueOpcode::Phi) {
|
||||
for (size_t index = 0; index < inst->NumArgs(); ++index) {
|
||||
Value arg;
|
||||
Io(reader, arg);
|
||||
inst->SetArg(index, arg);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
std::vector<Value> args;
|
||||
std::vector<uint32_t> predecessors;
|
||||
IoAll(reader, args, predecessors);
|
||||
for (const auto arg: args) inst->AddPhiOperand(nullptr, arg);
|
||||
if (!predecessors.empty()) phis.emplace(inst, std::move(predecessors));
|
||||
}
|
||||
}
|
||||
|
||||
// Binds every plan field: one added later fails to compile here until it is stored or derived.
|
||||
template <class S, class P> void IoPlan(S& io, P& plan) {
|
||||
auto& [stage, shader_hash, user_data_base, user_data_count, values, memory_info, descriptor_sources,
|
||||
selector_groups, control_flow, control_phis, srt_reads, source_reads, capture_specialization_reads,
|
||||
srt_plan_complete, resource_tracking_complete, info, uniform_fill,
|
||||
// Derived on load.
|
||||
srt_code, srt_user_data, srt_captures, srt_scratch, material_keys, selector_keys] = plan;
|
||||
IoValues(io, values, control_phis);
|
||||
IoAll(io, stage, shader_hash, user_data_base, user_data_count, memory_info, descriptor_sources,
|
||||
selector_groups, control_flow, srt_reads, source_reads, capture_specialization_reads,
|
||||
srt_plan_complete, resource_tracking_complete, info, uniform_fill);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<uint8_t> SaveResourcePlan(const ResourcePlan& plan) {
|
||||
Writer writer;
|
||||
IoPlan(writer, plan);
|
||||
return std::move(writer.bytes);
|
||||
}
|
||||
|
||||
bool LoadResourcePlan(std::span<const uint8_t> bytes, ResourcePlan& plan) {
|
||||
Reader reader {bytes};
|
||||
IoPlan(reader, plan);
|
||||
return reader.ok && reader.bytes.empty() && DerivePlanState(plan);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
@@ -53,6 +53,11 @@ struct ResourceSpecialization {
|
||||
// its values and is independent of the translated shader CFG.
|
||||
ResourcePlan ExtractResourcePlan(const Program& program);
|
||||
|
||||
// Shader cache record of a plan; loading it into a default-constructed plan rebuilds the walker
|
||||
// and scratch state like extraction. A failed load leaves a plan that must not be used.
|
||||
std::vector<uint8_t> SaveResourcePlan(const ResourcePlan& plan);
|
||||
bool LoadResourcePlan(std::span<const uint8_t> bytes, ResourcePlan& plan);
|
||||
|
||||
// Refreshes cached resources and specialization in place. A failed refresh must not be used.
|
||||
bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime,
|
||||
ResourceSnapshot& snapshot, ResourceSpecialization& specialization);
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <fmt/format.h>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
@@ -1014,7 +1016,9 @@ private:
|
||||
(!shared_image_heap && (a.table_offset != b.table_offset ||
|
||||
a.table_immediate != b.table_immediate ||
|
||||
a.table_stride != b.table_stride)) || a.indexed != b.indexed ||
|
||||
a.workgroup_axis != b.workgroup_axis || a.sources != b.sources ||
|
||||
a.workgroup_axis != b.workgroup_axis ||
|
||||
a.workgroup_multiplier != b.workgroup_multiplier ||
|
||||
a.workgroup_shift != b.workgroup_shift || a.sources != b.sources ||
|
||||
a.signed_count != b.signed_count ||
|
||||
!EquivalentValue(m_program, a.key_count, b.key_count) ||
|
||||
a.selector_first.IsEmpty() != b.selector_first.IsEmpty() ||
|
||||
@@ -1136,31 +1140,63 @@ private:
|
||||
LaneQuantifier lanes = LaneQuantifier::All;
|
||||
};
|
||||
|
||||
// The immediate that edge proves value differs from on every lane, if any.
|
||||
std::optional<uint32_t> ExcludedBy(const EdgePredicate& edge, Value value) const {
|
||||
if (edge.lanes != LaneQuantifier::All) return {};
|
||||
const auto* test = edge.condition.TryInstruction();
|
||||
if (test != nullptr && test->NumArgs() == 2u &&
|
||||
((test->GetOpcode() == ValueOpcode::INotEqual32 && edge.positive) ||
|
||||
(test->GetOpcode() == ValueOpcode::IEqual32 && !edge.positive))) {
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg) {
|
||||
uint32_t immediate;
|
||||
if (ImmediateU32(test->Arg(arg), immediate) &&
|
||||
EquivalentValue(m_program, test->Arg(arg ^ 1u), value)) return immediate;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool NonzeroOnEntry(Value value, const Block* block) const {
|
||||
if (block == nullptr) return false;
|
||||
bool witnessed = false;
|
||||
const bool guarded = GuardedOnEntry(block, [](const Block* current) {
|
||||
return current->ImmPredecessors().size() != 1u;
|
||||
}, [&](const EdgePredicate& edge) {
|
||||
if (edge.lanes != LaneQuantifier::All) return false;
|
||||
const auto* test = edge.condition.TryInstruction();
|
||||
if (test != nullptr && test->NumArgs() == 2u &&
|
||||
((test->GetOpcode() == ValueOpcode::INotEqual32 && edge.positive) ||
|
||||
(test->GetOpcode() == ValueOpcode::IEqual32 && !edge.positive))) {
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg) {
|
||||
uint32_t immediate;
|
||||
if (ImmediateU32(test->Arg(arg), immediate) && immediate == 0u &&
|
||||
EquivalentValue(m_program, test->Arg(arg ^ 1u), value)) return witnessed = true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return ExcludedBy(edge, value) == 0u && (witnessed = true);
|
||||
});
|
||||
return guarded && witnessed;
|
||||
}
|
||||
|
||||
uint32_t WorkgroupAxis(Value key) const {
|
||||
// The key that every path from its definition to block rules out, if any.
|
||||
std::optional<uint32_t> ExcludedKeyOnEntry(Value key, const Block* block) const {
|
||||
const auto* definition = key.Resolve().TryInstruction();
|
||||
if (definition == nullptr) return {};
|
||||
std::optional<uint32_t> excluded;
|
||||
const auto defined = [&](const Block* current) { return current == definition->Parent(); };
|
||||
const bool guarded = GuardedOnEntry(block, defined, [&](const EdgePredicate& edge) {
|
||||
const auto immediate = ExcludedBy(edge, key);
|
||||
if (!immediate || (excluded && excluded != immediate)) return false;
|
||||
excluded = immediate;
|
||||
return true;
|
||||
});
|
||||
return guarded ? excluded : std::nullopt;
|
||||
}
|
||||
|
||||
// The workgroup ID axis of key; with first, also of a runtime first + workgroup ID sum.
|
||||
uint32_t WorkgroupAxis(Value key, Value* first = nullptr) const {
|
||||
const auto* builtin = key.Resolve().TryInstruction();
|
||||
uint32_t axis = UINT32_MAX;
|
||||
if (first != nullptr && builtin != nullptr && builtin->GetOpcode() == ValueOpcode::IAdd32) {
|
||||
for (uint32_t arm = 0; arm < 2u; ++arm) {
|
||||
axis = WorkgroupAxis(builtin->Arg(arm));
|
||||
const auto base = builtin->Arg(arm ^ 1u);
|
||||
if (axis == UINT32_MAX ||
|
||||
!ValidateRuntimeValue(m_program, base, RuntimeValueType::Integer)) continue;
|
||||
*first = base;
|
||||
return axis;
|
||||
}
|
||||
return UINT32_MAX;
|
||||
}
|
||||
if (m_program.stage != ShaderType::Compute || builtin == nullptr ||
|
||||
builtin->GetOpcode() != ValueOpcode::GetBuiltin || builtin->NumArgs() != 2u ||
|
||||
builtin->Arg(0) != Value(static_cast<uint32_t>(StageInputKind::WorkgroupId)) ||
|
||||
@@ -1168,6 +1204,27 @@ private:
|
||||
return axis;
|
||||
}
|
||||
|
||||
// Matches key = (first + workgroup ID) * multiplier >> shift with shift >= 32; first and the
|
||||
// division are optional.
|
||||
bool MatchWorkgroupKey(Value key, DescriptorSource::IndirectDescriptor& indirect) const {
|
||||
uint32_t bits = 0, multiplier = 0;
|
||||
const auto* shift = key.Resolve().TryInstruction();
|
||||
if (shift != nullptr && shift->GetOpcode() == ValueOpcode::ShiftRightLogical32) {
|
||||
const auto* high = shift->Arg(0).Resolve().TryInstruction();
|
||||
if (high != nullptr && high->GetOpcode() == ValueOpcode::UMulHi &&
|
||||
ImmediateU32(shift->Arg(1), bits) && bits < 32u &&
|
||||
ImmediateU32(high->Arg(1), multiplier)) {
|
||||
key = high->Arg(0);
|
||||
bits += 32u;
|
||||
}
|
||||
}
|
||||
indirect.workgroup_axis = WorkgroupAxis(key, &indirect.selector_first);
|
||||
if (indirect.workgroup_axis == UINT32_MAX) return false;
|
||||
indirect.workgroup_multiplier = multiplier;
|
||||
indirect.workgroup_shift = bits;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MatchTableOffset(Value value, Value& key, uint32_t& offset, uint32_t& stride) const {
|
||||
offset = 0;
|
||||
for (;;) {
|
||||
@@ -1672,6 +1729,8 @@ private:
|
||||
uint32_t initial_arm;
|
||||
};
|
||||
|
||||
static bool ZeroBased(uint32_t initial, uint32_t step) { return initial == 0u && step == 1u; }
|
||||
|
||||
std::optional<Induction> MatchInduction(Value key, const auto& accepts_step) const {
|
||||
key = key.Resolve();
|
||||
const auto* phi = key.TryInstruction();
|
||||
@@ -1733,8 +1792,7 @@ private:
|
||||
}
|
||||
|
||||
uint32_t PostTestLoopMaximum(Value key) const {
|
||||
const auto induction = MatchInduction(key,
|
||||
[](uint32_t initial, uint32_t step) { return initial == 0u && step == 1u; });
|
||||
const auto induction = MatchInduction(key, ZeroBased);
|
||||
if (!induction) return UINT32_MAX;
|
||||
const auto [phi, step, initial_arm] = *induction;
|
||||
const auto* incoming = phi->PhiBlock(initial_arm ^ 1u);
|
||||
@@ -1767,8 +1825,7 @@ private:
|
||||
|
||||
const Inst* BoundedLoop(Value key, const Block* use, const auto& accepts_bound,
|
||||
LaneQuantifier lanes = LaneQuantifier::Any) const {
|
||||
return BoundedInduction(key, use,
|
||||
[](uint32_t initial, uint32_t step) { return initial == 0u && step == 1u; },
|
||||
return BoundedInduction(key, use, ZeroBased,
|
||||
[&](const Inst& compare, uint32_t initial) {
|
||||
return (compare.GetOpcode() == ValueOpcode::SLessThan32 ||
|
||||
compare.GetOpcode() == ValueOpcode::ULessThan32) &&
|
||||
@@ -1793,6 +1850,29 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// A zero-based key that only per-lane data bounds takes the count all uniform zero-based
|
||||
// loops in the program share, assuming each lane's count stays within it.
|
||||
bool ProgramLoopCount(Value key, DescriptorSource::IndirectDescriptor& indirect) const {
|
||||
if (!MatchInduction(key, ZeroBased) || BoundedLoop(key, nullptr) != nullptr) return false;
|
||||
const Inst* count = nullptr;
|
||||
for (auto* block: m_program.blocks) {
|
||||
for (auto& phi: *block) {
|
||||
if (phi.GetOpcode() != ValueOpcode::Phi) break;
|
||||
const auto* compare = BoundedLoop(Value(&phi), nullptr);
|
||||
if (compare == nullptr) continue;
|
||||
if (count != nullptr && !EquivalentValue(m_program, compare->Arg(1), count->Arg(1)))
|
||||
return false;
|
||||
count = compare;
|
||||
}
|
||||
}
|
||||
if (count == nullptr) return false;
|
||||
std::printf("Warning: shader 0x%016llx bounds a per-lane table loop by its uniform loops' "
|
||||
"count\n", static_cast<unsigned long long>(m_program.shader_hash));
|
||||
indirect.key_count = count->Arg(1);
|
||||
indirect.signed_count = count->GetOpcode() == ValueOpcode::SLessThan32;
|
||||
return true;
|
||||
}
|
||||
|
||||
Value ActiveValue(Value value, Value guard) const {
|
||||
value = value.Resolve();
|
||||
while (const auto* select = value.TryInstruction()) {
|
||||
@@ -1829,6 +1909,10 @@ private:
|
||||
}
|
||||
|
||||
bool Dominates(const Block* initial, const Block* use, Value witness = {}) const {
|
||||
// The proven local-zero invocation fails every all-lane bypass condition that excludes it,
|
||||
// such as an all-inactive or an all-nonzero bypass.
|
||||
const auto* negated = SimplifyGuard(witness).TryInstruction();
|
||||
const bool nonzero = negated != nullptr && negated->GetOpcode() == ValueOpcode::LogicalNot;
|
||||
std::vector<const Block*> pending {use};
|
||||
for (size_t index = 0; index < pending.size(); ++index) {
|
||||
const auto* current = pending[index];
|
||||
@@ -1837,9 +1921,9 @@ private:
|
||||
for (const auto* previous: current->ImmPredecessors()) {
|
||||
if (!witness.IsEmpty()) {
|
||||
const auto edge = ConditionalEdge(previous, current);
|
||||
// The proven local-zero invocation makes an all-inactive bypass impossible.
|
||||
if (edge && edge->lanes == LaneQuantifier::All && !edge->positive &&
|
||||
Implies(witness, edge->condition)) continue;
|
||||
if (edge && edge->lanes == LaneQuantifier::All &&
|
||||
(edge->positive ? nonzero && Implies(edge->condition, negated->Arg(0))
|
||||
: Implies(witness, edge->condition))) continue;
|
||||
}
|
||||
if (std::ranges::find(pending, previous) == pending.end()) pending.push_back(previous);
|
||||
}
|
||||
@@ -1948,7 +2032,10 @@ private:
|
||||
|
||||
bool LocalZeroWitness(Value guard) const {
|
||||
const auto* equal = SimplifyGuard(guard).TryInstruction();
|
||||
if (equal == nullptr || equal->GetOpcode() != ValueOpcode::IEqual32) return false;
|
||||
const bool negated = equal != nullptr && equal->GetOpcode() == ValueOpcode::LogicalNot;
|
||||
if (negated) equal = SimplifyGuard(equal->Arg(0)).TryInstruction();
|
||||
const auto expected = negated ? ValueOpcode::INotEqual32 : ValueOpcode::IEqual32;
|
||||
if (equal == nullptr || equal->GetOpcode() != expected) return false;
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg) {
|
||||
if (equal->Arg(arg).Resolve() != Value(0u)) continue;
|
||||
Value induction;
|
||||
@@ -1982,6 +2069,10 @@ private:
|
||||
EquivalentValue(m_program, test->Arg(0), value)) return test->Arg(1);
|
||||
if ((positive ? op == ValueOpcode::UGreaterThan32 : op == ValueOpcode::ULessThanEqual32) &&
|
||||
EquivalentValue(m_program, test->Arg(1), value)) return test->Arg(0);
|
||||
uint32_t limit = 0;
|
||||
if ((positive ? op == ValueOpcode::ULessThanEqual32 : op == ValueOpcode::UGreaterThan32) &&
|
||||
ImmediateU32(test->Arg(1), limit) && limit != UINT32_MAX &&
|
||||
EquivalentValue(m_program, test->Arg(0), value)) return Value(limit + 1u);
|
||||
const auto* scan = value.Resolve().TryInstruction();
|
||||
if (finite_bitscan && scan != nullptr && scan->GetOpcode() == ValueOpcode::FindILsb32 &&
|
||||
(positive ? op == ValueOpcode::INotEqual32 : op == ValueOpcode::IEqual32)) {
|
||||
@@ -2056,7 +2147,7 @@ private:
|
||||
const auto* read = ActiveValue(value, active).TryInstruction();
|
||||
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadSharedU32 ||
|
||||
m_program.stage != ShaderType::Compute ||
|
||||
read->Arg(1).Resolve() != Value(true)) return {};
|
||||
!Implies(active, read->Arg(1))) return {};
|
||||
uint64_t group_size = 1u;
|
||||
for (const auto dimension: m_input.compute->threads_num) {
|
||||
if (dimension == 0u || dimension > m_input.compute->wave_size) return {};
|
||||
@@ -2087,10 +2178,16 @@ private:
|
||||
const auto* limit = BoundedLoop(read_offset.index, read->Parent(),
|
||||
[](const Inst& compare, uint32_t) { return compare.GetOpcode() == ValueOpcode::ULessThan32; });
|
||||
if (limit == nullptr) return {};
|
||||
auto counter_active = active;
|
||||
const auto* counter_read = ActiveValue(limit->Arg(1), active).TryInstruction();
|
||||
// Every lane reads the same counter word, so a selected lane carries it.
|
||||
if (counter_read != nullptr && counter_read->GetOpcode() == ValueOpcode::ReadFirstLane) {
|
||||
counter_active = counter_read->Arg(1);
|
||||
counter_read = ActiveValue(counter_read->Arg(0), counter_active).TryInstruction();
|
||||
}
|
||||
uint32_t consumed_counter = 0;
|
||||
if (counter_read == nullptr || counter_read->GetOpcode() != ValueOpcode::LoadSharedU32 ||
|
||||
counter_read->Arg(1).Resolve() != Value(true) ||
|
||||
!Implies(counter_active, counter_read->Arg(1)) ||
|
||||
!constant_address(*counter_read, consumed_counter)) return {};
|
||||
|
||||
struct Initializer { const Inst* inst; uint32_t address; uint32_t words; };
|
||||
@@ -2304,25 +2401,23 @@ private:
|
||||
((offset | stride | memory->offset) & 3u) == 0u &&
|
||||
uint64_t {offset} + memory->offset <= UINT32_MAX;
|
||||
if (affine) {
|
||||
workgroup_axis = WorkgroupAxis(index);
|
||||
if (workgroup_axis == UINT32_MAX) {
|
||||
const auto* sum = index.Resolve().TryInstruction();
|
||||
if (sum != nullptr && sum->GetOpcode() == ValueOpcode::IAdd32) {
|
||||
for (uint32_t arm = 0; arm < 2u; ++arm) {
|
||||
const auto axis = WorkgroupAxis(sum->Arg(arm));
|
||||
const auto base = sum->Arg(arm ^ 1u);
|
||||
if (axis == UINT32_MAX ||
|
||||
!ValidateRuntimeValue(m_program, base, RuntimeValueType::Integer)) continue;
|
||||
workgroup_axis = axis;
|
||||
first = base;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
workgroup_axis = WorkgroupAxis(index, &first);
|
||||
const auto* bound = BoundedLoop(index, read->Parent());
|
||||
if (bound != nullptr && bound->GetOpcode() == ValueOpcode::ULessThan32) {
|
||||
count = bound->Arg(1);
|
||||
} else if (workgroup_axis == UINT32_MAX && !require_bound) {
|
||||
// Other bounded indices, such as bit scans, read the records up to their maximum;
|
||||
// a bit scan of a nonzero mask load reads only the ranks its words set.
|
||||
const auto maximum = SelectorMaximum(index);
|
||||
if (maximum == 31u) selector.ranks = BitScanRanks(index);
|
||||
if (maximum < MaxIndirectDescriptorProbes) count = Value(maximum + 1u);
|
||||
}
|
||||
// A record selected from a shared append list is below the list's bound.
|
||||
const auto* lane = index.Resolve().TryInstruction();
|
||||
if (count.IsEmpty() && lane != nullptr &&
|
||||
lane->GetOpcode() == ValueOpcode::ReadFirstLane &&
|
||||
HasActiveLane(lane->Arg(1), lane->Parent()))
|
||||
count = SharedLoadBound(lane->Arg(0), lane->Arg(1));
|
||||
}
|
||||
// Raw pointers have no V# bounds for enumerating an unbounded selector domain.
|
||||
if (memory->kind == ResourceKind::ScalarAddress && count.IsEmpty() &&
|
||||
@@ -2498,7 +2593,8 @@ private:
|
||||
// Each equation is an inductive subset obligation. Phis require every incoming
|
||||
// value; AND needs either operand. Grounding rejects cycles without an initial
|
||||
// value, then the greatest fixed point checks every initial and carried edge.
|
||||
bool BitsetMaskSubset(Value mask, Value active, Value word, const Inst& read) const {
|
||||
bool BitsetMaskSubset(Value mask, Value active, Value word,
|
||||
std::span<const Inst* const> reads) const {
|
||||
struct State {
|
||||
Value value;
|
||||
Value guard;
|
||||
@@ -2510,7 +2606,10 @@ private:
|
||||
bool all = true;
|
||||
std::vector<size_t> terms;
|
||||
};
|
||||
const auto load_guard = read.Arg(4).Resolve();
|
||||
// A scalar load reads one word without EXEC; four-word loads take their guard.
|
||||
const bool scalar = reads[0]->GetOpcode() == ValueOpcode::ReadConstBuffer;
|
||||
const auto load_guard = scalar ? Value(true) : reads[0]->Arg(4).Resolve();
|
||||
const auto words = static_cast<uint32_t>(reads.size()) * (scalar ? 1u : 4u);
|
||||
std::vector<State> states(2);
|
||||
std::vector<Equation> equations(2);
|
||||
const auto add = [&](State state) {
|
||||
@@ -2521,9 +2620,9 @@ private:
|
||||
equations.emplace_back();
|
||||
return states.size() - 1u;
|
||||
};
|
||||
std::array<size_t, 5> roots {add({.value = active})};
|
||||
for (uint32_t component = 0; component < 4u; ++component)
|
||||
roots[component + 1u] = add({mask, active, component, component});
|
||||
std::vector<size_t> roots {add({.value = active})};
|
||||
for (uint32_t component = 0; component < words; ++component)
|
||||
roots.push_back(add({mask, active, component, component}));
|
||||
for (size_t index = 2; index < states.size(); ++index) {
|
||||
const auto state = states[index];
|
||||
const auto* inst = state.value.TryInstruction();
|
||||
@@ -2573,11 +2672,14 @@ private:
|
||||
switch (inst->GetOpcode()) {
|
||||
case ValueOpcode::CompositeExtractU32x4: {
|
||||
uint32_t component;
|
||||
if (inst->Arg(0).Resolve().TryInstruction() == &read &&
|
||||
ImmediateU32(inst->Arg(1), component) && component == state.component)
|
||||
if (inst->Arg(0).Resolve().TryInstruction() == reads[state.component / 4u] &&
|
||||
ImmediateU32(inst->Arg(1), component) && component == state.component % 4u)
|
||||
equation.terms = {1u};
|
||||
break;
|
||||
}
|
||||
case ValueOpcode::ReadConstBuffer:
|
||||
if (scalar && inst == reads[state.component]) equation.terms = {1u};
|
||||
break;
|
||||
case ValueOpcode::Phi:
|
||||
// The predecessor may be an earlier outer iteration. Establish
|
||||
// this register's invariant without the current word or EXEC.
|
||||
@@ -2598,17 +2700,17 @@ private:
|
||||
const auto* masked = key.TryInstruction();
|
||||
uint32_t bound = 0;
|
||||
if (masked != nullptr && masked->GetOpcode() == ValueOpcode::BitwiseAnd32 &&
|
||||
DescendingLoop(word, masked->Parent(), bound) && bound == 3u) {
|
||||
DescendingLoop(word, masked->Parent(), bound) && bound == words - 1u) {
|
||||
for (uint32_t side = 0; side < 2u; ++side) {
|
||||
uint32_t bits;
|
||||
if (ImmediateU32(masked->Arg(side), bits) && (bits & 3u) == 3u)
|
||||
if (ImmediateU32(masked->Arg(side), bits) && (bits & bound) == bound)
|
||||
key = masked->Arg(side ^ 1u).Resolve();
|
||||
}
|
||||
}
|
||||
return EquivalentValue(m_program, key, word);
|
||||
});
|
||||
uint32_t literal;
|
||||
if (ImmediateU32(equality, literal) && literal < 4u) selected_word = literal;
|
||||
if (ImmediateU32(equality, literal) && literal < words) selected_word = literal;
|
||||
equation.terms.push_back(term(inst->Arg(1), inst->Arg(0), selected_word));
|
||||
}
|
||||
if (!selected || !*selected)
|
||||
@@ -2646,7 +2748,53 @@ private:
|
||||
return std::ranges::all_of(roots, [&](size_t root) { return valid[root]; });
|
||||
}
|
||||
|
||||
bool MatchBitsetSelector(Value rank, Value active, SelectorBounds bounds,
|
||||
// The mask words whose set bits are every record a bit scan of index selects: the scanned
|
||||
// value keeps only bits of one scalar load, which reads a word its address can reach.
|
||||
std::optional<DescriptorSource::IndirectDescriptor::SelectorRead::MaskRanks>
|
||||
BitScanRanks(Value index) {
|
||||
const auto* scan = index.Resolve().TryInstruction();
|
||||
if (scan == nullptr || scan->GetOpcode() != ValueOpcode::FindILsb32) return {};
|
||||
const auto* masked = scan->Arg(0).Resolve().TryInstruction();
|
||||
if (masked == nullptr) return {};
|
||||
std::vector<const Inst*> values {masked};
|
||||
const Inst* load = nullptr;
|
||||
for (size_t i = 0; i < values.size() && load == nullptr; ++i) {
|
||||
const auto* value = values[i];
|
||||
if (value->GetOpcode() == ValueOpcode::ReadConstBuffer) load = value;
|
||||
if (value->GetOpcode() != ValueOpcode::Phi &&
|
||||
value->GetOpcode() != ValueOpcode::BitwiseAnd32) continue;
|
||||
for (size_t arg = 0; arg < value->NumArgs(); ++arg) {
|
||||
const auto* next = value->Arg(arg).Resolve().TryInstruction();
|
||||
if (next != nullptr && std::ranges::find(values, next) == values.end())
|
||||
values.push_back(next);
|
||||
}
|
||||
}
|
||||
uint32_t memory_index = 0;
|
||||
const auto* memory = load != nullptr ? ScalarReadMemory(*load, memory_index) : nullptr;
|
||||
if (memory == nullptr || memory->offset > INT32_MAX ||
|
||||
!MemoryIndexBelongsTo(memory_index, *load) ||
|
||||
!BitsetMaskSubset(scan->Arg(0), Value(true), Value(0u), std::array {load})) return {};
|
||||
auto address = load->Arg(1);
|
||||
auto active = Value(true);
|
||||
const auto* lane = address.Resolve().TryInstruction();
|
||||
if (lane != nullptr && lane->GetOpcode() == ValueOpcode::ReadFirstLane) {
|
||||
if (!HasActiveLane(lane->Arg(1), lane->Parent())) return {};
|
||||
active = lane->Arg(1);
|
||||
address = lane->Arg(0);
|
||||
}
|
||||
// A left shift keeps the address a multiple of its scale; other addresses read any word.
|
||||
const auto* shift = ActiveValue(address, active).TryInstruction();
|
||||
uint32_t bits = 0;
|
||||
if (shift == nullptr || shift->GetOpcode() != ValueOpcode::ShiftLeftLogical32 ||
|
||||
!ImmediateU32(shift->Arg(1), bits) || bits >= 32u) bits = 0;
|
||||
DescriptorSource source;
|
||||
if (!MakeRuntimeTableSource(*load, source)) return {};
|
||||
return DescriptorSource::IndirectDescriptor::SelectorRead::MaskRanks {
|
||||
.source = InternSource(source), .stride = std::max(1u << bits, 4u),
|
||||
.offset = memory->offset & ~3u};
|
||||
}
|
||||
|
||||
bool MatchBitsetSelector(Value rank, Value active, SelectorBounds bounds, uint32_t words,
|
||||
DescriptorSource::IndirectDescriptor& indirect) {
|
||||
const auto selected_value = [&](Value value) {
|
||||
for (;;) {
|
||||
@@ -2670,7 +2818,7 @@ private:
|
||||
}
|
||||
const auto* word_inst = word.TryInstruction();
|
||||
if (word_inst == nullptr || std::ranges::none_of(bounds, [&](const auto& bound) {
|
||||
return bound.first == word_inst && bound.second == 3u;
|
||||
return bound.first == word_inst && bound.second == words - 1u;
|
||||
})) return false;
|
||||
// FFBH's leading-zero count is inverted again to recover the set-bit rank.
|
||||
for (uint32_t inverse = 0; inverse < 2u; ++inverse) {
|
||||
@@ -2682,21 +2830,34 @@ private:
|
||||
const auto* scan = bit.TryInstruction();
|
||||
if (scan == nullptr || scan->GetOpcode() != ValueOpcode::FindUMsb32 ||
|
||||
!ImpliesNonzero(active, scan->Arg(0))) return false;
|
||||
const auto bitmap_load = [&](const Inst& read, uint32_t offset) {
|
||||
const auto index = read.Flags<MemoryFlags>().index;
|
||||
if (read.GetOpcode() != ValueOpcode::LoadBufferU32x4 || InCycle(read.Parent()) ||
|
||||
index >= m_program.memory_info.size()) return false;
|
||||
const auto& memory = m_program.memory_info[index];
|
||||
return memory.kind == ResourceKind::Buffer && !memory.typed && !memory.formatted &&
|
||||
memory.idxen && !memory.offen && memory.offset == offset &&
|
||||
memory.data_bits == 32u && memory.data_dwords == 4u &&
|
||||
read.Arg(2).Resolve() == Value(0u) && read.Arg(3).Resolve() == Value(0u);
|
||||
};
|
||||
for (const auto* block: m_program.blocks) {
|
||||
for (const auto& read: *block) {
|
||||
if (read.GetOpcode() != ValueOpcode::LoadBufferU32x4 || InCycle(block)) continue;
|
||||
const auto index = read.Flags<MemoryFlags>().index;
|
||||
if (index >= m_program.memory_info.size()) continue;
|
||||
const auto& memory = m_program.memory_info[index];
|
||||
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
|
||||
!memory.idxen || memory.offen || memory.offset != 0u || memory.data_bits != 32u ||
|
||||
memory.data_dwords != 4u || read.Arg(2).Resolve() != Value(0u) ||
|
||||
read.Arg(3).Resolve() != Value(0u) ||
|
||||
!BitsetMaskSubset(scan->Arg(0), active, word, read)) continue;
|
||||
if (!bitmap_load(read, 0u)) continue;
|
||||
// An eight-word record continues in a second four-word load of the same buffer and EXEC.
|
||||
std::array<const Inst*, 2> reads {&read};
|
||||
if (words == 8u)
|
||||
for (const auto* other: m_program.blocks)
|
||||
for (const auto& load: *other)
|
||||
if (reads[1] == nullptr && bitmap_load(load, 16u) &&
|
||||
EquivalentValue(m_program, load.Arg(0), read.Arg(0)) &&
|
||||
EquivalentValue(m_program, load.Arg(4), read.Arg(4))) reads[1] = &load;
|
||||
const auto parts = std::span(reads).first(words / 4u);
|
||||
DescriptorSource source;
|
||||
if (!MakeRuntimeTableSource(read, source)) continue;
|
||||
if (std::ranges::find(parts, nullptr) != parts.end() ||
|
||||
!BitsetMaskSubset(scan->Arg(0), active, word, parts) ||
|
||||
!MakeRuntimeTableSource(read, source)) continue;
|
||||
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead {
|
||||
.source = InternSource(source), .stride = 16u, .offset = 0u, .bitset = true});
|
||||
.source = InternSource(source), .stride = words * 4u, .offset = 0u, .bitset = true});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2726,7 +2887,8 @@ private:
|
||||
first.SetArg(1, Value(maximum));
|
||||
indirect.selector_first = Value(&first);
|
||||
indirect.key_count = Value(maximum + 1u);
|
||||
if (maximum == 127u) MatchBitsetSelector(lane->Arg(0), lane->Arg(1), bounds, indirect);
|
||||
if (maximum == 127u || maximum == 255u)
|
||||
MatchBitsetSelector(lane->Arg(0), lane->Arg(1), bounds, (maximum + 1u) / 32u, indirect);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2993,6 +3155,47 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
// The EXEC predicate of the image access through use; empty for any other use.
|
||||
Value AccessPredicate(const Use& use, bool sampler) const {
|
||||
const auto& access = *use.user;
|
||||
if (use.operand != (sampler ? 1u : 0u) ||
|
||||
ImageOpcodeInfoOf(access.GetOpcode()).access == ImageAccess::None) return {};
|
||||
if (sampler && (access.GetOpcode() != ValueOpcode::ImageSampleRaw ||
|
||||
(m_program.memory_info[access.Flags<MemoryFlags>().index].image_sample_flags &
|
||||
Decoder::ImageSampleFlagAdjust) != 0u)) return {};
|
||||
const auto mask = access.Arg(access.NumArgs() - 1u);
|
||||
return mask.GetType() == Type::U1 ? mask : Value {};
|
||||
}
|
||||
|
||||
// A key no bound covers takes its maximum from a signed clamp to a constant, on the key or on
|
||||
// the key plus a constant in an earlier-tracked sibling read of the same table. Negative keys
|
||||
// and keys past that maximum read the first row.
|
||||
uint32_t ClampedTableMaximum(Value key, const DescriptorSource& table,
|
||||
const DescriptorSource::IndirectDescriptor& indirect) const {
|
||||
uint32_t row = 0, offset = 0;
|
||||
const auto clamped = [&](Value value) {
|
||||
const auto* min = value.Resolve().TryInstruction();
|
||||
if (min != nullptr && min->GetOpcode() == ValueOpcode::SMin32)
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg)
|
||||
if (ImmediateU32(min->Arg(arg), row)) return min->Arg(arg ^ 1u);
|
||||
return Value {};
|
||||
};
|
||||
if (!clamped(key).IsEmpty()) return row;
|
||||
for (const auto& sibling: m_indirect_descriptors) {
|
||||
const auto& read = *m_sources[sibling.source].indirect_descriptor;
|
||||
const auto* add = clamped(sibling.key).Resolve().TryInstruction();
|
||||
if (add == nullptr || add->GetOpcode() != ValueOpcode::IAdd32 ||
|
||||
m_sources[read.table_source] != table ||
|
||||
read.table_offset != indirect.table_offset ||
|
||||
read.table_stride != indirect.table_stride ||
|
||||
read.table_immediate != indirect.table_immediate) continue;
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg)
|
||||
if (ImmediateU32(add->Arg(arg), offset) &&
|
||||
EquivalentValue(m_program, add->Arg(arg ^ 1u), key)) return row;
|
||||
}
|
||||
return UINT32_MAX;
|
||||
}
|
||||
|
||||
bool TryMakeTableDescriptor(Inst& handle, const DescriptorSource& descriptor,
|
||||
IndirectDescriptorPlan& plan) {
|
||||
const bool sampler = handle.GetOpcode() == ValueOpcode::GetSamplerResource;
|
||||
@@ -3019,6 +3222,7 @@ private:
|
||||
} else if (!plan.table_indexed &&
|
||||
MatchScalarMaterialKey(key, indirect, material_source,
|
||||
!sampler && table_source.dword_count == 4u, sampler)) {
|
||||
indirect.selector->excluded = ExcludedKeyOnEntry(key, handle.Parent());
|
||||
// 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;
|
||||
@@ -3080,36 +3284,42 @@ private:
|
||||
if (indirect.key_count.IsEmpty() &&
|
||||
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, false) &&
|
||||
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, true) &&
|
||||
!MatchReversedImageRange(key, indirect)) {
|
||||
const auto maximum = SelectorMaximum(key);
|
||||
if (maximum >= MaxIndirectDescriptorProbes) return false;
|
||||
indirect.key_count = Value(maximum + 1u);
|
||||
!MatchReversedImageRange(key, indirect) && !MatchWorkgroupKey(key, indirect)) {
|
||||
auto maximum = SelectorMaximum(key);
|
||||
if (maximum >= MaxIndirectDescriptorProbes) {
|
||||
// Each image access bounds the key on the lanes it executes.
|
||||
maximum = 0u;
|
||||
for (const auto& use: handle.Uses()) {
|
||||
const auto active = AccessPredicate(use, false);
|
||||
if (active.IsEmpty()) {
|
||||
maximum = UINT32_MAX;
|
||||
break;
|
||||
}
|
||||
maximum = std::max(maximum, SelectorMaximum(key, {}, 0, active));
|
||||
}
|
||||
}
|
||||
if (maximum >= MaxIndirectDescriptorProbes)
|
||||
maximum = ClampedTableMaximum(key, table_source, indirect);
|
||||
if (maximum < MaxIndirectDescriptorProbes)
|
||||
indirect.key_count = Value(maximum + 1u);
|
||||
else if (!ProgramLoopCount(key, indirect))
|
||||
return false;
|
||||
}
|
||||
if ((table_offset & 3u) != 0u) return false;
|
||||
} else {
|
||||
// Keep the GPU selector and descriptor reads, and preserve the proven scalar
|
||||
// offset domain within the bounded image table.
|
||||
if (sampler || plan.split_offsets) return false;
|
||||
const auto active = [&](const Use& use, bool sampler) -> Value {
|
||||
const auto& access = *use.user;
|
||||
if (use.operand != (sampler ? 1u : 0u) ||
|
||||
ImageOpcodeInfoOf(access.GetOpcode()).access == ImageAccess::None) return {};
|
||||
if (sampler && (access.GetOpcode() != ValueOpcode::ImageSampleRaw ||
|
||||
(m_program.memory_info[access.Flags<MemoryFlags>().index].image_sample_flags &
|
||||
Decoder::ImageSampleFlagAdjust) != 0u)) return {};
|
||||
const auto mask = access.Arg(access.NumArgs() - 1u);
|
||||
return mask.GetType() == Type::U1 ? mask : Value {};
|
||||
};
|
||||
bool shared_selector = false;
|
||||
if (!plan.table_indexed) {
|
||||
for (const auto& owner: m_indirect_descriptors) {
|
||||
const auto& selected = *m_sources[owner.source].indirect_descriptor;
|
||||
if (!selected.selector || !EquivalentValue(m_program, key, owner.key) ||
|
||||
!std::ranges::all_of(handle.Uses(), [&](const Use& target) {
|
||||
const auto target_active = active(target, false);
|
||||
const auto target_active = AccessPredicate(target, false);
|
||||
return !target_active.IsEmpty() &&
|
||||
std::ranges::any_of(owner.handle->Uses(), [&](const Use& witness) {
|
||||
const auto owner_active = active(witness,
|
||||
const auto owner_active = AccessPredicate(witness,
|
||||
owner.handle->GetOpcode() == ValueOpcode::GetSamplerResource);
|
||||
return !owner_active.IsEmpty() && AvailableBefore(*witness.user, *target.user) &&
|
||||
Implies(target_active, owner_active);
|
||||
@@ -3430,6 +3640,42 @@ private:
|
||||
return result;
|
||||
}
|
||||
|
||||
// Bounds of an F32 value on the lanes where guard holds; NaN when unknown.
|
||||
std::pair<float, float> FloatRange(Value value, Value guard, uint32_t depth = 0) const {
|
||||
constexpr std::pair unknown {NAN, NAN};
|
||||
value = ActiveValue(value, guard);
|
||||
if (value.IsImmediate() && value.GetType() == Type::F32)
|
||||
return {value.F32Value(), value.F32Value()};
|
||||
const auto* inst = value.TryInstruction();
|
||||
// Each multiply-add evaluates three operands; keep the search shallow.
|
||||
if (depth > 12u || inst == nullptr) return unknown;
|
||||
const auto range = [&](uint32_t arg) {
|
||||
return FloatRange(inst->Arg(arg), guard, depth + 1u);
|
||||
};
|
||||
switch (inst->GetOpcode()) {
|
||||
case ValueOpcode::BitCastF32U32:
|
||||
case ValueOpcode::BitCastU32F32: return range(0);
|
||||
// Clamping maps NaN to zero.
|
||||
case ValueOpcode::FPSaturate32: return {0.0f, 1.0f};
|
||||
case ValueOpcode::FPFloor32: {
|
||||
const auto [low, high] = range(0);
|
||||
return {std::floor(low), std::floor(high)};
|
||||
}
|
||||
case ValueOpcode::FPMad32: {
|
||||
// The rounded multiply and add are monotonic, so the bounds lie at the corners.
|
||||
const auto a = range(0), b = range(1), c = range(2);
|
||||
if (!std::isfinite(a.first) || !std::isfinite(a.second) ||
|
||||
!std::isfinite(b.first) || !std::isfinite(b.second)) return unknown;
|
||||
const auto [low, high] = std::minmax({a.first * b.first, a.first * b.second,
|
||||
a.second * b.first, a.second * b.second});
|
||||
const std::pair bounds {low + c.first, high + c.second};
|
||||
if (!std::isfinite(bounds.first) || !std::isfinite(bounds.second)) return unknown;
|
||||
return bounds;
|
||||
}
|
||||
default: return unknown;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t SelectorMaximum(Value value, SelectorBounds bounds = {}, uint32_t depth = 0,
|
||||
Value guard = {}) const {
|
||||
if (depth > 32u) return UINT32_MAX;
|
||||
@@ -3445,9 +3691,13 @@ private:
|
||||
switch (inst->GetOpcode()) {
|
||||
case ValueOpcode::ReadFirstLane:
|
||||
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));
|
||||
case ValueOpcode::ReadLane: {
|
||||
// ReadLane ignores EXEC; only context-free bounds on every source lane apply,
|
||||
// unless guard proves a waterfall key equal to the executing lanes' local value.
|
||||
const auto local = EqualLocalKey(guard, value);
|
||||
return local.IsEmpty() ? PossibleU32Bits(inst->Arg(0))
|
||||
: SelectorMaximum(local, bounds, depth + 1u, guard);
|
||||
}
|
||||
case ValueOpcode::Phi: {
|
||||
const auto initial = InitialCandidateMask(value);
|
||||
if (!initial.IsEmpty()) return SelectorMaximum(initial, bounds, depth + 1u, guard);
|
||||
@@ -3474,6 +3724,18 @@ private:
|
||||
NonzeroOnEntry(inst->Arg(0), inst->Parent()) ? 31u : UINT32_MAX;
|
||||
case ValueOpcode::UMin32:
|
||||
case ValueOpcode::BitwiseAnd32: return std::min(maximum(0), maximum(1));
|
||||
case ValueOpcode::SMin32: {
|
||||
// Signed and unsigned minimums agree on nonnegative operands.
|
||||
const auto left = maximum(0), right = maximum(1);
|
||||
if (std::max(left, right) <= INT32_MAX) return std::min(left, right);
|
||||
break;
|
||||
}
|
||||
case ValueOpcode::ConvertS32F32: {
|
||||
// The conversion truncates toward zero.
|
||||
const auto [low, high] = FloatRange(inst->Arg(0), guard);
|
||||
if (low >= 0.0f && high < 0x1p31f) return static_cast<uint32_t>(high);
|
||||
break;
|
||||
}
|
||||
case ValueOpcode::SelectU32:
|
||||
if (!guard.IsEmpty() && Implies(guard, inst->Arg(0))) return maximum(1);
|
||||
return SelectorPredicateFalse(inst->Arg(0), bounds, depth + 1u, guard)
|
||||
|
||||
@@ -175,6 +175,8 @@ void AddBuiltinInput(ShaderInfo& info, StageInputKind kind) {
|
||||
case StageInputKind::GlobalInvocationId:
|
||||
AddInput(info, kind, 0, 3, "gl_GlobalInvocationID");
|
||||
break;
|
||||
case StageInputKind::BaseVertex: AddInput(info, kind, 0, 1, "gl_BaseVertex"); break;
|
||||
case StageInputKind::BaseInstance: AddInput(info, kind, 0, 1, "gl_BaseInstance"); break;
|
||||
case StageInputKind::PackedAncillary:
|
||||
case StageInputKind::Parameter: break;
|
||||
}
|
||||
@@ -366,7 +368,6 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
|
||||
info.function_lds |= kind == ResourceKind::Lds &&
|
||||
program.stage != ShaderType::Compute && program.stage != ShaderType::Mesh;
|
||||
if (shared_access == SharedAccess::Append || shared_access == SharedAccess::Consume) {
|
||||
info.subgroup_ballot = true;
|
||||
info.subgroup_shuffle = true;
|
||||
info.subgroup_local_invocation_id = true;
|
||||
}
|
||||
@@ -445,6 +446,8 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
|
||||
case StageInputKind::InstanceIndex:
|
||||
case StageInputKind::FrontFacing:
|
||||
case StageInputKind::LocalInvocationIndex:
|
||||
case StageInputKind::BaseVertex:
|
||||
case StageInputKind::BaseInstance:
|
||||
if (component != 0u) {
|
||||
return Fail("typed scalar builtin component is out of range");
|
||||
}
|
||||
@@ -517,7 +520,9 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
|
||||
inst.Flags<CFG::BranchCondition>() != CFG::BranchCondition::ScalarInstruction;
|
||||
break;
|
||||
case ValueOpcode::Ballot: info.subgroup_ballot = true; break;
|
||||
case ValueOpcode::SubgroupUMinU32: info.subgroup_arithmetic = true; break;
|
||||
case ValueOpcode::LiveMask: info.subgroup_ballot = info.helper_invocation = true; break;
|
||||
case ValueOpcode::SubgroupUMinU32:
|
||||
case ValueOpcode::SubgroupUMaxU32: info.subgroup_arithmetic = true; break;
|
||||
case ValueOpcode::DppMoveU32:
|
||||
case ValueOpcode::ReadFirstLane:
|
||||
case ValueOpcode::ReadLane: {
|
||||
@@ -576,7 +581,7 @@ void CollectShaderInfo(Program& program, ShaderStageInputInfo input_info) {
|
||||
info.has_bitwise_xor = info.uses_lds = info.uses_gds = info.uses_flattened_srt =
|
||||
info.uses_dispatch_threads = info.uses_swizzle = false;
|
||||
info.bvh = info.subgroup_ballot = info.subgroup_vote = info.subgroup_barrier =
|
||||
info.subgroup_shuffle = info.subgroup_arithmetic =
|
||||
info.subgroup_shuffle = info.subgroup_arithmetic = info.helper_invocation =
|
||||
info.subgroup_local_invocation_id = info.compute_derivatives = info.image_gather_extended =
|
||||
info.function_lds = info.function_scratch = info.pixel_valid_mask = info.buffer_int64_atomics =
|
||||
info.buffer_u8 = info.buffer_u16 = info.shared_int64_atomics = info.coherent_buffers = info.float64 = false;
|
||||
|
||||
@@ -82,6 +82,7 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
|
||||
case ValueOpcode::BitCastU32F32:
|
||||
case ValueOpcode::BitCastF32U32:
|
||||
case ValueOpcode::ConvertU32F32:
|
||||
case ValueOpcode::ConvertS32F32:
|
||||
case ValueOpcode::ConvertF32U32:
|
||||
case ValueOpcode::CompositeConstructU64:
|
||||
case ValueOpcode::CompositeExtractU64:
|
||||
@@ -147,8 +148,8 @@ bool ImmediateBits(Value value, uint64_t& bits) {
|
||||
}
|
||||
|
||||
// Scratch layout in bytes: per descriptor source its eight DWORDs and a known DWORD, one
|
||||
// activity byte per source, then one qword per walked value (all ones when unknown). The
|
||||
// walker's value slots, captured addresses, reachability and states follow.
|
||||
// activity byte per source, then per image, then one qword per walked value (all ones when
|
||||
// unknown). The walker's value slots, captured addresses, reachability and states follow.
|
||||
constexpr uint32_t DescriptorStride = 9u * sizeof(uint32_t);
|
||||
|
||||
uint32_t ActiveOffset(const ResourcePlan& plan) {
|
||||
@@ -156,7 +157,8 @@ uint32_t ActiveOffset(const ResourcePlan& plan) {
|
||||
}
|
||||
|
||||
uint32_t OutputOffset(const ResourcePlan& plan) {
|
||||
return (ActiveOffset(plan) + static_cast<uint32_t>(plan.descriptor_sources.size()) + 7u) & ~7u;
|
||||
const auto active = plan.descriptor_sources.size() + plan.info.images.size();
|
||||
return (ActiveOffset(plan) + static_cast<uint32_t>(active) + 7u) & ~7u;
|
||||
}
|
||||
|
||||
const uint8_t* Scratch(const ResourcePlan& plan) {
|
||||
@@ -1038,6 +1040,23 @@ void WalkerCompiler::EmitStrict(const Node& node) {
|
||||
c.L(done);
|
||||
break;
|
||||
}
|
||||
case ValueOpcode::ConvertS32F32: {
|
||||
// The conversion saturates and maps NaN to zero. cvttss2si's out-of-range result,
|
||||
// INT32_MIN, is already right for negative overflow.
|
||||
Xbyak::Label done;
|
||||
c.movd(c.xmm0, c.eax);
|
||||
c.xor_(c.eax, c.eax);
|
||||
c.ucomiss(c.xmm0, c.xmm0);
|
||||
c.jp(done, Near);
|
||||
c.mov(c.ecx, 0x4f000000u);
|
||||
c.movd(c.xmm1, c.ecx);
|
||||
c.mov(c.eax, INT32_MAX);
|
||||
c.ucomiss(c.xmm0, c.xmm1);
|
||||
c.jae(done, Near);
|
||||
c.cvttss2si(c.eax, c.xmm0);
|
||||
c.L(done);
|
||||
break;
|
||||
}
|
||||
case ValueOpcode::FPMul32:
|
||||
c.movd(c.xmm0, c.eax);
|
||||
c.movd(c.xmm1, c.ecx);
|
||||
@@ -1272,11 +1291,14 @@ void WalkerCompiler::Compile(std::span<const Value> values) {
|
||||
for (const auto slot: blocks[block].srt_reads) flat(slot);
|
||||
c.L(skip);
|
||||
}
|
||||
// A source no block lists is always active.
|
||||
for (uint32_t source = 0; source < sources.size(); ++source) {
|
||||
// A source or image no block lists is always active.
|
||||
for (uint32_t index = 0; index < sources.size() + m_plan.info.images.size(); ++index) {
|
||||
const bool image = index >= sources.size();
|
||||
const uint32_t id = image ? index - static_cast<uint32_t>(sources.size()) : index;
|
||||
bool listed = false;
|
||||
for (uint32_t block = 0; block < blocks.size(); ++block) {
|
||||
if (std::ranges::find(blocks[block].sources, source) == blocks[block].sources.end()) continue;
|
||||
const auto& list = image ? blocks[block].images : blocks[block].sources;
|
||||
if (std::ranges::find(list, id) == list.end()) continue;
|
||||
if (listed) {
|
||||
c.or_(c.al, c.byte[c.rdx + m_reach + block]);
|
||||
} else {
|
||||
@@ -1284,7 +1306,7 @@ void WalkerCompiler::Compile(std::span<const Value> values) {
|
||||
}
|
||||
listed = true;
|
||||
}
|
||||
const auto active = c.byte[c.rdx + ActiveOffset(m_plan) + source];
|
||||
const auto active = c.byte[c.rdx + ActiveOffset(m_plan) + index];
|
||||
if (listed) {
|
||||
c.mov(active, c.al);
|
||||
} else {
|
||||
@@ -1391,6 +1413,10 @@ bool WalkedActive(const ResourcePlan& plan, uint32_t source) {
|
||||
return Scratch(plan)[ActiveOffset(plan) + source] != 0u;
|
||||
}
|
||||
|
||||
bool WalkedImageActive(const ResourcePlan& plan, uint32_t image) {
|
||||
return Scratch(plan)[ActiveOffset(plan) + plan.descriptor_sources.size() + image] != 0u;
|
||||
}
|
||||
|
||||
bool ValidateRuntimeValue(const ResourcePlan& program, Value value, RuntimeValueType type) {
|
||||
return RuntimeValidator(program, type).Run(value);
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ bool WalkSrt(const ResourcePlan& plan, const SrtRuntime& runtime, std::vector<ui
|
||||
bool WalkedValue(const ResourcePlan& plan, Value value, uint32_t& result);
|
||||
bool WalkedDescriptor(const ResourcePlan& plan, uint32_t source, DescriptorValue& result);
|
||||
bool WalkedActive(const ResourcePlan& plan, uint32_t source);
|
||||
bool WalkedImageActive(const ResourcePlan& plan, uint32_t image);
|
||||
// Length of the guest load at rip if it belongs to the walker running on this thread, else 0.
|
||||
// The exception handler completes such a load as a zero read.
|
||||
uint32_t SrtWalkerLoadLength(uint64_t rip);
|
||||
|
||||
@@ -348,14 +348,20 @@ void VisitInstruction(Pass& pass, Block* block, Inst& inst) {
|
||||
} // namespace
|
||||
|
||||
void RewriteToSsa(const BlockList& blocks) {
|
||||
Pass pass;
|
||||
Pass pass;
|
||||
std::unordered_map<Block*, size_t> filled_predecessors;
|
||||
for (auto* block: blocks) {
|
||||
if (block->ImmPredecessors().empty()) {
|
||||
pass.Seal(block);
|
||||
}
|
||||
for (auto& inst: *block) {
|
||||
VisitInstruction(pass, block, inst);
|
||||
}
|
||||
}
|
||||
for (auto* block: blocks) {
|
||||
pass.Seal(block);
|
||||
for (auto* successor: block->ImmSuccessors()) {
|
||||
if (++filled_predecessors[successor] == successor->ImmPredecessors().size()) {
|
||||
pass.Seal(successor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,45 +18,9 @@ using ShaderRecompiler::Spirv::Builder;
|
||||
|
||||
constexpr uint32_t SpirvVersion15 = 0x00010500u;
|
||||
|
||||
struct Parameter {
|
||||
uint32_t input_location = 0;
|
||||
uint32_t output_location = 0;
|
||||
bool flat = false;
|
||||
};
|
||||
|
||||
std::vector<Parameter> GetParameters(const ShaderVertexInputInfo& vertex_info,
|
||||
const ShaderPixelInputInfo* pixel_info) {
|
||||
if (pixel_info == nullptr) {
|
||||
return {};
|
||||
}
|
||||
EXIT_IF(pixel_info->input_num > ShaderVertexInputInfo::RES_MAX);
|
||||
EXIT_IF(pixel_info->stage.program == nullptr);
|
||||
|
||||
std::vector<uint32_t> active_inputs;
|
||||
for (const auto& input: pixel_info->stage.program->info.inputs) {
|
||||
if (input.kind == ShaderRecompiler::IR::StageInputKind::Parameter) {
|
||||
active_inputs.push_back(input.location);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Parameter> parameters;
|
||||
std::array<bool, 32> output_locations {};
|
||||
for (const auto input: active_inputs) {
|
||||
const auto input_location = ShaderPixelParameterMappedLocation(*pixel_info, input);
|
||||
const auto output_location = ShaderPixelParameterLocation(*pixel_info, active_inputs, input);
|
||||
if ((vertex_info.stage.program->param_export_mask & (1u << input_location)) != 0 &&
|
||||
!output_locations[output_location]) {
|
||||
parameters.push_back({input_location, output_location,
|
||||
ShaderPixelParameterIsFlat(*pixel_info, input)});
|
||||
output_locations[output_location] = true;
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
class RectListEmitter {
|
||||
public:
|
||||
RectListEmitter(const std::vector<Parameter>& parameters_, spv::ExecutionModel model)
|
||||
RectListEmitter(const std::vector<RectListParameter>& parameters_, spv::ExecutionModel model)
|
||||
: parameters(parameters_) {
|
||||
builder.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450);
|
||||
|
||||
@@ -315,42 +279,68 @@ private:
|
||||
Result(spv::OpFAdd, vec4_float_type, p1, p2));
|
||||
}
|
||||
|
||||
Builder builder {SpirvVersion15};
|
||||
const std::vector<Parameter>& parameters;
|
||||
std::vector<uint32_t> interfaces;
|
||||
std::vector<uint32_t> inputs;
|
||||
std::vector<uint32_t> outputs;
|
||||
uint32_t main = 0;
|
||||
uint32_t void_type = 0;
|
||||
uint32_t bool_type = 0;
|
||||
uint32_t uint_type = 0;
|
||||
uint32_t int_type = 0;
|
||||
uint32_t float_type = 0;
|
||||
uint32_t vec2_bool_type = 0;
|
||||
uint32_t vec2_float_type = 0;
|
||||
uint32_t vec3_float_type = 0;
|
||||
uint32_t vec4_float_type = 0;
|
||||
uint32_t function_type = 0;
|
||||
uint32_t per_vertex_type = 0;
|
||||
uint32_t ptr_input_float = 0;
|
||||
uint32_t ptr_input_vec4_float = 0;
|
||||
uint32_t ptr_output_float = 0;
|
||||
uint32_t ptr_output_vec4_float = 0;
|
||||
uint32_t gl_in = 0;
|
||||
uint32_t gl_out = 0;
|
||||
uint32_t tess_inner = 0;
|
||||
uint32_t tess_outer = 0;
|
||||
uint32_t tess_coord = 0;
|
||||
uint32_t invocation_id = 0;
|
||||
Builder builder {SpirvVersion15};
|
||||
const std::vector<RectListParameter>& parameters;
|
||||
std::vector<uint32_t> interfaces;
|
||||
std::vector<uint32_t> inputs;
|
||||
std::vector<uint32_t> outputs;
|
||||
uint32_t main = 0;
|
||||
uint32_t void_type = 0;
|
||||
uint32_t bool_type = 0;
|
||||
uint32_t uint_type = 0;
|
||||
uint32_t int_type = 0;
|
||||
uint32_t float_type = 0;
|
||||
uint32_t vec2_bool_type = 0;
|
||||
uint32_t vec2_float_type = 0;
|
||||
uint32_t vec3_float_type = 0;
|
||||
uint32_t vec4_float_type = 0;
|
||||
uint32_t function_type = 0;
|
||||
uint32_t per_vertex_type = 0;
|
||||
uint32_t ptr_input_float = 0;
|
||||
uint32_t ptr_input_vec4_float = 0;
|
||||
uint32_t ptr_output_float = 0;
|
||||
uint32_t ptr_output_vec4_float = 0;
|
||||
uint32_t gl_in = 0;
|
||||
uint32_t gl_out = 0;
|
||||
uint32_t tess_inner = 0;
|
||||
uint32_t tess_outer = 0;
|
||||
uint32_t tess_coord = 0;
|
||||
uint32_t invocation_id = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
RectListShaders BuildRectListShaders(const ShaderVertexInputInfo& vertex_info,
|
||||
const ShaderPixelInputInfo* pixel_info) {
|
||||
const auto parameters = GetParameters(vertex_info, pixel_info);
|
||||
RectListEmitter control(parameters, spv::ExecutionModelTessellationControl);
|
||||
RectListEmitter evaluation(parameters, spv::ExecutionModelTessellationEvaluation);
|
||||
std::vector<RectListParameter> RectListParameters(
|
||||
const ShaderPixelInputInfo& pixel_info, const ShaderRecompiler::IR::CompiledShaderInfo& program) {
|
||||
EXIT_IF(pixel_info.input_num > ShaderVertexInputInfo::RES_MAX);
|
||||
std::vector<uint32_t> active_inputs;
|
||||
for (const auto& input: program.info.inputs) {
|
||||
if (input.kind == ShaderRecompiler::IR::StageInputKind::Parameter) {
|
||||
active_inputs.push_back(input.location);
|
||||
}
|
||||
}
|
||||
std::vector<RectListParameter> parameters;
|
||||
for (const auto input: active_inputs) {
|
||||
parameters.push_back({ShaderPixelParameterMappedLocation(pixel_info, input),
|
||||
ShaderPixelParameterLocation(pixel_info, active_inputs, input),
|
||||
ShaderPixelParameterIsFlat(pixel_info, input)});
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
RectListShaders BuildRectListShaders(std::span<const RectListParameter> parameters,
|
||||
uint32_t param_export_mask) {
|
||||
std::vector<RectListParameter> exported;
|
||||
std::array<bool, 32> output_locations {};
|
||||
for (const auto& parameter: parameters) {
|
||||
if ((param_export_mask & (1u << parameter.input_location)) != 0 &&
|
||||
!output_locations[parameter.output_location]) {
|
||||
exported.push_back(parameter);
|
||||
output_locations[parameter.output_location] = true;
|
||||
}
|
||||
}
|
||||
RectListEmitter control(exported, spv::ExecutionModelTessellationControl);
|
||||
RectListEmitter evaluation(exported, spv::ExecutionModelTessellationEvaluation);
|
||||
return {control.EmitControl(), evaluation.EmitEvaluation()};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,20 +2,35 @@
|
||||
#define EMULATOR_SRC_GRAPHICS_SHADER_RECTLISTSHADER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
struct ShaderPixelInputInfo;
|
||||
struct ShaderVertexInputInfo;
|
||||
|
||||
namespace ShaderRecompiler::IR {
|
||||
struct CompiledShaderInfo;
|
||||
} // namespace ShaderRecompiler::IR
|
||||
|
||||
// A pixel parameter the generated stages forward from vertex export input_location.
|
||||
struct RectListParameter {
|
||||
uint32_t input_location = 0;
|
||||
uint32_t output_location = 0;
|
||||
bool flat = false;
|
||||
};
|
||||
|
||||
struct RectListShaders {
|
||||
std::vector<uint32_t> control;
|
||||
std::vector<uint32_t> evaluation;
|
||||
};
|
||||
|
||||
RectListShaders BuildRectListShaders(const ShaderVertexInputInfo& vertex_info,
|
||||
const ShaderPixelInputInfo* pixel_info);
|
||||
// Every parameter a rectangle-list pixel program reads.
|
||||
std::vector<RectListParameter> RectListParameters(
|
||||
const ShaderPixelInputInfo& pixel_info, const ShaderRecompiler::IR::CompiledShaderInfo& program);
|
||||
// Forwards the parameters the vertex stage exports, one per output location.
|
||||
RectListShaders BuildRectListShaders(std::span<const RectListParameter> parameters,
|
||||
uint32_t param_export_mask);
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include "graphics/guest_gpu/hardwareContext.h"
|
||||
#include "graphics/host_gpu/renderer/renderContext.h"
|
||||
#include "graphics/shader/recompiler/ShaderRecompiler.h"
|
||||
#include "graphics/shader/recompiler/Tessellation.h"
|
||||
#include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h"
|
||||
#include "graphics/shader/shaderCompiler.h"
|
||||
#include "graphics/shader/shaderVertexMetadata.h"
|
||||
@@ -53,8 +52,6 @@ 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);
|
||||
@@ -425,6 +422,7 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
|
||||
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(info.indirect_offsets.vertex | (uint32_t {info.indirect_offsets.instance} << 8u));
|
||||
key.push_back(static_cast<uint32_t>(info.clip_space.enabled));
|
||||
if (info.clip_space.enabled) {
|
||||
for (const float value: info.clip_space.scale) {
|
||||
@@ -493,7 +491,9 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
|
||||
key.push_back(static_cast<uint32_t>(info.object_primitive_id));
|
||||
key.push_back(static_cast<uint32_t>(info.alpha_blend_source));
|
||||
key.insert(key.end(), std::begin(info.target_output_mode), std::end(info.target_output_mode));
|
||||
key.push_back(info.target_packed_unorm_mask | (uint32_t {info.target_round_mode} << 8u));
|
||||
key.push_back(info.target_packed_unorm_mask | (uint32_t {info.target_round_mode} << 8u) |
|
||||
(uint32_t {info.target_uint_mask} << 16u) |
|
||||
(uint32_t {info.target_sint_mask} << 24u));
|
||||
const auto mask = info.target_shader_mask;
|
||||
key.push_back((mask | (mask >> 1u) | (mask >> 2u) | (mask >> 3u)) & 0x11111111u);
|
||||
for (uint32_t base = 0; base < info.target_export_mapping.size(); base += 4u) {
|
||||
@@ -662,18 +662,6 @@ 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);
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -34,19 +34,14 @@ enum class ShaderType {
|
||||
TessellationEvaluation
|
||||
};
|
||||
|
||||
namespace ShaderRecompiler::IR {
|
||||
struct CompiledShaderInfo;
|
||||
} // namespace ShaderRecompiler::IR
|
||||
|
||||
struct ShaderProgram;
|
||||
struct ShaderModule;
|
||||
|
||||
struct ShaderStageRuntime {
|
||||
const ShaderRecompiler::IR::CompiledShaderInfo* program = nullptr;
|
||||
const ShaderRecompiler::IR::ResourceSnapshot* resources = nullptr;
|
||||
const ShaderProgram* handle = nullptr;
|
||||
const ShaderModule* module = nullptr;
|
||||
const ShaderRecompiler::IR::ResourceSnapshot* resources = nullptr;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return program != nullptr && resources != nullptr;
|
||||
return module != nullptr && resources != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -129,6 +124,18 @@ struct ShaderTessellationInputInfo {
|
||||
uint32_t output_topology = 0;
|
||||
};
|
||||
|
||||
// GS user-data slots an indirect draw packet writes its record's base vertex and first instance
|
||||
// to. Like the hardware index VGPRs, the vertex shader's indices exclude both bases once either
|
||||
// slot is set.
|
||||
struct ShaderIndirectOffsets {
|
||||
static constexpr uint8_t None = UINT8_MAX;
|
||||
|
||||
uint8_t vertex = None;
|
||||
uint8_t instance = None;
|
||||
|
||||
bool operator==(const ShaderIndirectOffsets&) const = default;
|
||||
};
|
||||
|
||||
struct ShaderVertexInputInfo {
|
||||
static constexpr int RES_MAX = 32;
|
||||
|
||||
@@ -151,6 +158,7 @@ struct ShaderVertexInputInfo {
|
||||
ShaderClipSpaceTransform clip_space;
|
||||
ShaderMeshInputInfo mesh;
|
||||
ShaderTessellationInputInfo tess;
|
||||
ShaderIndirectOffsets indirect_offsets;
|
||||
bool fetch_external = false;
|
||||
bool fetch_embedded = false;
|
||||
};
|
||||
@@ -186,6 +194,9 @@ struct ShaderPixelInputInfo {
|
||||
uint32_t target_shader_mask = UINT32_MAX;
|
||||
std::array<Prospero::ColorComponentMapping, 8> target_export_mapping = {};
|
||||
uint8_t target_packed_unorm_mask = 0;
|
||||
// Exports to UINT and SINT targets, which take integer outputs of the same signedness.
|
||||
uint8_t target_uint_mask = 0;
|
||||
uint8_t target_sint_mask = 0;
|
||||
// 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;
|
||||
|
||||
@@ -32,6 +32,7 @@ void BuildStageStaticKey(const ShaderComputeInputInfo& input_info, std::vector<u
|
||||
|
||||
ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context& context,
|
||||
const HW::UserConfig& user_config, ShaderVertexInputInfo& input_info);
|
||||
// Each stage's tess holds the VGT fields; the caller adds the layout reflected from the programs.
|
||||
std::array<ShaderParams, 3>
|
||||
PrepareTessellationPrograms(const HW::VertexShaderInfo& regs, const HW::Context& context,
|
||||
std::array<ShaderVertexInputInfo, 3>& input_info);
|
||||
|
||||
+64
-30
@@ -9,6 +9,7 @@
|
||||
#include "graphics/host_gpu/renderer/renderContext.h"
|
||||
#include "libs/errno.h"
|
||||
#include "libs/libs.h"
|
||||
#include "loader/systemContent.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -63,15 +64,21 @@ constexpr int PROT_GPU_WRITE = 0x20;
|
||||
|
||||
enum class GpuAccessMode { NoAccess, Read, Write, ReadWrite };
|
||||
|
||||
constexpr uint64_t PAGE_TABLE_POOL_SIZE = 4ull * 1024ull * 1024ull * 1024ull;
|
||||
constexpr uint64_t PAGE_TABLE_GRANULARITY = 2ull * 1024ull * 1024ull;
|
||||
constexpr int PAGE_TABLE_POOL_ENTRIES =
|
||||
static_cast<int>(PAGE_TABLE_POOL_SIZE / PAGE_TABLE_GRANULARITY);
|
||||
constexpr uint64_t DEFAULT_FLEXIBLE_MEMORY_SIZE = 1ull * 1024ull * 1024ull * 1024ull;
|
||||
constexpr uint64_t PAGE_TABLE_GRANULARITY = 2ull * 1024ull * 1024ull;
|
||||
constexpr uint64_t DEFAULT_FLEXIBLE_MEMORY_SIZE = 448ull * 1024ull * 1024ull;
|
||||
// A game gets 12.5 GiB. A fixed 64 MiB cache and mapping table memory past the 64 MiB included
|
||||
// for each of the CPU, GPU and AMM tables come off the top; flexible and direct memory share what
|
||||
// is left. One 4 KiB table maps 2 MiB.
|
||||
constexpr uint64_t GUEST_MEMORY_BUDGET = 12800ull * 1024ull * 1024ull;
|
||||
constexpr uint64_t RESERVED_CACHE_SIZE = 64ull * 1024ull * 1024ull;
|
||||
constexpr uint64_t INCLUDED_TABLE_MEMORY = 64ull * 1024ull * 1024ull;
|
||||
constexpr uint64_t MAPPING_TABLE_SIZE = 4ull * 1024ull;
|
||||
|
||||
static uint64_t g_flexible_memory_size = DEFAULT_FLEXIBLE_MEMORY_SIZE;
|
||||
static bool g_flexible_memory_size_frozen = false;
|
||||
static Graphics::RenderContext* g_gpu_resources = nullptr;
|
||||
static uint64_t g_guest_memory_size = GUEST_MEMORY_BUDGET - RESERVED_CACHE_SIZE;
|
||||
static uint64_t g_flexible_memory_size = DEFAULT_FLEXIBLE_MEMORY_SIZE;
|
||||
static uint64_t g_cpu_table_memory = INCLUDED_TABLE_MEMORY;
|
||||
static uint64_t g_gpu_table_memory = INCLUDED_TABLE_MEMORY;
|
||||
static Graphics::RenderContext* g_gpu_resources = nullptr;
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
static void (*g_test_gpu_range)(bool, uint64_t, size_t) = nullptr;
|
||||
#endif
|
||||
@@ -664,11 +671,8 @@ public:
|
||||
|
||||
KYTY_CLASS_NO_COPY(PhysicalMemory);
|
||||
|
||||
static constexpr uint64_t TotalSize() { return static_cast<uint64_t>(13824) * 1024 * 1024; }
|
||||
static uint64_t Size() {
|
||||
EXIT_IF(g_flexible_memory_size >= TotalSize());
|
||||
return TotalSize() - g_flexible_memory_size;
|
||||
}
|
||||
static uint64_t TotalSize() { return g_guest_memory_size; }
|
||||
static uint64_t Size() { return TotalSize() - g_flexible_memory_size; }
|
||||
|
||||
bool Alloc(uint64_t search_start, uint64_t search_end, size_t len, size_t alignment,
|
||||
uint64_t* phys_addr_out, int memory_type,
|
||||
@@ -738,6 +742,8 @@ public:
|
||||
|
||||
static uint64_t Size() { return g_flexible_memory_size; }
|
||||
uint64_t Available();
|
||||
void Charge(uint64_t size);
|
||||
void Release(uint64_t size);
|
||||
|
||||
bool Map(uint64_t vaddr, size_t len, int prot, VirtualMemory::Mode mode, GpuAccessMode gpu_mode,
|
||||
const char* name);
|
||||
@@ -996,7 +1002,22 @@ static bool SelfTestSub64SharedPlaceholderAlias() {
|
||||
static bool ReplaceFixedRangeWithReserved(uint64_t start, uint64_t size);
|
||||
|
||||
void Initialize() {
|
||||
g_flexible_memory_size_frozen = true;
|
||||
const auto params = Loader::SystemContentGetMemoryParams();
|
||||
if (params.flexible_size != 0) {
|
||||
g_flexible_memory_size = params.flexible_size;
|
||||
}
|
||||
g_cpu_table_memory = std::max(params.cpu_table_memory, INCLUDED_TABLE_MEMORY);
|
||||
g_gpu_table_memory = std::max(params.gpu_table_memory, INCLUDED_TABLE_MEMORY);
|
||||
|
||||
const auto table_overhead = g_cpu_table_memory + g_gpu_table_memory +
|
||||
std::max(params.amm_table_memory, INCLUDED_TABLE_MEMORY) -
|
||||
3 * INCLUDED_TABLE_MEMORY;
|
||||
EXIT_IF(g_flexible_memory_size + table_overhead >= GUEST_MEMORY_BUDGET - RESERVED_CACHE_SIZE);
|
||||
g_guest_memory_size = GUEST_MEMORY_BUDGET - RESERVED_CACHE_SIZE - table_overhead;
|
||||
LOGF("\t flexible memory size = %" PRIu64 " MiB, direct memory size = %" PRIu64 " MiB\n",
|
||||
g_flexible_memory_size / (1024ull * 1024ull),
|
||||
(g_guest_memory_size - g_flexible_memory_size) / (1024ull * 1024ull));
|
||||
|
||||
VirtualMemory::Init();
|
||||
g_guest_address_space = std::make_unique<GuestAddressSpace>(PhysicalMemory::TotalSize());
|
||||
g_physical_memory = std::make_unique<PhysicalMemory>();
|
||||
@@ -1064,15 +1085,6 @@ void RegisterCallbacks(callback_func_t alloc_func, callback_func_t free_func) {
|
||||
}
|
||||
}
|
||||
|
||||
void SetFlexibleMemorySize(uint64_t size) {
|
||||
constexpr uint64_t GuestPageSize = 0x4000;
|
||||
EXIT_IF(g_flexible_memory_size_frozen || g_guest_address_space != nullptr);
|
||||
EXIT_IF(size == 0 || (size & (GuestPageSize - 1u)) != 0 || size >= PhysicalMemory::TotalSize());
|
||||
g_flexible_memory_size = size;
|
||||
LOGF("\t flexible memory size = 0x%016" PRIx64 " (%" PRIu64 " MiB)\n", size,
|
||||
size / (1024ull * 1024ull));
|
||||
}
|
||||
|
||||
bool PhysicalMemory::Alloc(uint64_t search_start, uint64_t search_end, size_t len, size_t alignment,
|
||||
uint64_t* phys_addr_out, int memory_type, AllocationKind kind) {
|
||||
if (phys_addr_out == nullptr) {
|
||||
@@ -1967,6 +1979,19 @@ uint64_t FlexibleMemory::Available() {
|
||||
return (Size() >= m_allocated_total ? Size() - m_allocated_total : 0);
|
||||
}
|
||||
|
||||
void FlexibleMemory::Charge(uint64_t size) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
m_allocated_total += size;
|
||||
}
|
||||
|
||||
void FlexibleMemory::Release(uint64_t size) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
EXIT_IF(size > m_allocated_total);
|
||||
m_allocated_total -= size;
|
||||
}
|
||||
|
||||
void PooledMemory::AddFreeUnlocked(uint64_t start, uint64_t size) {
|
||||
if (size == 0) {
|
||||
return;
|
||||
@@ -2640,12 +2665,12 @@ int KYTY_SYSV_ABI KernelGetPageTableStats(int* cpu_total, int* cpu_available, in
|
||||
const auto gpu_used =
|
||||
(g_virtual_ranges != nullptr ? g_virtual_ranges->CountPageTableEntries(true) : 0);
|
||||
|
||||
*cpu_total = PAGE_TABLE_POOL_ENTRIES;
|
||||
*gpu_total = PAGE_TABLE_POOL_ENTRIES;
|
||||
*cpu_available = PAGE_TABLE_POOL_ENTRIES -
|
||||
static_cast<int>(std::min<uint64_t>(cpu_used, PAGE_TABLE_POOL_ENTRIES));
|
||||
*gpu_available = PAGE_TABLE_POOL_ENTRIES -
|
||||
static_cast<int>(std::min<uint64_t>(gpu_used, PAGE_TABLE_POOL_ENTRIES));
|
||||
const auto cpu_tables = g_cpu_table_memory / MAPPING_TABLE_SIZE;
|
||||
const auto gpu_tables = g_gpu_table_memory / MAPPING_TABLE_SIZE;
|
||||
*cpu_total = static_cast<int>(cpu_tables);
|
||||
*gpu_total = static_cast<int>(gpu_tables);
|
||||
*cpu_available = static_cast<int>(cpu_tables - std::min(cpu_used, cpu_tables));
|
||||
*gpu_available = static_cast<int>(gpu_tables - std::min(gpu_used, gpu_tables));
|
||||
|
||||
LOGF_COLOR(Log::Color::Green,
|
||||
"\t cpu_total = %d\n"
|
||||
@@ -3770,6 +3795,14 @@ uint64_t AllocateProgramMemory(uint64_t search_addr, uint64_t size, VirtualMemor
|
||||
return AllocateGuestRuntimeMemory(search_addr, size, mode, name, VirtualRangeType::Code, false);
|
||||
}
|
||||
|
||||
void ChargeFlexibleMemory(uint64_t size) {
|
||||
g_flexible_memory->Charge(size);
|
||||
}
|
||||
|
||||
void ReleaseFlexibleMemory(uint64_t size) {
|
||||
g_flexible_memory->Release(size);
|
||||
}
|
||||
|
||||
void SetProgramMemoryProtection(uint64_t vaddr, uint64_t size, VirtualMemory::Mode mode) {
|
||||
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
|
||||
|
||||
@@ -4358,7 +4391,8 @@ DebugStats GetDebugStats() {
|
||||
stats.direct_total = PhysicalMemory::Size();
|
||||
stats.flexible_total = FlexibleMemory::Size();
|
||||
stats.pool_committed = g_memory_pool_committed.load(std::memory_order_relaxed);
|
||||
stats.page_entries_total = PAGE_TABLE_POOL_ENTRIES;
|
||||
stats.cpu_page_entries_total = g_cpu_table_memory / MAPPING_TABLE_SIZE;
|
||||
stats.gpu_page_entries_total = g_gpu_table_memory / MAPPING_TABLE_SIZE;
|
||||
if (g_physical_memory != nullptr) {
|
||||
Common::LockGuard lock(g_physical_memory->GetMutex());
|
||||
for (const auto& [start, block]: g_physical_memory->GetPhysicalBlocks()) {
|
||||
|
||||
+7
-4
@@ -26,9 +26,10 @@ struct DebugStats {
|
||||
uint64_t flexible_total = 0;
|
||||
uint64_t flexible_used = 0;
|
||||
uint64_t pool_committed = 0;
|
||||
uint64_t cpu_page_entries = 0; // 2 MiB page-table entries in use.
|
||||
uint64_t gpu_page_entries = 0;
|
||||
uint64_t page_entries_total = 0; // Per side.
|
||||
uint64_t cpu_page_entries = 0; // 2 MiB page-table entries in use.
|
||||
uint64_t gpu_page_entries = 0;
|
||||
uint64_t cpu_page_entries_total = 0;
|
||||
uint64_t gpu_page_entries_total = 0;
|
||||
};
|
||||
[[nodiscard]] DebugStats GetDebugStats();
|
||||
|
||||
@@ -125,7 +126,6 @@ static_assert(sizeof(KernelMemoryPoolBlockStats) == 16,
|
||||
"KernelMemoryPoolBlockStats struct size is incorrect");
|
||||
|
||||
void RegisterCallbacks(callback_func_t alloc_func, callback_func_t free_func);
|
||||
void SetFlexibleMemorySize(uint64_t size);
|
||||
int AllocateDirectMemory(int64_t search_start, int64_t search_end, size_t size, size_t alignment,
|
||||
int memory_type, int64_t* phys_addr_out, bool automatic = false);
|
||||
int MapAutomaticMemory(uint64_t vaddr, size_t size, int type, int prot);
|
||||
@@ -197,6 +197,9 @@ int KYTY_SYSV_ABI KernelMemoryPoolGetBlockStats(KernelMemoryPoolBlockStats* outp
|
||||
|
||||
uint64_t AllocateProgramMemory(uint64_t search_addr, uint64_t size,
|
||||
Common::VirtualMemory::Mode mode, const char* name);
|
||||
// Loaded modules' segments use flexible memory without taking its backing.
|
||||
void ChargeFlexibleMemory(uint64_t size);
|
||||
void ReleaseFlexibleMemory(uint64_t size);
|
||||
void SetProgramMemoryProtection(uint64_t vaddr, uint64_t size, Common::VirtualMemory::Mode mode);
|
||||
uint64_t AllocateRuntimeMemory(uint64_t search_addr, uint64_t size,
|
||||
Common::VirtualMemory::Mode mode, const char* name,
|
||||
|
||||
@@ -125,6 +125,7 @@ if(APPLE)
|
||||
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
|
||||
install(TARGETS kyty_updater DESTINATION "KytyPS5.app/Contents/MacOS")
|
||||
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION "KytyPS5.app/Contents/Resources/assets/sounds")
|
||||
install(FILES ${KYTY_NOTO_SANS_FONTS} DESTINATION "KytyPS5.app/Contents/Resources/assets/fonts")
|
||||
install(DIRECTORY "${CMAKE_SOURCE_DIR}/assets/cursors" DESTINATION "KytyPS5.app/Contents/Resources/assets")
|
||||
# Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle.
|
||||
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
<property name="value"><number>60</number></property>
|
||||
</widget>
|
||||
</item>
|
||||
<!--
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_shader_optimization">
|
||||
<property name="text"><string>Shader optimization:</string></property>
|
||||
@@ -151,6 +152,7 @@
|
||||
<property name="toolTip"><string>Optimize shaders for code size or performance</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
-->
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_gpu">
|
||||
<property name="text"><string>GPU:</string></property>
|
||||
@@ -191,6 +193,12 @@
|
||||
<property name="text"><string>Enable HDR display support</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_shader_cache">
|
||||
<property name="toolTip"><string>Save compiled shaders and pipelines per game and load them at startup.</string></property>
|
||||
<property name="text"><string>Cache shaders</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -112,6 +112,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="close_terminal">
|
||||
<property name="text">
|
||||
<string>Close terminal automatically</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="themeLayout">
|
||||
<item>
|
||||
|
||||
@@ -4,26 +4,39 @@
|
||||
#include <QDialog>
|
||||
#include <QIcon>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include <functional>
|
||||
|
||||
class Configuration;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
|
||||
class AdditionalContentDialog final: public QDialog {
|
||||
public:
|
||||
AdditionalContentDialog(const Configuration& game, const QString& folder,
|
||||
QWidget* parent = nullptr);
|
||||
// entries are the game's user add-ons, "LABEL" or "LABEL=DIR"; save receives every edit.
|
||||
AdditionalContentDialog(const Configuration& game, const QString& folder, QStringList entries,
|
||||
std::function<void(const QStringList&)> save,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
private:
|
||||
void Scan();
|
||||
void Add();
|
||||
void Remove();
|
||||
|
||||
QString m_title_id;
|
||||
QString m_folder;
|
||||
QIcon m_game_icon;
|
||||
QLabel* m_status = nullptr;
|
||||
QListWidget* m_list = nullptr;
|
||||
QPushButton* m_open = nullptr;
|
||||
QString m_title_id;
|
||||
QString m_folder;
|
||||
QStringList m_entries;
|
||||
std::function<void(const QStringList&)> m_save;
|
||||
QIcon m_game_icon;
|
||||
QLabel* m_status = nullptr;
|
||||
QListWidget* m_list = nullptr;
|
||||
QPushButton* m_open = nullptr;
|
||||
QPushButton* m_remove = nullptr;
|
||||
QLineEdit* m_label_edit = nullptr;
|
||||
QLineEdit* m_dir_edit = nullptr;
|
||||
};
|
||||
|
||||
#endif // ADDITIONAL_CONTENT_DIALOG_H
|
||||
@@ -102,16 +102,17 @@ public:
|
||||
|
||||
Configuration() = default;
|
||||
|
||||
QString name;
|
||||
QString title_id; /* Serial / title id from sce_sys/param.json */
|
||||
QString gameVersion; /* appVersion / contentVersion from sce_sys/param.json */
|
||||
QString firmwareVer; /* requiredSystemSoftwareVersion from sce_sys/param.json */
|
||||
QString basedir; /* Game base directory */
|
||||
QString game_path; /* Launcher-unique game path */
|
||||
QString addons_dir; /* Global setting; not saved per game. */
|
||||
bool custom_settings = false;
|
||||
GameStatus game_status = GameStatus::Unknown;
|
||||
QString game_comment;
|
||||
QString name;
|
||||
QString title_id; /* Serial / title id from sce_sys/param.json */
|
||||
QString gameVersion; /* appVersion / contentVersion from sce_sys/param.json */
|
||||
QString firmwareVer; /* requiredSystemSoftwareVersion from sce_sys/param.json */
|
||||
QString basedir; /* Game base directory */
|
||||
QString game_path; /* Launcher-unique game path */
|
||||
QString addons_dir; /* Global setting; not saved per game. */
|
||||
QStringList addcont; /* User add-ons ("LABEL" or "LABEL=DIR"), saved by title ID. */
|
||||
bool custom_settings = false;
|
||||
GameStatus game_status = GameStatus::Unknown;
|
||||
QString game_comment;
|
||||
|
||||
// Controller preferences always come from the global configuration.
|
||||
ControllerSettings controller;
|
||||
@@ -130,6 +131,7 @@ public:
|
||||
bool sync_raw_image_buffers = false;
|
||||
bool tessellation_enabled = false;
|
||||
bool occlusion_queries_enabled = false;
|
||||
bool shader_cache_enabled = true;
|
||||
bool trophy_enabled = true;
|
||||
int trophy_duration_seconds = 6;
|
||||
bool perf_overlay_enabled = false;
|
||||
@@ -171,6 +173,7 @@ public:
|
||||
sync_raw_image_buffers = other.sync_raw_image_buffers;
|
||||
tessellation_enabled = other.tessellation_enabled;
|
||||
occlusion_queries_enabled = other.occlusion_queries_enabled;
|
||||
shader_cache_enabled = other.shader_cache_enabled;
|
||||
trophy_enabled = other.trophy_enabled;
|
||||
trophy_duration_seconds = other.trophy_duration_seconds;
|
||||
perf_overlay_enabled = other.perf_overlay_enabled;
|
||||
|
||||
@@ -87,6 +87,7 @@ private:
|
||||
QString m_runtime_directory;
|
||||
QStringList m_game_dirs;
|
||||
QString m_addons_dir;
|
||||
QMap<QString, QStringList> m_addcont; // User add-ons by title ID.
|
||||
Configuration m_global_info;
|
||||
QMap<QString, Configuration*> m_custom_infos;
|
||||
CompatibilityDatabase* m_compatibility = nullptr;
|
||||
|
||||
@@ -22,7 +22,7 @@ public:
|
||||
explicit MainDialog(QWidget* parent = nullptr);
|
||||
~MainDialog() override = default;
|
||||
|
||||
void RunInterpreter(QProcess* process, const Configuration& info);
|
||||
void RunInterpreter(QProcess* process, const Configuration& info, bool close_terminal);
|
||||
|
||||
static void WriteSettings(QSettings& s);
|
||||
static void ReadSettings(QSettings& s);
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
#include <QDesktopServices>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QPixmap>
|
||||
#include <QPushButton>
|
||||
@@ -16,8 +19,11 @@
|
||||
#include <QVBoxLayout>
|
||||
|
||||
AdditionalContentDialog::AdditionalContentDialog(const Configuration& game, const QString& folder,
|
||||
QWidget* parent)
|
||||
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()), m_folder(folder) {
|
||||
QStringList entries,
|
||||
std::function<void(const QStringList&)> save,
|
||||
QWidget* parent)
|
||||
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()), m_folder(folder),
|
||||
m_entries(std::move(entries)), m_save(std::move(save)) {
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
setWindowTitle(tr("Additional Content - %1").arg(game.name));
|
||||
resize(720, 480);
|
||||
@@ -39,6 +45,28 @@ AdditionalContentDialog::AdditionalContentDialog(const Configuration& game, cons
|
||||
m_list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
layout->addWidget(m_list, 1);
|
||||
|
||||
auto* add_layout = new QHBoxLayout;
|
||||
m_label_edit = new QLineEdit(this);
|
||||
m_label_edit->setPlaceholderText(tr("Label or content ID"));
|
||||
m_dir_edit = new QLineEdit(this);
|
||||
m_dir_edit->setPlaceholderText(tr("Add-on data folder (empty for a license only)"));
|
||||
auto* browse = new QPushButton(tr("Browse..."), this);
|
||||
auto* add = new QPushButton(tr("Add"), this);
|
||||
add->setDefault(true);
|
||||
add_layout->addWidget(m_label_edit, 1);
|
||||
add_layout->addWidget(m_dir_edit, 2);
|
||||
add_layout->addWidget(browse);
|
||||
add_layout->addWidget(add);
|
||||
layout->addLayout(add_layout);
|
||||
connect(browse, &QPushButton::clicked, this, [this]() {
|
||||
const auto dir =
|
||||
QFileDialog::getExistingDirectory(this, tr("Add-on data folder"), m_dir_edit->text());
|
||||
if (!dir.isEmpty()) {
|
||||
m_dir_edit->setText(QDir::toNativeSeparators(dir));
|
||||
}
|
||||
});
|
||||
connect(add, &QPushButton::clicked, this, &AdditionalContentDialog::Add);
|
||||
|
||||
m_status = new QLabel(this);
|
||||
m_status->setTextFormat(Qt::PlainText);
|
||||
m_status->setWordWrap(true);
|
||||
@@ -47,10 +75,15 @@ AdditionalContentDialog::AdditionalContentDialog(const Configuration& game, cons
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
|
||||
auto* refresh = buttons->addButton(tr("Refresh"), QDialogButtonBox::ActionRole);
|
||||
m_open = buttons->addButton(tr("Open folder"), QDialogButtonBox::ActionRole);
|
||||
m_remove = buttons->addButton(tr("Remove"), QDialogButtonBox::ActionRole);
|
||||
layout->addWidget(buttons);
|
||||
connect(refresh, &QPushButton::clicked, this, &AdditionalContentDialog::Scan);
|
||||
connect(m_open, &QPushButton::clicked, this,
|
||||
[this]() { QDesktopServices::openUrl(QUrl::fromLocalFile(m_folder)); });
|
||||
connect(m_remove, &QPushButton::clicked, this, &AdditionalContentDialog::Remove);
|
||||
connect(m_list, &QListWidget::currentItemChanged, this, [this](const QListWidgetItem* item) {
|
||||
m_remove->setEnabled(item != nullptr && item->data(Qt::UserRole).isValid());
|
||||
});
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
|
||||
|
||||
Scan();
|
||||
@@ -73,18 +106,54 @@ void AdditionalContentDialog::Scan() {
|
||||
tr("%1\nInstalled").arg(title), m_list);
|
||||
item->setToolTip(QString::fromStdString(entry.content_id));
|
||||
}
|
||||
for (const auto& entry: m_entries) {
|
||||
const auto label = entry.section('=', 0, 0);
|
||||
const auto dir = entry.section('=', 1);
|
||||
auto text = tr("%1\nLicense only").arg(label);
|
||||
if (!dir.isEmpty()) {
|
||||
text = (QDir(dir).exists() ? tr("%1\nData: %2") : tr("%1\nData folder not found: %2"))
|
||||
.arg(label, QDir::toNativeSeparators(dir));
|
||||
}
|
||||
auto* item = new QListWidgetItem(m_game_icon, text, m_list);
|
||||
item->setData(Qt::UserRole, entry);
|
||||
}
|
||||
m_remove->setEnabled(false);
|
||||
|
||||
if (m_folder.isEmpty()) {
|
||||
if (m_list->count() != 0) {
|
||||
m_status->setText(tr("%n add-on(s)", nullptr, m_list->count()));
|
||||
} else if (m_folder.isEmpty()) {
|
||||
m_status->setText(tr("No add-ons folder is set. Open Global settings and set the "
|
||||
"Add-ons folder to the location of your extracted add-on packages."));
|
||||
} else if (!folder_exists) {
|
||||
m_status->setText(tr("The add-ons folder does not exist:\n%1\nCheck the path in Global "
|
||||
"settings.")
|
||||
.arg(QDir::toNativeSeparators(m_folder)));
|
||||
} else if (m_list->count() == 0) {
|
||||
} else {
|
||||
m_status->setText(
|
||||
tr("No add-ons found for this game in:\n%1").arg(QDir::toNativeSeparators(m_folder)));
|
||||
} else {
|
||||
m_status->setText(tr("%n installed add-on(s)", nullptr, m_list->count()));
|
||||
}
|
||||
}
|
||||
|
||||
void AdditionalContentDialog::Add() {
|
||||
// A content ID ends with its entitlement label.
|
||||
const auto label = m_label_edit->text().trimmed().section('-', -1);
|
||||
const auto dir = m_dir_edit->text().trimmed();
|
||||
const auto entry = dir.isEmpty() ? label : label + '=' + QDir(dir).absolutePath();
|
||||
if (!Common::ParseAddContent(entry.toStdString())) {
|
||||
m_status->setText(tr("Enter a 16-character entitlement label (A-Z, 0-9) and an existing "
|
||||
"data folder, or no folder for a license only."));
|
||||
return;
|
||||
}
|
||||
m_entries.removeIf([&](const QString& e) { return e.section('=', 0, 0) == label; });
|
||||
m_entries.append(entry);
|
||||
m_save(m_entries);
|
||||
m_label_edit->clear();
|
||||
m_dir_edit->clear();
|
||||
Scan();
|
||||
}
|
||||
|
||||
void AdditionalContentDialog::Remove() {
|
||||
m_entries.removeOne(m_list->currentItem()->data(Qt::UserRole).toString());
|
||||
m_save(m_entries);
|
||||
Scan();
|
||||
}
|
||||
@@ -17,6 +17,7 @@ QVariantMap Configuration::GameSettings() const {
|
||||
KYTY_CFG_SET(sync_raw_image_buffers);
|
||||
KYTY_CFG_SET(tessellation_enabled);
|
||||
KYTY_CFG_SET(occlusion_queries_enabled);
|
||||
KYTY_CFG_SET(shader_cache_enabled);
|
||||
KYTY_CFG_SET(trophy_enabled);
|
||||
KYTY_CFG_SET(trophy_duration_seconds);
|
||||
KYTY_CFG_SET(perf_overlay_enabled);
|
||||
@@ -68,7 +69,8 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
|
||||
KYTY_CFG_GET(sync_raw_image_buffers);
|
||||
KYTY_CFG_GET(tessellation_enabled);
|
||||
KYTY_CFG_GET(occlusion_queries_enabled);
|
||||
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
|
||||
shader_cache_enabled = s.value("shader_cache_enabled", shader_cache_enabled).toBool();
|
||||
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
|
||||
KYTY_CFG_GET(trophy_duration_seconds);
|
||||
KYTY_CFG_GET(perf_overlay_enabled);
|
||||
KYTY_CFG_GET(skip_notice_screen);
|
||||
|
||||
Loaded 100 of 136 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user