mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-28 16:23:05 +00:00
Compare commits
49
Commits
f90a8b1589
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c01401862 | ||
|
|
a833e1b735 | ||
|
|
1be1c6c4f9 | ||
|
|
69fdb03dc2 | ||
|
|
2efd31443f | ||
|
|
9eef2e1434 | ||
|
|
39faa62ebd | ||
|
|
7ff8aff5e6 | ||
|
|
64a554dea7 | ||
|
|
4b0282e343 | ||
|
|
ffc81377b2 | ||
|
|
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 |
No files matched your search
Vendored
+22
@@ -42,6 +42,28 @@ if(NOT KYTY_CLANG_CL)
|
|||||||
endif()
|
endif()
|
||||||
install(FILES "${openxr_SOURCE_DIR}/LICENSE" DESTINATION "licenses/openxr")
|
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
|
# The guest instruction patcher needs a decoder/encoder and a small
|
||||||
# trampoline assembler. Pin the same revisions used by shadPS4's implementation.
|
# trampoline assembler. Pin the same revisions used by shadPS4's implementation.
|
||||||
set(XBYAK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
set(XBYAK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||||
|
|||||||
+17
-2
@@ -407,6 +407,8 @@ add_executable(scalar_provenance_tests EXCLUDE_FROM_ALL
|
|||||||
target_link_libraries(scalar_provenance_tests fmt::fmt xbyak::xbyak)
|
target_link_libraries(scalar_provenance_tests fmt::fmt xbyak::xbyak)
|
||||||
target_include_directories(scalar_provenance_tests PRIVATE ${inc_headers})
|
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
|
add_executable(page_manager_tests EXCLUDE_FROM_ALL
|
||||||
"${KYTY_TESTS_DIR}/PageManagerTests.cpp"
|
"${KYTY_TESTS_DIR}/PageManagerTests.cpp"
|
||||||
"${KYTY_SOURCE_DIR}/graphics/host_gpu/pageManager.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 image_page_table COMMAND $<TARGET_FILE:image_page_table_tests>)
|
||||||
add_test(NAME memory_tracker COMMAND $<TARGET_FILE:memory_tracker_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 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 archive_file COMMAND $<TARGET_FILE:archive_file_tests>)
|
||||||
add_test(NAME trophy_system COMMAND $<TARGET_FILE:trophy_system_tests>)
|
add_test(NAME trophy_system COMMAND $<TARGET_FILE:trophy_system_tests>)
|
||||||
add_test(NAME avplayer_file COMMAND $<TARGET_FILE:avplayer_file_tests>)
|
add_test(NAME avplayer_file COMMAND $<TARGET_FILE:avplayer_file_tests>)
|
||||||
@@ -697,8 +700,6 @@ if(BUILD_TESTING)
|
|||||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --stream-buffer-only)
|
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --stream-buffer-only)
|
||||||
add_test(NAME gpu_command_lane
|
add_test(NAME gpu_command_lane
|
||||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --gpu-command-lane-only)
|
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --gpu-command-lane-only)
|
||||||
add_test(NAME wait_until_safe_for_rendering
|
|
||||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --wait-until-safe-for-rendering-only)
|
|
||||||
add_test(NAME pm4_flip
|
add_test(NAME pm4_flip
|
||||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --flip-only)
|
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --flip-only)
|
||||||
add_test(NAME depth_bits
|
add_test(NAME depth_bits
|
||||||
@@ -743,6 +744,7 @@ if(BUILD_TESTING)
|
|||||||
image_page_table_tests
|
image_page_table_tests
|
||||||
memory_tracker_tests
|
memory_tracker_tests
|
||||||
page_manager_tests
|
page_manager_tests
|
||||||
|
build_flags_tests
|
||||||
archive_file_tests
|
archive_file_tests
|
||||||
trophy_system_tests
|
trophy_system_tests
|
||||||
avplayer_file_tests
|
avplayer_file_tests
|
||||||
@@ -811,6 +813,19 @@ add_custom_target(kyty_touch_cursors
|
|||||||
add_dependencies(kyty_emulator kyty_touch_cursors)
|
add_dependencies(kyty_emulator kyty_touch_cursors)
|
||||||
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/assets/cursors" DESTINATION assets)
|
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})
|
target_link_libraries(kyty_emulator ${kyty_emulator_link_libraries})
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
target_link_libraries(kyty_emulator opengl32)
|
target_link_libraries(kyty_emulator opengl32)
|
||||||
|
|||||||
@@ -105,6 +105,10 @@ bool HideCursorEnabled() {
|
|||||||
return g_config->hide_cursor_enabled;
|
return g_config->hide_cursor_enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool StretchEnabled() {
|
||||||
|
return g_config->stretch_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
bool VrEnabled() {
|
bool VrEnabled() {
|
||||||
return g_config->vr_enabled;
|
return g_config->vr_enabled;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ struct ConfigOptions {
|
|||||||
bool fullscreen_enabled = false;
|
bool fullscreen_enabled = false;
|
||||||
bool hdr_enabled = false;
|
bool hdr_enabled = false;
|
||||||
bool hide_cursor_enabled = false;
|
bool hide_cursor_enabled = false;
|
||||||
|
bool stretch_enabled = false;
|
||||||
bool splash_enabled = true;
|
bool splash_enabled = true;
|
||||||
bool vr_enabled = false;
|
bool vr_enabled = false;
|
||||||
bool amd_cpu_enabled = false;
|
bool amd_cpu_enabled = false;
|
||||||
@@ -125,6 +126,7 @@ int32_t GetGpuIndex();
|
|||||||
bool FullscreenEnabled();
|
bool FullscreenEnabled();
|
||||||
bool HdrEnabled();
|
bool HdrEnabled();
|
||||||
bool HideCursorEnabled();
|
bool HideCursorEnabled();
|
||||||
|
bool StretchEnabled();
|
||||||
bool VrEnabled();
|
bool VrEnabled();
|
||||||
bool AmdCpuEnabled();
|
bool AmdCpuEnabled();
|
||||||
bool IsNetworkConnected();
|
bool IsNetworkConnected();
|
||||||
|
|||||||
@@ -99,10 +99,6 @@ static void Init(const Config::ConfigOptions& cfg, const std::filesystem::path&
|
|||||||
|
|
||||||
if (Common::File::IsFileExisting(param_json)) {
|
if (Common::File::IsFileExisting(param_json)) {
|
||||||
Loader::SystemContentLoadParamSfo(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) {
|
if (cfg.splash_enabled) {
|
||||||
Libs::Graphics::InitializeSplashScreen(param_json.parent_path());
|
Libs::Graphics::InitializeSplashScreen(param_json.parent_path());
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ public:
|
|||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
void WaitRegMem(uint32_t func, const T* addr, T ref, T mask, uint32_t poll, uint32_t wait_op);
|
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 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 WriteReferenceClock(uint64_t dst_address, uint32_t num_bytes);
|
||||||
void DmaData(uint8_t engine, uint8_t dst_sel, uint8_t dst_cache_policy,
|
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_DMA_DATA] = CpOpDmaData;
|
||||||
g_cp_op_func[Pm4::IT_ACQUIRE_MEM] = CpOpAcquireMem;
|
g_cp_op_func[Pm4::IT_ACQUIRE_MEM] = CpOpAcquireMem;
|
||||||
g_cp_op_func[Pm4::IT_RELEASE_MEM] = CpOpReleaseMem;
|
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_REWIND] = CpOpRewind;
|
||||||
g_cp_op_func[Pm4::IT_SET_CONTEXT_REG] = CpOpSetContextReg;
|
g_cp_op_func[Pm4::IT_SET_CONTEXT_REG] = CpOpSetContextReg;
|
||||||
g_cp_op_func[Pm4::IT_SET_SH_REG] = CpOpSetShaderReg;
|
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 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 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 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 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 CpOpSetShaderReg(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
|
||||||
uint32_t CpOpSetUconfigReg(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
|
uint32_t CpOpSetUconfigReg(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
|
||||||
|
|||||||
@@ -2060,6 +2060,21 @@ KYTY_CP_OP_PARSER(CpOpReleaseMem) {
|
|||||||
return 7;
|
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_CP_OP_PARSER(CpOpSetContextReg) {
|
||||||
KYTY_PROFILER_FUNCTION();
|
KYTY_PROFILER_FUNCTION();
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <semaphore>
|
#include <semaphore>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace Libs::Graphics {
|
namespace Libs::Graphics {
|
||||||
@@ -92,12 +91,6 @@ void GuestGpu::SendCommand(Common::UniqueFunction<void>&& command) {
|
|||||||
m_work_available.Signal();
|
m_work_available.Signal();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GuestGpu::Wake() {
|
|
||||||
Common::LockGuard lock(m_queue_mutex);
|
|
||||||
m_wake_pending = true;
|
|
||||||
m_work_available.Signal();
|
|
||||||
}
|
|
||||||
|
|
||||||
void GuestGpu::ProcessCommands() {
|
void GuestGpu::ProcessCommands() {
|
||||||
EXIT_IF(!IsGpuThread());
|
EXIT_IF(!IsGpuThread());
|
||||||
while (m_pending_commands.load(std::memory_order_acquire) != 0) {
|
while (m_pending_commands.load(std::memory_order_acquire) != 0) {
|
||||||
@@ -281,6 +274,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,
|
template void CommandProcessor::WaitRegMem<uint64_t>(uint32_t, const uint64_t*, uint64_t, uint64_t,
|
||||||
uint32_t, uint32_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,
|
void CommandProcessor::WriteData(uint32_t* dst, const uint32_t* src, uint32_t dw_num,
|
||||||
uint32_t write_control) {
|
uint32_t write_control) {
|
||||||
const uint32_t dst_sel = ((write_control >> 30u) & 0x1u) | ((write_control >> 7u) & 0x1eu);
|
const uint32_t dst_sel = ((write_control >> 30u) & 0x1u) | ((write_control >> 7u) & 0x1eu);
|
||||||
@@ -433,10 +436,7 @@ void GuestGpu::ThreadRun(void* data) {
|
|||||||
}
|
}
|
||||||
if (selected_queue < 0) {
|
if (selected_queue < 0) {
|
||||||
gpu->m_processing = false;
|
gpu->m_processing = false;
|
||||||
// A wake that came before this wait must not be lost.
|
gpu->m_work_available.WaitFor(&gpu->m_queue_mutex, 100);
|
||||||
if (!std::exchange(gpu->m_wake_pending, false)) {
|
|
||||||
gpu->m_work_available.WaitFor(&gpu->m_queue_mutex, 100);
|
|
||||||
}
|
|
||||||
for (auto& queue: gpu->m_queues) {
|
for (auto& queue: gpu->m_queues) {
|
||||||
if (!queue.empty()) {
|
if (!queue.empty()) {
|
||||||
queue.front().blocked = false;
|
queue.front().blocked = false;
|
||||||
@@ -804,6 +804,27 @@ static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters
|
|||||||
write(registers.draw_index, draw_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,
|
void CommandProcessor::DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers,
|
||||||
uint32_t draw_initiator, bool indexed) {
|
uint32_t draw_initiator, bool indexed) {
|
||||||
const auto args_size =
|
const auto args_size =
|
||||||
@@ -835,13 +856,10 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
|
|||||||
// NUM_INSTANCES keeps the last draw's instance count.
|
// 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 +
|
m_num_instances_addr = args_addr + static_cast<uint64_t>(draw_count - 1) * stride_in_bytes +
|
||||||
offsetof(vk::DrawIndirectCommand, instanceCount);
|
offsetof(vk::DrawIndirectCommand, instanceCount);
|
||||||
// With no SGPRs to patch, the GPU reads the records if the shaders allow. Shaders apply
|
// The GPU reads the records if the shaders allow; the VS prolog removes Vulkan's bases
|
||||||
// the records' offsets only through those SGPRs; Vulkan applies them, which matches
|
// from the index VGPRs and copies them only to the user SGPRs the packet patches.
|
||||||
// while they are zero.
|
DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes};
|
||||||
const DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes};
|
if (IndirectDrawOffsets(registers, records.offsets) && (!indexed || m_index_buffer_size != 0) &&
|
||||||
if (registers.vertex_offset == Pm4::SH_NOP && registers.instance_offset == Pm4::SH_NOP &&
|
|
||||||
registers.index_offset == Pm4::SH_NOP && registers.draw_index == Pm4::SH_NOP &&
|
|
||||||
(!indexed || m_index_buffer_size != 0) &&
|
|
||||||
(indexed ? executor.DrawIndex(m_submit_id, CurrentBuffer(),
|
(indexed ? executor.DrawIndex(m_submit_id, CurrentBuffer(),
|
||||||
{.index_count = m_index_buffer_size,
|
{.index_count = m_index_buffer_size,
|
||||||
.index_addr = IndexAddress(0),
|
.index_addr = IndexAddress(0),
|
||||||
@@ -931,11 +949,10 @@ void CommandProcessor::DrawIndexAuto(DrawAutoArgs args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CommandProcessor::WaitFlipDone(uint32_t video_out_handle, uint32_t display_buffer_index) {
|
void CommandProcessor::WaitFlipDone(uint32_t video_out_handle, uint32_t display_buffer_index) {
|
||||||
// Stall only this command processor, not the host command lane or other queues.
|
BufferFlush();
|
||||||
if (!m_renderer.GetVideoOut().IsSafeForRendering(static_cast<int>(video_out_handle),
|
|
||||||
static_cast<int>(display_buffer_index))) {
|
m_renderer.GetVideoOut().WaitFlipDone(static_cast<int>(video_out_handle),
|
||||||
SuspendPm4();
|
static_cast<int>(display_buffer_index));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommandProcessor::ReleaseMem(uint32_t data_sel, uint32_t interrupt_selector, void* address,
|
void CommandProcessor::ReleaseMem(uint32_t data_sel, uint32_t interrupt_selector, void* address,
|
||||||
@@ -966,16 +983,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.
|
// Dumped occlusion counters are written before any end-of-pipe signal reaches the guest.
|
||||||
m_renderer.GetOcclusionQueries().Publish();
|
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.
|
// EndOfPipeData: kNone, kImmediate32b, kImmediate64b, kRefClk; 5 reads GDS.
|
||||||
switch (data_sel) {
|
switch (data_sel) {
|
||||||
case 0: break;
|
case 0: break;
|
||||||
case 1: {
|
case 1: {
|
||||||
const auto value = static_cast<uint32_t>(data);
|
const auto value = static_cast<uint32_t>(data);
|
||||||
std::memcpy(address, &value, sizeof(value));
|
write_label(&value, sizeof(value));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 3: data = Sync::ReadReferenceClock(); [[fallthrough]];
|
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: {
|
case 5: {
|
||||||
GetScheduler().Finish();
|
GetScheduler().Finish();
|
||||||
const auto gds = static_cast<uint32_t>(data);
|
const auto gds = static_cast<uint32_t>(data);
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ public:
|
|||||||
[[nodiscard]] bool IsStopping();
|
[[nodiscard]] bool IsStopping();
|
||||||
void SendCommand(Common::UniqueFunction<void>&& command);
|
void SendCommand(Common::UniqueFunction<void>&& command);
|
||||||
void SendCommandSync(Common::UniqueFunction<void>&& command);
|
void SendCommandSync(Common::UniqueFunction<void>&& command);
|
||||||
// Retries blocked submissions now instead of after the 100 ms stall timeout.
|
|
||||||
void Wake();
|
|
||||||
|
|
||||||
// Submitted command memory is borrowed and must remain valid until GPU execution completes.
|
// Submitted command memory is borrowed and must remain valid until GPU execution completes.
|
||||||
void Submit(std::span<const uint32_t> commands);
|
void Submit(std::span<const uint32_t> commands);
|
||||||
@@ -84,7 +82,6 @@ private:
|
|||||||
bool m_processing = false;
|
bool m_processing = false;
|
||||||
bool m_accepting = true;
|
bool m_accepting = true;
|
||||||
bool m_stopping = false;
|
bool m_stopping = false;
|
||||||
bool m_wake_pending = false;
|
|
||||||
bool m_shutdown_complete = false;
|
bool m_shutdown_complete = false;
|
||||||
// Completion callbacks can outlive GuestGpu during renderer shutdown.
|
// Completion callbacks can outlive GuestGpu during renderer shutdown.
|
||||||
std::shared_ptr<std::binary_semaphore> m_suspend_point_ready =
|
std::shared_ptr<std::binary_semaphore> m_suspend_point_ready =
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <iterator>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
namespace Libs::Graphics {
|
namespace Libs::Graphics {
|
||||||
@@ -17,14 +18,19 @@ public:
|
|||||||
if (it != m_ranges.begin() && std::prev(it)->second >= address) {
|
if (it != m_ranges.begin() && std::prev(it)->second >= address) {
|
||||||
it = std::prev(it);
|
it = std::prev(it);
|
||||||
}
|
}
|
||||||
uint64_t begin = address;
|
if (it == m_ranges.end() || it->first > address) {
|
||||||
uint64_t last = end;
|
it = m_ranges.emplace_hint(it, address, end);
|
||||||
while (it != m_ranges.end() && it->first <= last) {
|
} else {
|
||||||
begin = std::min(begin, it->first);
|
if (it->second >= end) {
|
||||||
last = std::max(last, it->second);
|
return;
|
||||||
it = m_ranges.erase(it);
|
}
|
||||||
|
it->second = end;
|
||||||
|
}
|
||||||
|
auto next = std::next(it);
|
||||||
|
while (next != m_ranges.end() && next->first <= it->second) {
|
||||||
|
it->second = std::max(it->second, next->second);
|
||||||
|
next = m_ranges.erase(next);
|
||||||
}
|
}
|
||||||
m_ranges.emplace(begin, last);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Subtract(uint64_t address, uint64_t size) {
|
void Subtract(uint64_t address, uint64_t size) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <iterator>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -118,6 +119,13 @@ public:
|
|||||||
|
|
||||||
class const_iterator final {
|
class const_iterator final {
|
||||||
public:
|
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); }
|
[[nodiscard]] OwnerT operator*() const noexcept { return m_owners->At(m_index); }
|
||||||
const_iterator& operator++() noexcept {
|
const_iterator& operator++() noexcept {
|
||||||
++m_index;
|
++m_index;
|
||||||
|
|||||||
+30
-30
@@ -488,16 +488,15 @@ void TextureCache::RefreshCopySource(ImageId id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool TextureCache::CopyD16(Image& destination, Image& source) {
|
bool TextureCache::CopyD16(Image& destination, Image& source) {
|
||||||
const bool source_depth = source.info.IsDepth();
|
// Converts guest D16 between a four-byte host depth aspect and two-byte color or D16 depth.
|
||||||
const bool destination_depth = destination.info.IsDepth();
|
const bool source_wide = DepthAspectTransferBytes(source.backing.format) == sizeof(uint32_t);
|
||||||
if (source_depth == destination_depth) {
|
if (source_wide == (DepthAspectTransferBytes(destination.backing.format) == sizeof(uint32_t))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto& depth = source_depth ? source : destination;
|
auto& depth = source_wide ? source : destination;
|
||||||
auto& color = source_depth ? destination : source;
|
auto& narrow = source_wide ? destination : source;
|
||||||
const auto transfer_bytes = DepthAspectTransferBytes(depth.backing.format);
|
|
||||||
if (depth.info.bytes_per_block != sizeof(uint16_t) ||
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
EXIT_IF(source.backing.samples != 1 || destination.backing.samples != 1 ||
|
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 auto layers = depth.info.resources.layers;
|
||||||
const uint64_t depth_slice =
|
const uint64_t depth_slice =
|
||||||
static_cast<uint64_t>(depth.info.pitch) * depth.info.extent.height * transfer_bytes;
|
static_cast<uint64_t>(depth.info.pitch) * depth.info.extent.height * sizeof(uint32_t);
|
||||||
const uint64_t color_slice =
|
const uint64_t narrow_slice =
|
||||||
static_cast<uint64_t>(color.info.pitch) * color.info.extent.height * sizeof(uint16_t);
|
static_cast<uint64_t>(narrow.info.pitch) * narrow.info.extent.height * sizeof(uint16_t);
|
||||||
EXIT_IF(layers == 0 || depth_slice > UINT64_MAX / layers || color_slice > UINT64_MAX / layers);
|
EXIT_IF(layers == 0 || depth_slice > UINT64_MAX / layers || narrow_slice > UINT64_MAX / layers);
|
||||||
const auto depth_size = depth_slice * layers;
|
const std::array depth_copies {
|
||||||
const auto color_size = color_slice * layers;
|
|
||||||
const std::array depth_copies {
|
|
||||||
vk::BufferImageCopy {0,
|
vk::BufferImageCopy {0,
|
||||||
depth.info.pitch,
|
depth.info.pitch,
|
||||||
depth.info.extent.height,
|
depth.info.extent.height,
|
||||||
{vk::ImageAspectFlagBits::eDepth, 0, 0, layers},
|
{vk::ImageAspectFlagBits::eDepth, 0, 0, layers},
|
||||||
{},
|
{},
|
||||||
depth.info.extent}};
|
depth.info.extent}};
|
||||||
const std::array color_copies {
|
const std::array narrow_copies {
|
||||||
vk::BufferImageCopy {0,
|
vk::BufferImageCopy {0,
|
||||||
color.info.pitch,
|
narrow.info.pitch,
|
||||||
color.info.extent.height,
|
narrow.info.extent.height,
|
||||||
{vk::ImageAspectFlagBits::eColor, 0, 0, layers},
|
{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 depth_buffer = m_tiler.GetScratchBuffer(depth_slice * layers);
|
||||||
auto color_buffer = m_tiler.GetScratchBuffer(color_size, depth_buffer.buffer);
|
auto narrow_buffer = m_tiler.GetScratchBuffer(narrow_slice * layers, depth_buffer.buffer);
|
||||||
const TileManager::D16Layout promote_layout {
|
const TileManager::D16Layout promote_layout {
|
||||||
.width = depth.info.extent.width,
|
.width = depth.info.extent.width,
|
||||||
.height = depth.info.extent.height,
|
.height = depth.info.extent.height,
|
||||||
.layers = layers,
|
.layers = layers,
|
||||||
.source_row_stride = static_cast<uint64_t>(color.info.pitch) * sizeof(uint16_t),
|
.source_row_stride = static_cast<uint64_t>(narrow.info.pitch) * sizeof(uint16_t),
|
||||||
.target_row_stride = static_cast<uint64_t>(depth.info.pitch) * transfer_bytes,
|
.target_row_stride = static_cast<uint64_t>(depth.info.pitch) * sizeof(uint32_t),
|
||||||
.source_slice_stride = color_slice,
|
.source_slice_stride = narrow_slice,
|
||||||
.target_slice_stride = depth_slice,
|
.target_slice_stride = depth_slice,
|
||||||
};
|
};
|
||||||
const bool d32 = DepthAspectTransferFormat(depth.backing.format) == vk::Format::eD32Sfloat;
|
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);
|
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,
|
{.width = promote_layout.width,
|
||||||
.height = promote_layout.height,
|
.height = promote_layout.height,
|
||||||
.layers = promote_layout.layers,
|
.layers = promote_layout.layers,
|
||||||
@@ -550,11 +549,12 @@ bool TextureCache::CopyD16(Image& destination, Image& source) {
|
|||||||
.target_row_stride = promote_layout.source_row_stride,
|
.target_row_stride = promote_layout.source_row_stride,
|
||||||
.source_slice_stride = promote_layout.target_slice_stride,
|
.source_slice_stride = promote_layout.target_slice_stride,
|
||||||
.target_slice_stride = promote_layout.source_slice_stride});
|
.target_slice_stride = promote_layout.source_slice_stride});
|
||||||
destination.Upload(color_copies, color_buffer.buffer, color_buffer.offset,
|
destination.Upload(narrow_copies, narrow_buffer.buffer, narrow_buffer.offset,
|
||||||
color_buffer.size);
|
narrow_buffer.size);
|
||||||
} else {
|
} else {
|
||||||
source.Download(color_copies, color_buffer.buffer, color_buffer.offset, color_buffer.size);
|
source.Download(narrow_copies, narrow_buffer.buffer, narrow_buffer.offset,
|
||||||
m_tiler.ConvertD16(color_buffer, depth_buffer, TileManager::D16Direction::Promote, d32,
|
narrow_buffer.size);
|
||||||
|
m_tiler.ConvertD16(narrow_buffer, depth_buffer, TileManager::D16Direction::Promote, d32,
|
||||||
promote_layout);
|
promote_layout);
|
||||||
destination.Upload(depth_copies, depth_buffer.buffer, depth_buffer.offset,
|
destination.Upload(depth_copies, depth_buffer.buffer, depth_buffer.offset,
|
||||||
depth_buffer.size);
|
depth_buffer.size);
|
||||||
|
|||||||
@@ -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) {
|
if (dirty_state.blend_constants) {
|
||||||
dirty_state.blend_constants = false;
|
dirty_state.blend_constants = false;
|
||||||
cmdbuf.setBlendConstants(blend_constants.data());
|
cmdbuf.setBlendConstants(blend_constants.data());
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ struct DynamicState {
|
|||||||
bool stencil_back_write_mask : 1;
|
bool stencil_back_write_mask : 1;
|
||||||
bool stencil_back_compare_mask : 1;
|
bool stencil_back_compare_mask : 1;
|
||||||
|
|
||||||
|
bool cull_mode : 1;
|
||||||
|
bool front_face : 1;
|
||||||
|
|
||||||
bool blend_constants : 1;
|
bool blend_constants : 1;
|
||||||
bool color_write_enables : 1;
|
bool color_write_enables : 1;
|
||||||
bool line_width : 1;
|
bool line_width : 1;
|
||||||
@@ -86,6 +89,9 @@ struct DynamicState {
|
|||||||
uint32_t stencil_back_write_mask {};
|
uint32_t stencil_back_write_mask {};
|
||||||
uint32_t stencil_back_compare_mask {};
|
uint32_t stencil_back_compare_mask {};
|
||||||
|
|
||||||
|
vk::CullModeFlags cull_mode {};
|
||||||
|
vk::FrontFace front_face {};
|
||||||
|
|
||||||
std::array<float, 4> blend_constants {};
|
std::array<float, 4> blend_constants {};
|
||||||
std::array<vk::Bool32, RENDER_COLOR_ATTACHMENTS_MAX> color_write_enables {};
|
std::array<vk::Bool32, RENDER_COLOR_ATTACHMENTS_MAX> color_write_enables {};
|
||||||
uint32_t num_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_) {
|
void SetBlendConstants(const std::array<float, 4> blend_constants_) {
|
||||||
if (blend_constants != blend_constants_) {
|
if (blend_constants != blend_constants_) {
|
||||||
blend_constants = blend_constants_;
|
blend_constants = blend_constants_;
|
||||||
|
|||||||
@@ -19,7 +19,13 @@ namespace Libs::Graphics {
|
|||||||
|
|
||||||
enum class ColorTransform : uint32_t { None, SwapBgra16, Reverse10_11_11 };
|
enum class ColorTransform : uint32_t { None, SwapBgra16, Reverse10_11_11 };
|
||||||
|
|
||||||
enum class VideoOutCompression : uint8_t { Uncompressed, Dcc256_256_0, Dcc256_64_64, Unsupported };
|
enum class VideoOutCompression : uint8_t {
|
||||||
|
Uncompressed,
|
||||||
|
Dcc256_256_0,
|
||||||
|
Dcc256_64_64,
|
||||||
|
Dcc128_128_128,
|
||||||
|
Unsupported
|
||||||
|
};
|
||||||
|
|
||||||
enum class ImageMetadataKind : uint8_t { None, Htile, Dcc, Cmask };
|
enum class ImageMetadataKind : uint8_t { None, Htile, Dcc, Cmask };
|
||||||
|
|
||||||
@@ -287,8 +293,9 @@ inline bool ImageInfo::IsDepth() const noexcept {
|
|||||||
[[nodiscard]] inline VideoOutCompression
|
[[nodiscard]] inline VideoOutCompression
|
||||||
ClassifyVideoOutCompression(bool compressed, uint64_t metadata_address, uint32_t dcc_control,
|
ClassifyVideoOutCompression(bool compressed, uint64_t metadata_address, uint32_t dcc_control,
|
||||||
uint64_t dcc_clear_color) noexcept {
|
uint64_t dcc_clear_color) noexcept {
|
||||||
constexpr uint32_t DCC_256_256_0 = 0x00000048u;
|
constexpr uint32_t DCC_256_256_0 = 0x00000048u;
|
||||||
constexpr uint32_t DCC_256_64_64 = 0x00000208u;
|
constexpr uint32_t DCC_256_64_64 = 0x00000208u;
|
||||||
|
constexpr uint32_t DCC_128_128_128 = 0x00100024u;
|
||||||
if (!compressed) {
|
if (!compressed) {
|
||||||
return metadata_address == 0 && dcc_control == 0 && dcc_clear_color == 0
|
return metadata_address == 0 && dcc_control == 0 && dcc_clear_color == 0
|
||||||
? VideoOutCompression::Uncompressed
|
? VideoOutCompression::Uncompressed
|
||||||
@@ -300,6 +307,7 @@ ClassifyVideoOutCompression(bool compressed, uint64_t metadata_address, uint32_t
|
|||||||
switch (dcc_control) {
|
switch (dcc_control) {
|
||||||
case DCC_256_256_0: return VideoOutCompression::Dcc256_256_0;
|
case DCC_256_256_0: return VideoOutCompression::Dcc256_256_0;
|
||||||
case DCC_256_64_64: return VideoOutCompression::Dcc256_64_64;
|
case DCC_256_64_64: return VideoOutCompression::Dcc256_64_64;
|
||||||
|
case DCC_128_128_128: return VideoOutCompression::Dcc128_128_128;
|
||||||
default: return VideoOutCompression::Unsupported;
|
default: return VideoOutCompression::Unsupported;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -484,7 +484,7 @@ void TileManager::ConvertD16(Result source, Result target, D16Direction directio
|
|||||||
vk::AccessFlagBits::eHostWrite |
|
vk::AccessFlagBits::eHostWrite |
|
||||||
vk::AccessFlagBits::eTransferWrite |
|
vk::AccessFlagBits::eTransferWrite |
|
||||||
vk::AccessFlagBits::eShaderWrite;
|
vk::AccessFlagBits::eShaderWrite;
|
||||||
barriers[1].dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite;
|
barriers[1].dstAccessMask = vk::AccessFlagBits::eShaderWrite;
|
||||||
command.pipelineBarrier(
|
command.pipelineBarrier(
|
||||||
vk::PipelineStageFlagBits::eAllCommands | vk::PipelineStageFlagBits::eHost,
|
vk::PipelineStageFlagBits::eAllCommands | vk::PipelineStageFlagBits::eHost,
|
||||||
vk::PipelineStageFlagBits::eComputeShader, {}, 0, nullptr, 2, barriers, 0, nullptr);
|
vk::PipelineStageFlagBits::eComputeShader, {}, 0, nullptr, 2, barriers, 0, nullptr);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace {
|
|||||||
|
|
||||||
namespace IR = ShaderRecompiler::IR;
|
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 uint64_t MinTableBytes = 64ull * 1024ull;
|
||||||
constexpr uint32_t ElementMask = (1u << IR::ImageHeapKindShift) - 1u;
|
constexpr uint32_t ElementMask = (1u << IR::ImageHeapKindShift) - 1u;
|
||||||
|
|
||||||
@@ -133,12 +133,16 @@ ImageHeap::~ImageHeap() {
|
|||||||
bool ImageHeap::Acquire(const IR::ImageHeapRequest& request, IR::ImageHeapView& result) {
|
bool ImageHeap::Acquire(const IR::ImageHeapRequest& request, IR::ImageHeapView& result) {
|
||||||
KYTY_PROFILER_FUNCTION();
|
KYTY_PROFILER_FUNCTION();
|
||||||
const auto size = request.size & ~uint64_t {3};
|
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) {
|
((request.base | request.first | request.step) & 3u) != 0u) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto memory = std::ranges::find_if(
|
auto memory = std::ranges::find_if(
|
||||||
m_memories, [&](const auto& candidate) { return candidate->base == request.base; });
|
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.
|
// 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 (memory == m_memories.end() || ((*memory)->size != size && (*memory)->draw != m_draw)) {
|
||||||
if (!m_context.IsMapped(request.base, size)) return false;
|
if (!m_context.IsMapped(request.base, size)) return false;
|
||||||
@@ -593,26 +597,42 @@ void ImageHeap::UnmapMemory(uint64_t address, uint64_t size) {
|
|||||||
} else {
|
} else {
|
||||||
m_unmapped.push_back({address, size});
|
m_unmapped.push_back({address, size});
|
||||||
}
|
}
|
||||||
auto& scheduler = m_context.GetCommandScheduler();
|
|
||||||
std::scoped_lock lock {m_mutex};
|
std::scoped_lock lock {m_mutex};
|
||||||
std::erase_if(m_memories, [&](const std::unique_ptr<Memory>& memory) {
|
std::erase_if(m_memories, [&](const std::unique_ptr<Memory>& memory) {
|
||||||
if (address >= memory->base + memory->size || memory->base >= address + size) {
|
if (address >= memory->base + memory->size || memory->base >= address + size) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Unwatch(*memory);
|
Drop(*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)] {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
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
|
} // namespace Libs::Graphics
|
||||||
@@ -26,7 +26,7 @@ struct ImageHeapTestAccess;
|
|||||||
// of a table owns device slot words, one per table dword, that select an array element. CPU
|
// 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
|
// 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
|
// changes when a draw first acquires it. Changes to the images behind elements bump
|
||||||
// Image::heap_epoch.
|
// 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 {
|
class ImageHeap final: public ShaderRecompiler::IR::ImageHeapSource {
|
||||||
public:
|
public:
|
||||||
ImageHeap(RenderContext& context, PageManager& page_manager);
|
ImageHeap(RenderContext& context, PageManager& page_manager);
|
||||||
@@ -47,6 +47,8 @@ public:
|
|||||||
// CPU write fault, on any thread.
|
// CPU write fault, on any thread.
|
||||||
void InvalidateMemory(uint64_t address, uint64_t size);
|
void InvalidateMemory(uint64_t address, uint64_t size);
|
||||||
void UnmapMemory(uint64_t address, uint64_t size);
|
void UnmapMemory(uint64_t address, uint64_t size);
|
||||||
|
// At a suspend point.
|
||||||
|
void RunGarbageCollector();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
using DescriptorValue = ShaderRecompiler::IR::DescriptorValue;
|
using DescriptorValue = ShaderRecompiler::IR::DescriptorValue;
|
||||||
@@ -107,6 +109,7 @@ private:
|
|||||||
template <bool track>
|
template <bool track>
|
||||||
void Watch(const Memory& memory, size_t begin, size_t end);
|
void Watch(const Memory& memory, size_t begin, size_t end);
|
||||||
void Unwatch(const Memory& memory);
|
void Unwatch(const Memory& memory);
|
||||||
|
void Drop(Memory& memory);
|
||||||
void UpdateProbes(View& view, uint64_t begin, uint64_t end);
|
void UpdateProbes(View& view, uint64_t begin, uint64_t end);
|
||||||
void UpdateProbe(View& view, uint64_t probe);
|
void UpdateProbe(View& view, uint64_t probe);
|
||||||
void Assign(View& view, size_t slot, uint32_t entry);
|
void Assign(View& view, size_t slot, uint32_t entry);
|
||||||
@@ -125,9 +128,11 @@ private:
|
|||||||
std::mutex m_mutex; // Fault-visible Memory state and m_memories.
|
std::mutex m_mutex; // Fault-visible Memory state and m_memories.
|
||||||
std::vector<std::unique_ptr<Memory>> m_memories;
|
std::vector<std::unique_ptr<Memory>> m_memories;
|
||||||
std::vector<View*> m_active; // Acquired since the last commit.
|
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<Element> m_elements;
|
||||||
std::vector<uint32_t> m_free;
|
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::deque<std::pair<uint64_t, uint32_t>> m_retired; // Tick, element.
|
||||||
std::vector<std::pair<size_t, size_t>> m_runs;
|
std::vector<std::pair<size_t, size_t>> m_runs;
|
||||||
std::vector<uint32_t> m_scratch;
|
std::vector<uint32_t> m_scratch;
|
||||||
|
|||||||
@@ -687,6 +687,10 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
|
|||||||
vertex_params[0] = PrepareProgram(vertex_regs, context, user_config, vertex_info[0]);
|
vertex_params[0] = PrepareProgram(vertex_regs, context, user_config, vertex_info[0]);
|
||||||
}
|
}
|
||||||
const bool mesh_active = vertex_info[0].logical_stage == ShaderType::Mesh;
|
const bool mesh_active = vertex_info[0].logical_stage == ShaderType::Mesh;
|
||||||
|
if (!tess_active && !mesh_active) {
|
||||||
|
vertex_info[0].indirect_offsets = state.indirect_offsets;
|
||||||
|
vertex_info[0].indirect_draw = state.indirect_draw;
|
||||||
|
}
|
||||||
if (object_primitive_id) {
|
if (object_primitive_id) {
|
||||||
// CxObjectIdMode: register source plus a 32-bit pipelined primitive-ID addition.
|
// CxObjectIdMode: register source plus a 32-bit pipelined primitive-ID addition.
|
||||||
EXIT_NOT_IMPLEMENTED(sh.m_paClObjprimIdCntl != 2u || mesh_active || tess_active ||
|
EXIT_NOT_IMPLEMENTED(sh.m_paClObjprimIdCntl != 2u || mesh_active || tess_active ||
|
||||||
@@ -1003,15 +1007,11 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(const CommandBuffer&
|
|||||||
}
|
}
|
||||||
static_params.alpha_to_coverage_enable = ps_active && ctx.GetShaderRegisters().AlphaToMask();
|
static_params.alpha_to_coverage_enable = ps_active && ctx.GetShaderRegisters().AlphaToMask();
|
||||||
const bool rect_list = Prospero::IsRectList(command.GetUserConfig().GetPrimType());
|
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.provoking_vtx_last = mc.provoking_vtx_last;
|
||||||
static_params.polygon_mode =
|
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) {
|
if (tessellation) {
|
||||||
static_params.patch_control_points =
|
static_params.patch_control_points = vs_input_info.tess.input_control_points;
|
||||||
static_cast<uint8_t>(vs_input_info.tess.input_control_points);
|
|
||||||
} else if (!mesh && static_params.topology == vk::PrimitiveTopology::ePatchList) {
|
} else if (!mesh && static_params.topology == vk::PrimitiveTopology::ePatchList) {
|
||||||
// Rectangle lists expand through generated tessellation stages.
|
// Rectangle lists expand through generated tessellation stages.
|
||||||
static_params.patch_control_points = 3;
|
static_params.patch_control_points = 3;
|
||||||
|
|||||||
@@ -54,15 +54,12 @@ struct PipelineStaticParameters {
|
|||||||
bool depth_clip_enable = true;
|
bool depth_clip_enable = true;
|
||||||
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
|
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
|
||||||
// 0 without tessellation, 3 for a rectangle list, else the input control points.
|
// 0 without tessellation, 3 for a rectangle list, else the input control points.
|
||||||
uint8_t patch_control_points = 0;
|
uint32_t patch_control_points = 0;
|
||||||
bool primitive_restart_enable = false;
|
bool primitive_restart_enable = false;
|
||||||
uint32_t samples = 1;
|
uint32_t samples = 1;
|
||||||
bool sample_shading_enable = false;
|
bool sample_shading_enable = false;
|
||||||
bool alpha_to_coverage_enable = false;
|
bool alpha_to_coverage_enable = false;
|
||||||
uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||||
bool cull_front = false;
|
|
||||||
bool cull_back = false;
|
|
||||||
bool face = false;
|
|
||||||
bool provoking_vtx_last = false;
|
bool provoking_vtx_last = false;
|
||||||
vk::PolygonMode polygon_mode = vk::PolygonMode::eFill;
|
vk::PolygonMode polygon_mode = vk::PolygonMode::eFill;
|
||||||
uint8_t color_srcblend[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
uint8_t color_srcblend[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||||
@@ -140,6 +137,9 @@ struct DrawRenderState {
|
|||||||
bool ps_active = false;
|
bool ps_active = false;
|
||||||
std::array<ShaderVertexInputInfo, 3> vertex_info;
|
std::array<ShaderVertexInputInfo, 3> vertex_info;
|
||||||
ShaderPixelInputInfo ps_input_info;
|
ShaderPixelInputInfo ps_input_info;
|
||||||
|
// Input: the user-data slots a GPU-read indirect draw patches.
|
||||||
|
ShaderIndirectOffsets indirect_offsets;
|
||||||
|
bool indirect_draw = false;
|
||||||
|
|
||||||
[[nodiscard]] std::span<const ShaderVertexInputInfo> VertexStages() const {
|
[[nodiscard]] std::span<const ShaderVertexInputInfo> VertexStages() const {
|
||||||
return {vertex_info.data(), vertex_info[1].stage ? 3u : 1u};
|
return {vertex_info.data(), vertex_info[1].stage ? 3u : 1u};
|
||||||
|
|||||||
@@ -219,17 +219,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
|||||||
vk::PipelineViewportStateCreateInfo viewport_state {};
|
vk::PipelineViewportStateCreateInfo viewport_state {};
|
||||||
viewport_state.pNext = &depth_clip_control;
|
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 {};
|
vk::PipelineRasterizationDepthClipStateCreateInfoEXT clip_ext {};
|
||||||
clip_ext.depthClipEnable = static_params.depth_clip_enable ? VK_TRUE : VK_FALSE;
|
clip_ext.depthClipEnable = static_params.depth_clip_enable ? VK_TRUE : VK_FALSE;
|
||||||
|
|
||||||
@@ -250,8 +239,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
|||||||
provoking_vertex.pNext = rasterizer.pNext;
|
provoking_vertex.pNext = rasterizer.pNext;
|
||||||
rasterizer.pNext = &provoking_vertex;
|
rasterizer.pNext = &provoking_vertex;
|
||||||
}
|
}
|
||||||
rasterizer.cullMode = cull_mode;
|
|
||||||
rasterizer.frontFace = front_face;
|
|
||||||
rasterizer.polygonMode = static_params.polygon_mode;
|
rasterizer.polygonMode = static_params.polygon_mode;
|
||||||
rasterizer.lineWidth = 1.0f;
|
rasterizer.lineWidth = 1.0f;
|
||||||
|
|
||||||
@@ -347,6 +334,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
|||||||
vk::DynamicState::eViewportWithCount,
|
vk::DynamicState::eViewportWithCount,
|
||||||
vk::DynamicState::eScissorWithCount,
|
vk::DynamicState::eScissorWithCount,
|
||||||
vk::DynamicState::eLineWidth,
|
vk::DynamicState::eLineWidth,
|
||||||
|
vk::DynamicState::eCullMode,
|
||||||
|
vk::DynamicState::eFrontFace,
|
||||||
vk::DynamicState::eDepthTestEnable,
|
vk::DynamicState::eDepthTestEnable,
|
||||||
vk::DynamicState::eDepthWriteEnable,
|
vk::DynamicState::eDepthWriteEnable,
|
||||||
vk::DynamicState::eDepthCompareOp,
|
vk::DynamicState::eDepthCompareOp,
|
||||||
|
|||||||
@@ -45,9 +45,10 @@ enum class DrawOffsetSource : uint8_t {
|
|||||||
|
|
||||||
// Nonzero address: the GPU reads count Vulkan indirect draw records, stride bytes apart.
|
// Nonzero address: the GPU reads count Vulkan indirect draw records, stride bytes apart.
|
||||||
struct DrawIndirectArgs {
|
struct DrawIndirectArgs {
|
||||||
uint64_t address = 0;
|
uint64_t address = 0;
|
||||||
uint32_t count = 0;
|
uint32_t count = 0;
|
||||||
uint32_t stride = 0;
|
uint32_t stride = 0;
|
||||||
|
ShaderIndirectOffsets offsets;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DrawIndexArgs {
|
struct DrawIndexArgs {
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ void RenderContext::RunGarbageCollector() {
|
|||||||
m_fault_process_pending = false;
|
m_fault_process_pending = false;
|
||||||
m_buffer_cache.ProcessFaultBuffer();
|
m_buffer_cache.ProcessFaultBuffer();
|
||||||
}
|
}
|
||||||
|
m_image_heap.RunGarbageCollector();
|
||||||
m_texture_cache.ProcessDownloadImages();
|
m_texture_cache.ProcessDownloadImages();
|
||||||
m_texture_cache.RunGarbageCollector();
|
m_texture_cache.RunGarbageCollector();
|
||||||
m_buffer_cache.RunGarbageCollector();
|
m_buffer_cache.RunGarbageCollector();
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ std::pair<int32_t, uint32_t> ResolveDrawOffsets(uint32_t index_offset,
|
|||||||
const ShaderVertexInputInfo& vs_input_info) {
|
const ShaderVertexInputInfo& vs_input_info) {
|
||||||
auto vertex_offset = static_cast<int32_t>(index_offset);
|
auto vertex_offset = static_cast<int32_t>(index_offset);
|
||||||
uint32_t instance_offset = 0;
|
uint32_t instance_offset = 0;
|
||||||
if (!vs_input_info.fetch_embedded) {
|
if (!vs_input_info.has_dynamic_vertex_fetch) {
|
||||||
return {vertex_offset, instance_offset};
|
return {vertex_offset, instance_offset};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,6 +403,20 @@ static void UpdateGraphicsDynamicState(const CommandBuffer& buffer, DynamicState
|
|||||||
dynamic_state.SetDepthBias(constant_factor, poly_offset.clamp, slope_factor);
|
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);
|
dynamic_state.SetStencilTestEnabled(depth.stencil_test_enable);
|
||||||
if (depth.stencil_test_enable) {
|
if (depth.stencil_test_enable) {
|
||||||
const auto& front = depth.stencil_front;
|
const auto& front = depth.stencil_front;
|
||||||
@@ -1236,24 +1250,36 @@ 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
|
// Vulkan offsets for a prepared draw, or nullopt when its indirect records need the CPU: mesh
|
||||||
// mesh and quad-list draws do. Indirect records carry their own offsets.
|
// and quad-list draws for counts, tessellation draws for the index/SGPR prolog. Indirect records
|
||||||
|
// carry their own offsets.
|
||||||
static std::optional<DrawEmitInfo> ResolveDrawEmit(const HW::UserConfig& ucfg,
|
static std::optional<DrawEmitInfo> ResolveDrawEmit(const HW::UserConfig& ucfg,
|
||||||
const ShaderVertexInputInfo& vs,
|
const ShaderVertexInputInfo& vs,
|
||||||
DrawOffsetSource source,
|
DrawOffsetSource source,
|
||||||
int32_t first_vertex,
|
int32_t first_vertex,
|
||||||
uint32_t first_instance,
|
uint32_t first_instance,
|
||||||
const DrawIndirectArgs& indirect) {
|
const DrawIndirectArgs& indirect) {
|
||||||
|
if ((indirect.address != 0 || source == DrawOffsetSource::IndirectArgs) &&
|
||||||
|
ucfg.GetIndexOffset() != 0) {
|
||||||
|
static std::once_flag warning_once;
|
||||||
|
std::call_once(warning_once, [&] {
|
||||||
|
Log::WriteToConsoleAndLog(fmt::format(
|
||||||
|
"GPU: indirect draw ignores GE_INDX_OFFSET=0x{:08x}; geometry may be incorrect.\n",
|
||||||
|
ucfg.GetIndexOffset()));
|
||||||
|
});
|
||||||
|
}
|
||||||
if (indirect.address != 0) {
|
if (indirect.address != 0) {
|
||||||
if (vs.stage.module->info.stage == ShaderType::Mesh ||
|
if (!vs.indirect_draw ||
|
||||||
ucfg.GetPrimType() == Prospero::PrimitiveType::kQuadListLegacy) {
|
vs.stage.module->info.stage == ShaderType::Mesh ||
|
||||||
|
ucfg.GetPrimType() == Prospero::PrimitiveType::kQuadListLegacy ||
|
||||||
|
vs.indirect_offsets != indirect.offsets) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
return DrawEmitInfo {.indirect = indirect};
|
return DrawEmitInfo {.indirect = indirect};
|
||||||
}
|
}
|
||||||
if (source == DrawOffsetSource::IndirectArgs) {
|
if (source == DrawOffsetSource::IndirectArgs) {
|
||||||
// Native fetches consume the patched SGPRs; rewritten fetches need Vulkan offsets.
|
// Native fetches consume the patched SGPRs; rewritten fetches need Vulkan offsets.
|
||||||
return vs.fetch_embedded ? DrawEmitInfo {first_vertex, first_instance} : DrawEmitInfo {};
|
return vs.has_dynamic_vertex_fetch ? DrawEmitInfo {first_vertex, first_instance} : DrawEmitInfo {};
|
||||||
}
|
}
|
||||||
const auto [vertex_offset, instance_offset] = ResolveDrawOffsets(ucfg.GetIndexOffset(), vs);
|
const auto [vertex_offset, instance_offset] = ResolveDrawOffsets(ucfg.GetIndexOffset(), vs);
|
||||||
return DrawEmitInfo {vertex_offset + first_vertex, instance_offset};
|
return DrawEmitInfo {vertex_offset + first_vertex, instance_offset};
|
||||||
@@ -1310,7 +1336,8 @@ bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
|
|||||||
|
|
||||||
hw_check(buffer);
|
hw_check(buffer);
|
||||||
|
|
||||||
DrawRenderState state {};
|
DrawRenderState state {.indirect_offsets = args.indirect.offsets,
|
||||||
|
.indirect_draw = args.indirect.address != 0};
|
||||||
if (!GetDrawTopology(ucfg, state.topology)) {
|
if (!GetDrawTopology(ucfg, state.topology)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1415,7 +1442,8 @@ bool RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
|
|||||||
const DrawCallInfo draw {CommandBufferDebugOp::DrawIndexAuto,
|
const DrawCallInfo draw {CommandBufferDebugOp::DrawIndexAuto,
|
||||||
args.vertex_count, args.instance_count, args.first_instance};
|
args.vertex_count, args.instance_count, args.first_instance};
|
||||||
|
|
||||||
DrawRenderState state {};
|
DrawRenderState state {.indirect_offsets = args.indirect.offsets,
|
||||||
|
.indirect_draw = args.indirect.address != 0};
|
||||||
if (!GetDrawTopology(ucfg, state.topology)) {
|
if (!GetDrawTopology(ucfg, state.topology)) {
|
||||||
ResetBindings();
|
ResetBindings();
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
#version 450
|
#version 450
|
||||||
|
#extension GL_EXT_shader_16bit_storage : require
|
||||||
|
|
||||||
layout(constant_id = 0) const uint D32 = 0u;
|
layout(constant_id = 0) const uint D32 = 0u;
|
||||||
layout(local_size_x = 64) in;
|
layout(local_size_x = 64) in;
|
||||||
|
|
||||||
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } input_buffer;
|
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 {
|
layout(push_constant, std430) uniform Conversion {
|
||||||
uint src_base;
|
uint src_base;
|
||||||
@@ -32,9 +33,5 @@ void main() {
|
|||||||
}
|
}
|
||||||
uint src = params.src_base + y * params.source_pitch + x * 4u;
|
uint src = params.src_base + y * params.source_pitch + x * 4u;
|
||||||
uint dst = params.dst_base + y * params.target_pitch + x * 2u;
|
uint dst = params.dst_base + y * params.target_pitch + x * 2u;
|
||||||
uint shift = (dst & 2u) * 8u;
|
output_buffer.data[dst >> 1u] = uint16_t(decode(input_buffer.data[src >> 2u]));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
@@ -533,16 +533,16 @@ void DrawMemoryAndIo(const Snapshot& shown) {
|
|||||||
Bytes(guest.pooled_allocated) + " (" + Bytes(guest.pool_committed) + " committed)");
|
Bytes(guest.pooled_allocated) + " (" + Bytes(guest.pool_committed) + " committed)");
|
||||||
Row("Flexible", Bytes(guest.flexible_used) + " / " + Bytes(guest.flexible_total));
|
Row("Flexible", Bytes(guest.flexible_used) + " / " + Bytes(guest.flexible_total));
|
||||||
Row("Extended", Bytes(guest.automatic_allocated));
|
Row("Extended", Bytes(guest.automatic_allocated));
|
||||||
// Past the pool size the guest sees no page-table entries available.
|
// Past its total a side sees no page-table entries available.
|
||||||
const auto entries = std::max(guest.cpu_page_entries, guest.gpu_page_entries);
|
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)",
|
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),
|
||||||
|
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.gpu_page_entries),
|
||||||
static_cast<unsigned long long>(guest.page_entries_total)),
|
static_cast<unsigned long long>(guest.gpu_page_entries_total)),
|
||||||
entries > guest.page_entries_total ? BAD
|
LevelColor(static_cast<double>(percent), 90, 100));
|
||||||
: entries * 10 > guest.page_entries_total * 9 ? MEDIUM
|
|
||||||
: GOOD);
|
|
||||||
ImGui::EndTable();
|
ImGui::EndTable();
|
||||||
}
|
}
|
||||||
if (BeginSection("Emulator process")) {
|
if (BeginSection("Emulator process")) {
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ public:
|
|||||||
void WaitForSubmitSlot(VideoOutConfig& cfg);
|
void WaitForSubmitSlot(VideoOutConfig& cfg);
|
||||||
bool Flip(uint32_t micros);
|
bool Flip(uint32_t micros);
|
||||||
void GetFlipStatus(VideoOutConfig& cfg, VideoOutFlipStatus& out);
|
void GetFlipStatus(VideoOutConfig& cfg, VideoOutFlipStatus& out);
|
||||||
bool IsSafeForRendering(VideoOutConfig& cfg, int index);
|
void Wait(VideoOutConfig& cfg, int index);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class RequestState { Reserved, Recording, Ready, Presenting };
|
enum class RequestState { Reserved, Recording, Ready, Presenting };
|
||||||
@@ -240,7 +240,6 @@ private:
|
|||||||
Graphics::Presenter::Frame* frame;
|
Graphics::Presenter::Frame* frame;
|
||||||
bool premultiplied_alpha;
|
bool premultiplied_alpha;
|
||||||
};
|
};
|
||||||
void SignalDoneLocked();
|
|
||||||
|
|
||||||
Graphics::Presenter& m_presenter;
|
Graphics::Presenter& m_presenter;
|
||||||
Common::Mutex m_mutex;
|
Common::Mutex m_mutex;
|
||||||
@@ -251,7 +250,6 @@ private:
|
|||||||
std::list<Request> m_cpu_requests;
|
std::list<Request> m_cpu_requests;
|
||||||
std::list<Request> m_cancelled_requests;
|
std::list<Request> m_cancelled_requests;
|
||||||
bool m_processing = false;
|
bool m_processing = false;
|
||||||
bool m_gpu_waiting = false;
|
|
||||||
uint64_t m_next_request_id = 1;
|
uint64_t m_next_request_id = 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -955,7 +953,7 @@ bool FlipQueue::Reserve(VideoOutConfig& cfg, int index, int64_t flip_arg, FlipRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SignalDoneLocked();
|
m_done_cond_var.SignalAll();
|
||||||
}
|
}
|
||||||
if (cfg.flip_status.flipPendingNum >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) {
|
if (cfg.flip_status.flipPendingNum >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) {
|
||||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::FlipsRejected);
|
Common::DebugCounters::Add(Common::DebugCounters::Counter::FlipsRejected);
|
||||||
@@ -1035,7 +1033,7 @@ void FlipQueue::Cancel(VideoOutConfig& cfg) {
|
|||||||
it = queue->erase(it);
|
it = queue->erase(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SignalDoneLocked();
|
m_done_cond_var.SignalAll();
|
||||||
m_submit_slot_cond_var.SignalAll();
|
m_submit_slot_cond_var.SignalAll();
|
||||||
m_submit_cond_var.SignalAll();
|
m_submit_cond_var.SignalAll();
|
||||||
m_mutex.Unlock();
|
m_mutex.Unlock();
|
||||||
@@ -1207,24 +1205,17 @@ void FlipQueue::WaitForSubmitSlot(VideoOutConfig& cfg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FlipQueue::SignalDoneLocked() {
|
void FlipQueue::Wait(VideoOutConfig& cfg, int index) {
|
||||||
m_done_cond_var.SignalAll();
|
|
||||||
// A stalled GPU cannot drain and shut down without re-checking under m_mutex, so it is alive here.
|
|
||||||
if (m_gpu_waiting) {
|
|
||||||
m_gpu_waiting = false;
|
|
||||||
m_presenter.Renderer().GetGpu().Wake();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A display buffer is safe for rendering once it is neither displayed nor waiting to be.
|
|
||||||
bool FlipQueue::IsSafeForRendering(VideoOutConfig& cfg, int index) {
|
|
||||||
Common::LockGuard lock(m_mutex);
|
Common::LockGuard lock(m_mutex);
|
||||||
auto matches = [&cfg, index](const auto& r) { return r.cfg == &cfg && r.index == index; };
|
|
||||||
const bool safe = cfg.flip_status.currentBuffer != index &&
|
auto has_request = [this, &cfg, index] {
|
||||||
std::none_of(m_requests.begin(), m_requests.end(), matches) &&
|
auto matches = [&cfg, index](const auto& r) { return r.cfg == &cfg && r.index == index; };
|
||||||
std::none_of(m_cpu_requests.begin(), m_cpu_requests.end(), matches);
|
return std::any_of(m_requests.begin(), m_requests.end(), matches) ||
|
||||||
m_gpu_waiting = !safe;
|
std::any_of(m_cpu_requests.begin(), m_cpu_requests.end(), matches);
|
||||||
return safe;
|
};
|
||||||
|
while (has_request()) {
|
||||||
|
m_done_cond_var.Wait(&m_mutex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FlipQueue::Flip(uint32_t micros) {
|
bool FlipQueue::Flip(uint32_t micros) {
|
||||||
@@ -1320,7 +1311,7 @@ bool FlipQueue::Flip(uint32_t micros) {
|
|||||||
it = m_requests.erase(it);
|
it = m_requests.erase(it);
|
||||||
}
|
}
|
||||||
m_processing = false;
|
m_processing = false;
|
||||||
SignalDoneLocked();
|
m_done_cond_var.SignalAll();
|
||||||
m_submit_slot_cond_var.SignalAll();
|
m_submit_slot_cond_var.SignalAll();
|
||||||
m_mutex.Unlock();
|
m_mutex.Unlock();
|
||||||
for (size_t i = count; i != 0; i--) {
|
for (size_t i = count; i != 0; i--) {
|
||||||
@@ -1703,12 +1694,12 @@ void VideoOutDriver::WaitForSubmitSlot(int handle) {
|
|||||||
m_impl->GetFlipQueue().WaitForSubmitSlot(*cfg);
|
m_impl->GetFlipQueue().WaitForSubmitSlot(*cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VideoOutDriver::IsSafeForRendering(int handle, int index) {
|
void VideoOutDriver::WaitFlipDone(int handle, int index) {
|
||||||
auto* ctx = m_impl->Get(handle);
|
auto* ctx = m_impl->Get(handle);
|
||||||
EXIT_IF(ctx == nullptr);
|
EXIT_IF(ctx == nullptr);
|
||||||
|
|
||||||
EXIT_NOT_IMPLEMENTED(!IsValidBufferIndex(index));
|
EXIT_NOT_IMPLEMENTED(!IsValidBufferIndex(index));
|
||||||
return m_impl->GetFlipQueue().IsSafeForRendering(*ctx, index);
|
m_impl->GetFlipQueue().Wait(*ctx, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
KYTY_SYSV_ABI int VideoOutGetFlipStatus(int handle, VideoOutFlipStatus* status) {
|
KYTY_SYSV_ABI int VideoOutGetFlipStatus(int handle, VideoOutFlipStatus* status) {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public:
|
|||||||
void CompleteFlip(uint64_t request_id);
|
void CompleteFlip(uint64_t request_id);
|
||||||
void SubmitFlipPreparation(uint64_t request_id);
|
void SubmitFlipPreparation(uint64_t request_id);
|
||||||
void WaitForSubmitSlot(int handle);
|
void WaitForSubmitSlot(int handle);
|
||||||
[[nodiscard]] bool IsSafeForRendering(int handle, int index);
|
void WaitFlipDone(int handle, int index);
|
||||||
|
|
||||||
[[nodiscard]] Impl& State() noexcept;
|
[[nodiscard]] Impl& State() noexcept;
|
||||||
|
|
||||||
|
|||||||
@@ -290,7 +290,8 @@ public:
|
|||||||
private:
|
private:
|
||||||
vk::SurfaceFormatKHR SelectFormat();
|
vk::SurfaceFormatKHR SelectFormat();
|
||||||
void Destroy();
|
void Destroy();
|
||||||
void DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer);
|
void DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer,
|
||||||
|
const vk::Rect2D& rect);
|
||||||
|
|
||||||
WindowContext& m_window;
|
WindowContext& m_window;
|
||||||
vk::SwapchainKHR m_handle = nullptr;
|
vk::SwapchainKHR m_handle = nullptr;
|
||||||
@@ -637,7 +638,23 @@ bool Swapchain::PrepareSystemOverlay() {
|
|||||||
return m_system_overlay->PrepareFrame(m_extent, m_format, ImageCount());
|
return m_system_overlay->PrepareFrame(m_extent, m_format, ImageCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer) {
|
static vk::Rect2D FitAspect(const vk::Extent3D& source, const vk::Extent2D& target) {
|
||||||
|
const uint64_t src_w = std::max(source.width, 1u);
|
||||||
|
const uint64_t src_h = std::max(source.height, 1u);
|
||||||
|
uint32_t w = target.width;
|
||||||
|
uint32_t h = target.height;
|
||||||
|
if (src_w * h > src_h * w) {
|
||||||
|
h = std::max<uint32_t>(1, static_cast<uint32_t>(src_h * w / src_w));
|
||||||
|
} else {
|
||||||
|
w = std::max<uint32_t>(1, static_cast<uint32_t>(src_w * h / src_h));
|
||||||
|
}
|
||||||
|
return {{static_cast<int32_t>((target.width - w) / 2),
|
||||||
|
static_cast<int32_t>((target.height - h) / 2)},
|
||||||
|
{w, h}};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer,
|
||||||
|
const vk::Rect2D& rect) {
|
||||||
auto device = m_window.graphic_ctx.device;
|
auto device = m_window.graphic_ctx.device;
|
||||||
if (m_overlay_pipeline == nullptr) {
|
if (m_overlay_pipeline == nullptr) {
|
||||||
vk::SamplerCreateInfo sampler {};
|
vk::SamplerCreateInfo sampler {};
|
||||||
@@ -756,12 +773,14 @@ void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& l
|
|||||||
rendering.colorAttachmentCount = 1;
|
rendering.colorAttachmentCount = 1;
|
||||||
rendering.pColorAttachments = &attachment;
|
rendering.pColorAttachments = &attachment;
|
||||||
command.beginRendering(&rendering);
|
command.beginRendering(&rendering);
|
||||||
const vk::Viewport viewport {
|
const vk::Viewport viewport {static_cast<float>(rect.offset.x),
|
||||||
0.0f, 0.0f, static_cast<float>(m_extent.width), static_cast<float>(m_extent.height),
|
static_cast<float>(rect.offset.y),
|
||||||
0.0f, 1.0f};
|
static_cast<float>(rect.extent.width),
|
||||||
const vk::Rect2D scissor {{0, 0}, m_extent};
|
static_cast<float>(rect.extent.height),
|
||||||
|
0.0f,
|
||||||
|
1.0f};
|
||||||
command.setViewport(0, 1, &viewport);
|
command.setViewport(0, 1, &viewport);
|
||||||
command.setScissor(0, 1, &scissor);
|
command.setScissor(0, 1, &rect);
|
||||||
command.draw(3, 1, 0, 0);
|
command.draw(3, 1, 0, 0);
|
||||||
command.endRendering();
|
command.endRendering();
|
||||||
}
|
}
|
||||||
@@ -799,6 +818,24 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame*
|
|||||||
vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlags {}, 0,
|
vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlags {}, 0,
|
||||||
nullptr, 0, nullptr, 1, &to_transfer);
|
nullptr, 0, nullptr, 1, &to_transfer);
|
||||||
|
|
||||||
|
const auto* aspect_source = source != nullptr ? source : overlay.frame;
|
||||||
|
const auto game_rect = !Config::StretchEnabled() && aspect_source != nullptr
|
||||||
|
? FitAspect(aspect_source->image.extent, m_extent)
|
||||||
|
: vk::Rect2D {{0, 0}, m_extent};
|
||||||
|
if (source == nullptr || game_rect.extent != m_extent) {
|
||||||
|
const vk::ClearColorValue black {};
|
||||||
|
vk_command.clearColorImage(m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal,
|
||||||
|
&black, 1, &to_transfer.subresourceRange);
|
||||||
|
if (source != nullptr) {
|
||||||
|
vk::ImageMemoryBarrier after_clear = to_transfer;
|
||||||
|
after_clear.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
|
||||||
|
after_clear.oldLayout = vk::ImageLayout::eTransferDstOptimal;
|
||||||
|
vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
|
||||||
|
vk::PipelineStageFlagBits::eTransfer,
|
||||||
|
vk::DependencyFlags {}, 0, nullptr, 0, nullptr, 1,
|
||||||
|
&after_clear);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (source != nullptr) {
|
if (source != nullptr) {
|
||||||
vk::ImageBlit region {};
|
vk::ImageBlit region {};
|
||||||
region.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
|
region.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
|
||||||
@@ -812,16 +849,14 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame*
|
|||||||
region.dstSubresource.mipLevel = 0;
|
region.dstSubresource.mipLevel = 0;
|
||||||
region.dstSubresource.baseArrayLayer = 0;
|
region.dstSubresource.baseArrayLayer = 0;
|
||||||
region.dstSubresource.layerCount = 1;
|
region.dstSubresource.layerCount = 1;
|
||||||
region.dstOffsets[1].x = static_cast<int>(m_extent.width);
|
region.dstOffsets[0].x = game_rect.offset.x;
|
||||||
region.dstOffsets[1].y = static_cast<int>(m_extent.height);
|
region.dstOffsets[0].y = game_rect.offset.y;
|
||||||
|
region.dstOffsets[1].x = game_rect.offset.x + static_cast<int>(game_rect.extent.width);
|
||||||
|
region.dstOffsets[1].y = game_rect.offset.y + static_cast<int>(game_rect.extent.height);
|
||||||
region.dstOffsets[1].z = 1;
|
region.dstOffsets[1].z = 1;
|
||||||
vk_command.blitImage(source->image.image, vk::ImageLayout::eTransferSrcOptimal,
|
vk_command.blitImage(source->image.image, vk::ImageLayout::eTransferSrcOptimal,
|
||||||
m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal, 1,
|
m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal, 1,
|
||||||
®ion, vk::Filter::eLinear);
|
®ion, vk::Filter::eLinear);
|
||||||
} else {
|
|
||||||
const vk::ClearColorValue black {};
|
|
||||||
vk_command.clearColorImage(m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal,
|
|
||||||
&black, 1, &to_transfer.subresourceRange);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vk::ImageMemoryBarrier to_present {};
|
vk::ImageMemoryBarrier to_present {};
|
||||||
@@ -848,7 +883,7 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame*
|
|||||||
&to_present);
|
&to_present);
|
||||||
if (draw_attachment) {
|
if (draw_attachment) {
|
||||||
if (draw_overlay) {
|
if (draw_overlay) {
|
||||||
DrawOverlay(vk_command, overlay);
|
DrawOverlay(vk_command, overlay, game_rect);
|
||||||
if (draw_system_overlay) {
|
if (draw_system_overlay) {
|
||||||
to_present.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
|
to_present.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
|
||||||
to_present.oldLayout = vk::ImageLayout::eColorAttachmentOptimal;
|
to_present.oldLayout = vk::ImageLayout::eColorAttachmentOptimal;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ struct VulkanExtensions {
|
|||||||
vk::PhysicalDeviceVulkan11Features WindowContext::RequiredVulkan11Features() noexcept {
|
vk::PhysicalDeviceVulkan11Features WindowContext::RequiredVulkan11Features() noexcept {
|
||||||
vk::PhysicalDeviceVulkan11Features features {};
|
vk::PhysicalDeviceVulkan11Features features {};
|
||||||
features.storageBuffer16BitAccess = VK_TRUE;
|
features.storageBuffer16BitAccess = VK_TRUE;
|
||||||
|
features.shaderDrawParameters = VK_TRUE;
|
||||||
return features;
|
return features;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +279,8 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
|||||||
required_features12.samplerMirrorClampToEdge);
|
required_features12.samplerMirrorClampToEdge);
|
||||||
check_feature(features11.storageBuffer16BitAccess, "storageBuffer16BitAccess",
|
check_feature(features11.storageBuffer16BitAccess, "storageBuffer16BitAccess",
|
||||||
WindowContext::RequiredVulkan11Features().storageBuffer16BitAccess);
|
WindowContext::RequiredVulkan11Features().storageBuffer16BitAccess);
|
||||||
|
check_feature(features11.shaderDrawParameters, "shaderDrawParameters",
|
||||||
|
WindowContext::RequiredVulkan11Features().shaderDrawParameters);
|
||||||
check_feature(features12.storageBuffer8BitAccess, "storageBuffer8BitAccess",
|
check_feature(features12.storageBuffer8BitAccess, "storageBuffer8BitAccess",
|
||||||
required_features12.storageBuffer8BitAccess);
|
required_features12.storageBuffer8BitAccess);
|
||||||
check_feature(features12.timelineSemaphore, "timelineSemaphore",
|
check_feature(features12.timelineSemaphore, "timelineSemaphore",
|
||||||
|
|||||||
@@ -814,7 +814,7 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
|
|||||||
const auto& cfg = *selected_cfg;
|
const auto& cfg = *selected_cfg;
|
||||||
Frontend::EmbeddedFetchPlan embedded_fetch;
|
Frontend::EmbeddedFetchPlan embedded_fetch;
|
||||||
if ((options.stage == ShaderType::Vertex || options.stage == ShaderType::Local) &&
|
if ((options.stage == ShaderType::Vertex || options.stage == ShaderType::Local) &&
|
||||||
options.input_info.vertex->fetch_embedded) {
|
options.input_info.vertex->has_dynamic_vertex_fetch) {
|
||||||
embedded_fetch = DetectEmbeddedVertexFetch(
|
embedded_fetch = DetectEmbeddedVertexFetch(
|
||||||
decoded, *options.input_info.vertex, options.user_data_base,
|
decoded, *options.input_info.vertex, options.user_data_base,
|
||||||
static_cast<uint32_t>(options.user_data.size()), options.wave_size);
|
static_cast<uint32_t>(options.user_data.size()), options.wave_size);
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
|
|||||||
}
|
}
|
||||||
if (kind == IR::StageInputKind::VertexIndex || kind == IR::StageInputKind::InstanceIndex ||
|
if (kind == IR::StageInputKind::VertexIndex || kind == IR::StageInputKind::InstanceIndex ||
|
||||||
kind == IR::StageInputKind::InvocationId || kind == IR::StageInputKind::PrimitiveId ||
|
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 value = state.builder.AllocateId();
|
||||||
const auto bits = state.builder.AllocateId();
|
const auto bits = state.builder.AllocateId();
|
||||||
state.builder.AddFunction(spv::OpLoad, TypeI32(state), value, variable);
|
state.builder.AddFunction(spv::OpLoad, TypeI32(state), value, variable);
|
||||||
|
|||||||
@@ -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 AddressU32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address,
|
||||||
uint32_t component) {
|
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;
|
const auto packed = layout.bit_offset / 32u;
|
||||||
if (packed >= address.NumArgs()) return ConstantU32(ctx.state, 0);
|
if (packed >= address.NumArgs()) return ConstantU32(ctx.state, 0);
|
||||||
auto value = ctx.Def(address.Arg(packed));
|
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 AddressF32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address,
|
||||||
uint32_t component) {
|
uint32_t component) {
|
||||||
const auto value = AddressU32(ctx, mem, address, component);
|
const auto value = AddressU32(ctx, mem, address, component);
|
||||||
return Decoder::ImageAddressComponentLayout(mem.image_sample_flags, component).bit_width == 16u
|
const auto layout = Decoder::ImageAddressComponentLayout(mem.image_sample_flags,
|
||||||
? EmitF16BitsToF32(ctx.state, value)
|
mem.image_dimension, component);
|
||||||
: Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), value);
|
return layout.bit_width == 16u ? EmitF16BitsToF32(ctx.state, value)
|
||||||
|
: Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageSampleLayout Layout(const IR::MemoryInfo& mem) {
|
ImageSampleLayout Layout(const IR::MemoryInfo& mem) {
|
||||||
@@ -811,6 +813,25 @@ uint32_t EmitImageResult(ValueEmitContext& ctx, const IR::Inst& inst, const Emit
|
|||||||
return mixed ? value : result_bits(mem.resource, value);
|
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 {
|
struct ImageSampler {
|
||||||
uint32_t id = 0;
|
uint32_t id = 0;
|
||||||
bool dynamic = false;
|
bool dynamic = false;
|
||||||
@@ -846,9 +867,7 @@ std::array<ImageSampler, 2> LoadImageSamplers(ValueEmitContext& ctx, const IR::I
|
|||||||
selected = EmitIndirectResourceIndex(state, selected,
|
selected = EmitIndirectResourceIndex(state, selected,
|
||||||
samplers[index].indirect_mapping_offset,
|
samplers[index].indirect_mapping_offset,
|
||||||
samplers[index].indirect_search_iterations, 0u);
|
samplers[index].indirect_search_iterations, 0u);
|
||||||
array_index = EmitIndexSwitch(
|
array_index = SamplerArrayIndex(state, selected, candidates);
|
||||||
state, selected, static_cast<uint32_t>(candidates.size()),
|
|
||||||
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, candidates[ordinal]); });
|
|
||||||
}
|
}
|
||||||
sampler = {LoadSamplerDescriptor(state, index, array_index), array_index != 0u};
|
sampler = {LoadSamplerDescriptor(state, index, array_index), array_index != 0u};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ inline constexpr auto EmitBufferAtomicIAdd32 = EmitAtomic32;
|
|||||||
inline constexpr auto EmitBufferAtomicISub32 = EmitAtomic32;
|
inline constexpr auto EmitBufferAtomicISub32 = EmitAtomic32;
|
||||||
inline constexpr auto EmitBufferAtomicSMin32 = EmitAtomic32;
|
inline constexpr auto EmitBufferAtomicSMin32 = EmitAtomic32;
|
||||||
inline constexpr auto EmitBufferAtomicUMin32 = EmitAtomic32;
|
inline constexpr auto EmitBufferAtomicUMin32 = EmitAtomic32;
|
||||||
|
inline constexpr auto EmitBufferAtomicUMin64 = EmitBufferAtomic64;
|
||||||
inline constexpr auto EmitBufferAtomicSMax32 = EmitAtomic32;
|
inline constexpr auto EmitBufferAtomicSMax32 = EmitAtomic32;
|
||||||
inline constexpr auto EmitBufferAtomicUMax32 = EmitAtomic32;
|
inline constexpr auto EmitBufferAtomicUMax32 = EmitAtomic32;
|
||||||
inline constexpr auto EmitBufferAtomicAnd32 = EmitAtomic32;
|
inline constexpr auto EmitBufferAtomicAnd32 = EmitAtomic32;
|
||||||
|
|||||||
@@ -585,6 +585,7 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
|||||||
case IR::ValueOpcode::ImageAtomicUMin32:
|
case IR::ValueOpcode::ImageAtomicUMin32:
|
||||||
case IR::ValueOpcode::ImageAtomicUMin64:
|
case IR::ValueOpcode::ImageAtomicUMin64:
|
||||||
case IR::ValueOpcode::BufferAtomicUMin32:
|
case IR::ValueOpcode::BufferAtomicUMin32:
|
||||||
|
case IR::ValueOpcode::BufferAtomicUMin64:
|
||||||
case IR::ValueOpcode::SharedAtomicUMin32: return spv::OpAtomicUMin;
|
case IR::ValueOpcode::SharedAtomicUMin32: return spv::OpAtomicUMin;
|
||||||
case IR::ValueOpcode::ImageAtomicSMax32:
|
case IR::ValueOpcode::ImageAtomicSMax32:
|
||||||
case IR::ValueOpcode::BufferAtomicSMax32:
|
case IR::ValueOpcode::BufferAtomicSMax32:
|
||||||
|
|||||||
@@ -476,6 +476,8 @@ uint32_t BuiltInForInput(IR::StageInputKind kind) {
|
|||||||
case IR::StageInputKind::LocalInvocationId: return spv::BuiltInLocalInvocationId;
|
case IR::StageInputKind::LocalInvocationId: return spv::BuiltInLocalInvocationId;
|
||||||
case IR::StageInputKind::LocalInvocationIndex: return spv::BuiltInLocalInvocationIndex;
|
case IR::StageInputKind::LocalInvocationIndex: return spv::BuiltInLocalInvocationIndex;
|
||||||
case IR::StageInputKind::GlobalInvocationId: return spv::BuiltInGlobalInvocationId;
|
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;
|
default: return UINT32_MAX;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -526,7 +528,9 @@ void DefineInputs(EmitterState& state) {
|
|||||||
case IR::StageInputKind::PrimitiveId:
|
case IR::StageInputKind::PrimitiveId:
|
||||||
case IR::StageInputKind::InstanceIndex:
|
case IR::StageInputKind::InstanceIndex:
|
||||||
case IR::StageInputKind::Layer:
|
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::WorkgroupId:
|
||||||
case IR::StageInputKind::NumWorkgroups:
|
case IR::StageInputKind::NumWorkgroups:
|
||||||
case IR::StageInputKind::LocalInvocationId:
|
case IR::StageInputKind::LocalInvocationId:
|
||||||
@@ -802,6 +806,10 @@ void DefineModule(EmitterState& state) {
|
|||||||
if (InputVariableForKind(state, IR::StageInputKind::SampleId) != 0) {
|
if (InputVariableForKind(state, IR::StageInputKind::SampleId) != 0) {
|
||||||
state.builder.RequireCapability(spv::CapabilitySampleRateShading);
|
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) {
|
if (state.program.info.image_gather_extended) {
|
||||||
state.builder.RequireCapability(spv::CapabilityImageGatherExtended);
|
state.builder.RequireCapability(spv::CapabilityImageGatherExtended);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,6 @@ struct MimgSampleInfo {
|
|||||||
uint32_t flags = 0;
|
uint32_t flags = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MimgGatherInfo {
|
|
||||||
uint32_t encoding = 0;
|
|
||||||
Opcode decoded = Opcode::UNSUPPORTED;
|
|
||||||
uint32_t flags = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct MimgAtomicInfo {
|
struct MimgAtomicInfo {
|
||||||
uint32_t encoding = 0;
|
uint32_t encoding = 0;
|
||||||
Opcode decoded = Opcode::UNSUPPORTED;
|
Opcode decoded = Opcode::UNSUPPORTED;
|
||||||
@@ -181,23 +175,72 @@ constexpr MimgSampleInfo MIMG_SAMPLE_OPCODE_LIST[] = {
|
|||||||
ImageSampleFlagOffset | ImageSampleFlagAdjust},
|
ImageSampleFlagOffset | ImageSampleFlagAdjust},
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr MimgGatherInfo MIMG_GATHER_OPCODE_LIST[] = {
|
constexpr MimgSampleInfo MIMG_GATHER_OPCODE_LIST[] = {
|
||||||
{0x44u, Opcode::IMAGE_GATHER4_L, ImageSampleFlagLod},
|
{0x40u, "image_gather4", 0},
|
||||||
{0x47u, Opcode::IMAGE_GATHER4_LZ, ImageSampleFlagLevelZero},
|
{0x41u, "image_gather4_cl", ImageSampleFlagLodClamp},
|
||||||
{0x48u, Opcode::IMAGE_GATHER4_C, ImageSampleFlagCompare},
|
{0x44u, "image_gather4_l", ImageSampleFlagLod},
|
||||||
{0x4cu, Opcode::IMAGE_GATHER4_C_L, ImageSampleFlagCompare | ImageSampleFlagLod},
|
{0x45u, "image_gather4_b", ImageSampleFlagBias},
|
||||||
{0x4fu, Opcode::IMAGE_GATHER4_C_LZ,
|
{0x46u, "image_gather4_b_cl", ImageSampleFlagBias | ImageSampleFlagLodClamp},
|
||||||
ImageSampleFlagCompare | ImageSampleFlagLevelZero},
|
{0x47u, "image_gather4_lz", ImageSampleFlagLevelZero},
|
||||||
{0x57u, Opcode::IMAGE_GATHER4_LZ_O,
|
{0x48u, "image_gather4_c", ImageSampleFlagCompare},
|
||||||
ImageSampleFlagLevelZero | ImageSampleFlagOffset},
|
{0x49u, "image_gather4_c_cl", ImageSampleFlagCompare | ImageSampleFlagLodClamp},
|
||||||
{0x58u, Opcode::IMAGE_GATHER4_C_O,
|
{0x4cu, "image_gather4_c_l", ImageSampleFlagCompare | ImageSampleFlagLod},
|
||||||
ImageSampleFlagCompare | ImageSampleFlagOffset},
|
{0x4du, "image_gather4_c_b", ImageSampleFlagCompare | ImageSampleFlagBias},
|
||||||
{0x5fu, Opcode::IMAGE_GATHER4_C_LZ_O,
|
{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},
|
ImageSampleFlagCompare | ImageSampleFlagLevelZero | ImageSampleFlagOffset},
|
||||||
{0x61u, Opcode::IMAGE_GATHER4H, ImageSampleFlagGatherHorizontal},
|
{0x61u, "image_gather4h", ImageSampleFlagGatherHorizontal},
|
||||||
{0xc8u, Opcode::IMAGE_GATHER4_C, ImageSampleFlagCompare | ImageSampleFlagAdjust},
|
{0xc0u, "image_gather4_a", ImageSampleFlagAdjust},
|
||||||
{0xd8u, Opcode::IMAGE_GATHER4_C_O,
|
{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},
|
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[] = {
|
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_GATHER_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_GATHER_OPCODE_LIST);
|
||||||
constexpr auto MIMG_ATOMIC_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_ATOMIC_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) {
|
const MimgAtomicInfo* atomic) {
|
||||||
if (sample != nullptr) {
|
if (sample != nullptr) {
|
||||||
return Opcode::IMAGE_SAMPLE;
|
return Opcode::IMAGE_SAMPLE;
|
||||||
}
|
}
|
||||||
if (gather != nullptr) {
|
if (gather != nullptr) {
|
||||||
return gather->decoded;
|
return Opcode::IMAGE_GATHER4;
|
||||||
}
|
}
|
||||||
if (atomic != nullptr) {
|
if (atomic != nullptr) {
|
||||||
return atomic->decoded;
|
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,
|
uint32_t DecodeMimgAddressComponents(uint32_t opcode, ImageDimension dimension,
|
||||||
const MimgSampleInfo* sample, const MimgGatherInfo* gather,
|
const MimgSampleInfo* sampled, const MimgAtomicInfo* atomic) {
|
||||||
const MimgAtomicInfo* atomic) {
|
if (sampled != nullptr) {
|
||||||
if (sample != nullptr) {
|
return ImageSampleAddressComponents(sampled->flags, dimension);
|
||||||
return ImageSampleAddressComponents(sample->flags, dimension);
|
|
||||||
}
|
|
||||||
if (gather != nullptr) {
|
|
||||||
return ImageSampleAddressComponents(gather->flags, dimension);
|
|
||||||
}
|
}
|
||||||
if (atomic != nullptr) {
|
if (atomic != nullptr) {
|
||||||
return ImageCoordComponents(dimension);
|
return ImageCoordComponents(dimension);
|
||||||
@@ -290,33 +319,31 @@ uint32_t CountDmaskComponents(uint32_t dmask) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, uint32_t component) {
|
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, ImageDimension dimension,
|
||||||
const auto width = [flags](uint32_t index) {
|
uint32_t component) {
|
||||||
if ((flags & ImageSampleFlagA16) == 0u) return 32u;
|
if ((flags & ImageSampleFlagA16) == 0u) return {component * 32u, 32u};
|
||||||
uint32_t cursor = 0;
|
// A16 offsets, bias and PCF reference each take a whole VGPR, the bias as a half in its low
|
||||||
if ((flags & ImageSampleFlagOffset) != 0u) {
|
// 16 bits. The other components pack two per VGPR, except that each 1D derivative and the last
|
||||||
if (index == cursor++) return 32u;
|
// two 3D ones take the low half of their own VGPR.
|
||||||
}
|
const bool has_bias = (flags & ImageSampleFlagBias) != 0u;
|
||||||
if ((flags & ImageSampleFlagBias) != 0u) {
|
const uint32_t bias_index = (flags & ImageSampleFlagOffset) != 0u ? 1u : 0u;
|
||||||
if (index == cursor++) return 16u;
|
const uint32_t whole_vgprs = std::popcount(
|
||||||
}
|
flags & (ImageSampleFlagOffset | ImageSampleFlagBias | ImageSampleFlagCompare));
|
||||||
if ((flags & ImageSampleFlagCompare) != 0u && index == cursor) return 32u;
|
if (component < whole_vgprs) {
|
||||||
return 16u;
|
return {component * 32u, has_bias && component == bias_index ? 16u : 32u};
|
||||||
};
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
const auto bits = width(component);
|
const uint32_t axes =
|
||||||
if (bits == 32u) offset = (offset + 31u) & ~31u;
|
(flags & ImageSampleFlagDerivative) != 0u ? ImageGradientComponents(dimension) : 0u;
|
||||||
return {offset, bits};
|
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;
|
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;
|
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 auto dimension = DecodeImageDimension((word0 >> 3u) & 0x7u);
|
||||||
const uint32_t word_count = 2u + nsa_dwords;
|
const uint32_t word_count = 2u + nsa_dwords;
|
||||||
|
|
||||||
const uint32_t vdata = (word1 >> 8u) & 0xffu;
|
const uint32_t vdata = (word1 >> 8u) & 0xffu;
|
||||||
const uint32_t vaddr = word1 & 0xffu;
|
const uint32_t vaddr = word1 & 0xffu;
|
||||||
const uint32_t srsrc = (word1 >> 16u) & 0x1fu;
|
const uint32_t srsrc = (word1 >> 16u) & 0x1fu;
|
||||||
const uint32_t ssamp = (word1 >> 21u) & 0x1fu;
|
const uint32_t ssamp = (word1 >> 21u) & 0x1fu;
|
||||||
const bool r128 = ((word0 >> 15u) & 0x1u) != 0u;
|
const bool r128 = ((word0 >> 15u) & 0x1u) != 0u;
|
||||||
const bool a16 = ((word1 >> 30u) & 0x1u) != 0u;
|
const bool a16 = ((word1 >> 30u) & 0x1u) != 0u;
|
||||||
const bool d16 = ((word1 >> 31u) & 0x1u) != 0u;
|
const bool d16 = ((word1 >> 31u) & 0x1u) != 0u;
|
||||||
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
|
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
|
||||||
const auto* gather = Detail::FindOpcode(MIMG_GATHER_OPS, opcode);
|
const auto* gather = Detail::FindOpcode(MIMG_GATHER_OPS, opcode);
|
||||||
const auto* atomic = Detail::FindOpcode(MIMG_ATOMIC_OPS, opcode);
|
const auto* atomic = Detail::FindOpcode(MIMG_ATOMIC_OPS, opcode);
|
||||||
|
const auto* sampled = sample != nullptr ? sample : gather;
|
||||||
|
|
||||||
inst.pc = pc;
|
inst.pc = pc;
|
||||||
inst.word_count = word_count;
|
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.data_dwords = d16 ? (inst.data_components + 1u) / 2u : inst.data_components;
|
||||||
inst.glc = ((word0 >> 13u) & 1u) != 0;
|
inst.glc = ((word0 >> 13u) & 1u) != 0;
|
||||||
inst.slc = ((word0 >> 25u) & 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) {
|
if (a16) {
|
||||||
inst.image_sample_flags |= ImageSampleFlagA16;
|
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_sample_flags |= ImageSampleFlagUnnormalized;
|
||||||
}
|
}
|
||||||
inst.image_dimension = dimension;
|
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++) {
|
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_nsa_addr[i] = (code[word_index + 2u + i / 4u] >> ((i % 4u) * 8u)) & 0xffu;
|
||||||
}
|
}
|
||||||
inst.image_address_components =
|
inst.image_address_components = DecodeMimgAddressComponents(opcode, dimension, sampled, atomic);
|
||||||
DecodeMimgAddressComponents(opcode, dimension, sample, gather, atomic);
|
|
||||||
SetRawWords(inst, code, word_index, word_count);
|
SetRawWords(inst, code, word_index, word_count);
|
||||||
|
|
||||||
if (inst.opcode == Opcode::UNSUPPORTED) {
|
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 ||
|
const bool supports_d16 = sampled != nullptr || opcode == 0x00u || opcode == 0x01u ||
|
||||||
opcode == 0x01u || opcode == 0x08u || opcode == 0x09u;
|
opcode == 0x08u || opcode == 0x09u;
|
||||||
if (d16 && !supports_d16) {
|
if (d16 && !supports_d16) {
|
||||||
SetUnsupported(inst, Family::MIMG, opcode, "MIMG opcode does not support D16 data");
|
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;
|
inst.src_count = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* MimgSampleOpcodeName(uint32_t opcode) {
|
const char* MimgSampledOpcodeName(uint32_t opcode) {
|
||||||
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
|
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
|
||||||
return sample != nullptr ? sample->name : nullptr;
|
const auto* sampled = sample != nullptr ? sample : Detail::FindOpcode(MIMG_GATHER_OPS, opcode);
|
||||||
|
return sampled != nullptr ? sampled->name : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Libs::Graphics::ShaderRecompiler::Decoder
|
} // 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,
|
void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
|
||||||
Instruction& inst);
|
Instruction& inst);
|
||||||
|
|
||||||
const char* MimgSampleOpcodeName(uint32_t opcode);
|
const char* MimgSampledOpcodeName(uint32_t opcode);
|
||||||
|
|
||||||
struct ImageAddressComponent {
|
struct ImageAddressComponent {
|
||||||
uint32_t bit_offset;
|
uint32_t bit_offset;
|
||||||
uint32_t bit_width;
|
uint32_t bit_width;
|
||||||
};
|
};
|
||||||
|
|
||||||
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, uint32_t component);
|
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, ImageDimension dimension,
|
||||||
uint32_t ImageAddressDwordCount(uint32_t flags, uint32_t components);
|
uint32_t component);
|
||||||
|
uint32_t ImageAddressDwordCount(uint32_t flags, ImageDimension dimension, uint32_t components);
|
||||||
|
|
||||||
} // namespace Libs::Graphics::ShaderRecompiler::Decoder
|
} // 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},
|
{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},
|
{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},
|
{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[] = {
|
constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
|
||||||
@@ -69,6 +69,7 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
|
|||||||
{0x40u, Opcode::BUFFER_ATOMIC_FMAX, 1, 32},
|
{0x40u, Opcode::BUFFER_ATOMIC_FMAX, 1, 32},
|
||||||
{0x50u, Opcode::BUFFER_ATOMIC_SWAP_X2, 2, 32},
|
{0x50u, Opcode::BUFFER_ATOMIC_SWAP_X2, 2, 32},
|
||||||
{0x52u, Opcode::BUFFER_ATOMIC_ADD_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},
|
{0x59u, Opcode::BUFFER_ATOMIC_AND_X2, 2, 32},
|
||||||
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
|
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
|
||||||
{0x71u, Opcode::BUFFER_GL0_INV, 0, 0},
|
{0x71u, Opcode::BUFFER_GL0_INV, 0, 0},
|
||||||
@@ -95,14 +96,14 @@ constexpr MemoryOpcodeInfo MTBUF_OPCODE_LIST[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constexpr MemoryOpcodeInfo FLAT_OPCODE_LIST[] = {
|
constexpr MemoryOpcodeInfo FLAT_OPCODE_LIST[] = {
|
||||||
{0x08u, Opcode::FLAT_LOAD_UBYTE, 1, 8}, {0x09u, Opcode::FLAT_LOAD_SBYTE, 1, 8, true},
|
{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},
|
{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},
|
{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},
|
{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},
|
{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},
|
{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},
|
{0x1eu, Opcode::FLAT_STORE_DWORDX4, 4, 32}, {0x1fu, Opcode::FLAT_STORE_DWORDX3, 3, 32},
|
||||||
{0x24u, Opcode::FLAT_LOAD_SHORT_D16, 1, 16},
|
{0x24u, Opcode::FLAT_LOAD_SHORT_D16, 1, 16}, {0x25u, Opcode::FLAT_LOAD_SHORT_D16_HI, 1, 16},
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
|
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) {
|
if (inst.opcode == Opcode::UNSUPPORTED) {
|
||||||
SetUnsupported(inst, Family::SMEM, opcode, "SMEM opcode is not implemented");
|
SetUnsupported(inst, Family::SMEM, opcode, "SMEM opcode is not implemented");
|
||||||
}
|
}
|
||||||
|
if (inst.opcode == Opcode::S_DCACHE_INV) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
DecodeScalarDestination(sdst, pc, inst.dst);
|
DecodeScalarDestination(sdst, pc, inst.dst);
|
||||||
if (inst.opcode == Opcode::S_MEMREALTIME) {
|
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);
|
DecodeVectorGpr(IsFlatStoreOpcode(inst.opcode) ? data : vdst, inst.dst);
|
||||||
if (inst.opcode == Opcode::FLAT_LOAD_SHORT_D16) {
|
if (inst.opcode == Opcode::FLAT_LOAD_SHORT_D16 ||
|
||||||
// D16 loads reuse partial destinations to preserve the untouched high half.
|
inst.opcode == Opcode::FLAT_LOAD_SHORT_D16_HI) {
|
||||||
inst.dst.sdwa_sel = 4u;
|
// 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);
|
DecodeVectorGpr(addr, inst.src0);
|
||||||
inst.src_count = 1;
|
inst.src_count = 1;
|
||||||
|
|||||||
@@ -123,10 +123,9 @@ std::string ImageSampleFlagsToString(uint32_t flags) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string FormatMimg(const Instruction& inst) {
|
std::string FormatMimg(const Instruction& inst) {
|
||||||
const char* sample_name =
|
const char* sampled_name = MimgSampledOpcodeName(inst.opcode_id);
|
||||||
inst.opcode == Opcode::IMAGE_SAMPLE ? MimgSampleOpcodeName(inst.opcode_id) : nullptr;
|
|
||||||
const std::string_view name =
|
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 =
|
std::string text =
|
||||||
fmt::format("0x{:08x}: {} {}, {}, {}, {} ; dmask=0x{:x} image_dim={}", inst.pc, name,
|
fmt::format("0x{:08x}: {} {}, {}, {}, {} ; dmask=0x{:x} image_dim={}", inst.pc, name,
|
||||||
OperandToString(inst.dst).c_str(), OperandToString(inst.src0).c_str(),
|
OperandToString(inst.dst).c_str(), OperandToString(inst.src0).c_str(),
|
||||||
@@ -138,22 +137,10 @@ std::string FormatMimg(const Instruction& inst) {
|
|||||||
if (inst.image_r128) {
|
if (inst.image_r128) {
|
||||||
text += " r128=1";
|
text += " r128=1";
|
||||||
}
|
}
|
||||||
switch (inst.opcode) {
|
if (sampled_name != nullptr) {
|
||||||
case Opcode::IMAGE_SAMPLE:
|
text += fmt::format(" sample_flags={} addr_components={}",
|
||||||
case Opcode::IMAGE_GATHER4_L:
|
ImageSampleFlagsToString(inst.image_sample_flags).c_str(),
|
||||||
case Opcode::IMAGE_GATHER4_LZ:
|
inst.image_address_components);
|
||||||
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 (inst.image_nsa_dwords != 0) {
|
if (inst.image_nsa_dwords != 0) {
|
||||||
text += fmt::format(" nsa_dwords={} nsa_addr=", inst.image_nsa_dwords);
|
text += fmt::format(" nsa_dwords={} nsa_addr=", inst.image_nsa_dwords);
|
||||||
@@ -570,6 +557,7 @@ std::string InstructionToString(const Instruction& inst) {
|
|||||||
OperandToString(inst.src0).c_str()));
|
OperandToString(inst.src0).c_str()));
|
||||||
case Opcode::S_BARRIER:
|
case Opcode::S_BARRIER:
|
||||||
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: s_barrier", inst.pc));
|
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: s_barrier", inst.pc));
|
||||||
|
case Opcode::S_DCACHE_INV:
|
||||||
case Opcode::BUFFER_GL0_INV:
|
case Opcode::BUFFER_GL0_INV:
|
||||||
case Opcode::BUFFER_GL1_INV:
|
case Opcode::BUFFER_GL1_INV:
|
||||||
return fmt::format("0x{:08x}: {}", inst.pc, magic_enum::enum_name(inst.opcode));
|
return fmt::format("0x{:08x}: {}", inst.pc, magic_enum::enum_name(inst.opcode));
|
||||||
@@ -616,15 +604,7 @@ std::string InstructionToString(const Instruction& inst) {
|
|||||||
case Opcode::IMAGE_LOAD_MIP:
|
case Opcode::IMAGE_LOAD_MIP:
|
||||||
case Opcode::IMAGE_GET_RESINFO:
|
case Opcode::IMAGE_GET_RESINFO:
|
||||||
case Opcode::IMAGE_GET_LOD:
|
case Opcode::IMAGE_GET_LOD:
|
||||||
case Opcode::IMAGE_GATHER4_L:
|
case Opcode::IMAGE_GATHER4: return WithUnsupportedReason(inst, FormatMimg(inst));
|
||||||
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::S_LOAD_DWORD:
|
case Opcode::S_LOAD_DWORD:
|
||||||
case Opcode::S_LOAD_DWORDX2:
|
case Opcode::S_LOAD_DWORDX2:
|
||||||
case Opcode::S_LOAD_DWORDX4:
|
case Opcode::S_LOAD_DWORDX4:
|
||||||
@@ -685,6 +665,7 @@ std::string InstructionToString(const Instruction& inst) {
|
|||||||
case Opcode::BUFFER_ATOMIC_SUB:
|
case Opcode::BUFFER_ATOMIC_SUB:
|
||||||
case Opcode::BUFFER_ATOMIC_SMIN:
|
case Opcode::BUFFER_ATOMIC_SMIN:
|
||||||
case Opcode::BUFFER_ATOMIC_UMIN:
|
case Opcode::BUFFER_ATOMIC_UMIN:
|
||||||
|
case Opcode::BUFFER_ATOMIC_UMIN_X2:
|
||||||
case Opcode::BUFFER_ATOMIC_SMAX:
|
case Opcode::BUFFER_ATOMIC_SMAX:
|
||||||
case Opcode::BUFFER_ATOMIC_UMAX:
|
case Opcode::BUFFER_ATOMIC_UMAX:
|
||||||
case Opcode::BUFFER_ATOMIC_AND:
|
case Opcode::BUFFER_ATOMIC_AND:
|
||||||
@@ -701,6 +682,7 @@ std::string InstructionToString(const Instruction& inst) {
|
|||||||
case Opcode::FLAT_LOAD_USHORT:
|
case Opcode::FLAT_LOAD_USHORT:
|
||||||
case Opcode::FLAT_LOAD_SSHORT:
|
case Opcode::FLAT_LOAD_SSHORT:
|
||||||
case Opcode::FLAT_LOAD_SHORT_D16:
|
case Opcode::FLAT_LOAD_SHORT_D16:
|
||||||
|
case Opcode::FLAT_LOAD_SHORT_D16_HI:
|
||||||
case Opcode::FLAT_LOAD_DWORD:
|
case Opcode::FLAT_LOAD_DWORD:
|
||||||
case Opcode::FLAT_LOAD_DWORDX2:
|
case Opcode::FLAT_LOAD_DWORDX2:
|
||||||
case Opcode::FLAT_LOAD_DWORDX3:
|
case Opcode::FLAT_LOAD_DWORDX3:
|
||||||
|
|||||||
@@ -544,6 +544,7 @@ enum class Opcode {
|
|||||||
S_BUFFER_LOAD_DWORDX8,
|
S_BUFFER_LOAD_DWORDX8,
|
||||||
S_BUFFER_LOAD_DWORDX16,
|
S_BUFFER_LOAD_DWORDX16,
|
||||||
S_MEMREALTIME,
|
S_MEMREALTIME,
|
||||||
|
S_DCACHE_INV,
|
||||||
BUFFER_LOAD_FORMAT_X,
|
BUFFER_LOAD_FORMAT_X,
|
||||||
BUFFER_LOAD_FORMAT_XY,
|
BUFFER_LOAD_FORMAT_XY,
|
||||||
BUFFER_LOAD_FORMAT_XYZ,
|
BUFFER_LOAD_FORMAT_XYZ,
|
||||||
@@ -596,6 +597,7 @@ enum class Opcode {
|
|||||||
BUFFER_ATOMIC_SUB,
|
BUFFER_ATOMIC_SUB,
|
||||||
BUFFER_ATOMIC_SMIN,
|
BUFFER_ATOMIC_SMIN,
|
||||||
BUFFER_ATOMIC_UMIN,
|
BUFFER_ATOMIC_UMIN,
|
||||||
|
BUFFER_ATOMIC_UMIN_X2,
|
||||||
BUFFER_ATOMIC_SMAX,
|
BUFFER_ATOMIC_SMAX,
|
||||||
BUFFER_ATOMIC_UMAX,
|
BUFFER_ATOMIC_UMAX,
|
||||||
BUFFER_ATOMIC_AND,
|
BUFFER_ATOMIC_AND,
|
||||||
@@ -612,6 +614,7 @@ enum class Opcode {
|
|||||||
FLAT_LOAD_USHORT,
|
FLAT_LOAD_USHORT,
|
||||||
FLAT_LOAD_SSHORT,
|
FLAT_LOAD_SSHORT,
|
||||||
FLAT_LOAD_SHORT_D16,
|
FLAT_LOAD_SHORT_D16,
|
||||||
|
FLAT_LOAD_SHORT_D16_HI,
|
||||||
FLAT_LOAD_DWORD,
|
FLAT_LOAD_DWORD,
|
||||||
FLAT_LOAD_DWORDX2,
|
FLAT_LOAD_DWORDX2,
|
||||||
FLAT_LOAD_DWORDX3,
|
FLAT_LOAD_DWORDX3,
|
||||||
@@ -707,15 +710,7 @@ enum class Opcode {
|
|||||||
IMAGE_ATOMIC_FMIN,
|
IMAGE_ATOMIC_FMIN,
|
||||||
IMAGE_ATOMIC_FMAX,
|
IMAGE_ATOMIC_FMAX,
|
||||||
IMAGE_SAMPLE,
|
IMAGE_SAMPLE,
|
||||||
IMAGE_GATHER4_L,
|
IMAGE_GATHER4,
|
||||||
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,
|
|
||||||
V_INTERP_P1_F32,
|
V_INTERP_P1_F32,
|
||||||
V_INTERP_P2_F32,
|
V_INTERP_P2_F32,
|
||||||
V_INTERP_MOV_F32,
|
V_INTERP_MOV_F32,
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ void Translator::V_MAD_U64_U32(const Decoder::Instruction& inst) {
|
|||||||
inst.dst2.kind != Decoder::OperandKind::Unknown) {
|
inst.dst2.kind != Decoder::OperandKind::Unknown) {
|
||||||
// The 32x32 product fits in 64 bits, so only the addition can carry.
|
// The 32x32 product fits in 64 bits, so only the addition can carry.
|
||||||
const auto carry = IR::U1(ir.Emit(IR::ValueOpcode::ULessThan64, {result, product}));
|
const auto carry = IR::U1(ir.Emit(IR::ValueOpcode::ULessThan64, {result, product}));
|
||||||
WriteMask(inst.dst2, carry, VectorMask(carry));
|
WriteVectorMask(inst.dst2, carry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -322,8 +322,8 @@ IR::Value Translator::MakeImageAddress(const Decoder::Instruction& inst,
|
|||||||
const Decoder::Operand& base) {
|
const Decoder::Operand& base) {
|
||||||
std::array<IR::Value, 13> components {};
|
std::array<IR::Value, 13> components {};
|
||||||
components.fill(IR::Value(0u));
|
components.fill(IR::Value(0u));
|
||||||
const auto count =
|
const auto count = Decoder::ImageAddressDwordCount(
|
||||||
Decoder::ImageAddressDwordCount(inst.image_sample_flags, inst.image_address_components);
|
inst.image_sample_flags, inst.image_dimension, inst.image_address_components);
|
||||||
EXIT_IF(count > components.size());
|
EXIT_IF(count > components.size());
|
||||||
const auto nsa_components =
|
const auto nsa_components =
|
||||||
std::min(inst.image_nsa_dwords * 4u, Decoder::MaxImageNsaAddressComponents);
|
std::min(inst.image_nsa_dwords * 4u, Decoder::MaxImageNsaAddressComponents);
|
||||||
@@ -941,6 +941,8 @@ void Translator::DS_PERMUTE(const Decoder::Instruction& inst, bool backward) {
|
|||||||
|
|
||||||
void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||||
switch (inst.opcode) {
|
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_GL0_INV:
|
||||||
case Decoder::Opcode::BUFFER_GL1_INV:
|
case Decoder::Opcode::BUFFER_GL1_INV:
|
||||||
ir.Emit(IR::ValueOpcode::DataCacheInvalidate);
|
ir.Emit(IR::ValueOpcode::DataCacheInvalidate);
|
||||||
@@ -1029,6 +1031,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
|||||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicSMin32);
|
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicSMin32);
|
||||||
case Decoder::Opcode::BUFFER_ATOMIC_UMIN:
|
case Decoder::Opcode::BUFFER_ATOMIC_UMIN:
|
||||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicUMin32);
|
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:
|
case Decoder::Opcode::BUFFER_ATOMIC_SMAX:
|
||||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicSMax32);
|
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicSMax32);
|
||||||
case Decoder::Opcode::BUFFER_ATOMIC_UMAX:
|
case Decoder::Opcode::BUFFER_ATOMIC_UMAX:
|
||||||
@@ -1146,6 +1150,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
|||||||
case Decoder::Opcode::FLAT_LOAD_USHORT:
|
case Decoder::Opcode::FLAT_LOAD_USHORT:
|
||||||
case Decoder::Opcode::FLAT_LOAD_SSHORT:
|
case Decoder::Opcode::FLAT_LOAD_SSHORT:
|
||||||
case Decoder::Opcode::FLAT_LOAD_SHORT_D16:
|
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_DWORD:
|
||||||
case Decoder::Opcode::FLAT_LOAD_DWORDX2:
|
case Decoder::Opcode::FLAT_LOAD_DWORDX2:
|
||||||
case Decoder::Opcode::FLAT_LOAD_DWORDX3:
|
case Decoder::Opcode::FLAT_LOAD_DWORDX3:
|
||||||
@@ -1165,15 +1170,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
|||||||
case Decoder::Opcode::IMAGE_STORE:
|
case Decoder::Opcode::IMAGE_STORE:
|
||||||
case Decoder::Opcode::IMAGE_STORE_MIP: return IMAGE_STORE(inst);
|
case Decoder::Opcode::IMAGE_STORE_MIP: return IMAGE_STORE(inst);
|
||||||
case Decoder::Opcode::IMAGE_SAMPLE: return IMAGE_SAMPLE(inst);
|
case Decoder::Opcode::IMAGE_SAMPLE: return IMAGE_SAMPLE(inst);
|
||||||
case Decoder::Opcode::IMAGE_GATHER4_L:
|
case Decoder::Opcode::IMAGE_GATHER4: return IMAGE_GATHER(inst);
|
||||||
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::DS_MIN_F32:
|
case Decoder::Opcode::DS_MIN_F32:
|
||||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicFMin32, false);
|
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicFMin32, false);
|
||||||
|
|||||||
@@ -1389,10 +1389,28 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
|||||||
IR::U32(IR::Value(options.wave_size << 12u)));
|
IR::U32(IR::Value(options.wave_size << 12u)));
|
||||||
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3),
|
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3),
|
||||||
entry_ir.BitwiseOr(vertex_count, IR::U32(IR::Value(1u << 28u))));
|
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. SH_NOP leaves those SGPRs unchanged;
|
||||||
|
// it does not make the record's bases zero.
|
||||||
|
const auto& offsets = options.input_info.vertex->indirect_offsets;
|
||||||
|
const auto index = [&](uint8_t slot, IR::StageInputKind kind,
|
||||||
|
IR::StageInputKind base_kind) {
|
||||||
|
if (!options.input_info.vertex->indirect_draw) {
|
||||||
|
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),
|
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),
|
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 &&
|
const bool flush_f32_inputs = options.stage == ShaderType::Compute &&
|
||||||
|
|||||||
@@ -118,15 +118,14 @@ ResourcePlan& ResourcePlan::operator=(ResourcePlan&& other) noexcept {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Program::~Program() {
|
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) {
|
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* block: blocks) {
|
||||||
for (auto& inst: *block) {
|
for (auto& inst: *block) {
|
||||||
inst.Invalidate();
|
inst.DropArgs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,6 +168,8 @@ bool EquivalentSelectorDomain(const ResourcePlan& program,
|
|||||||
const DescriptorSource::IndirectDescriptor& right) {
|
const DescriptorSource::IndirectDescriptor& right) {
|
||||||
return left.selector == right.selector && left.signed_count == right.signed_count &&
|
return left.selector == right.selector && left.signed_count == right.signed_count &&
|
||||||
left.workgroup_axis == right.workgroup_axis &&
|
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.key_count, right.key_count) &&
|
||||||
EquivalentValue(program, left.selector_first, right.selector_first) &&
|
EquivalentValue(program, left.selector_first, right.selector_first) &&
|
||||||
EquivalentValue(program, left.selector_mask, right.selector_mask);
|
EquivalentValue(program, left.selector_mask, right.selector_mask);
|
||||||
|
|||||||
@@ -157,12 +157,15 @@ 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,
|
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)
|
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::CountedRows, a, b)
|
||||||
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::SelectorRead, a, b, c, d, e, f, g, h, i, j)
|
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::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)
|
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(DescriptorSource, a, b, c)
|
||||||
KYTY_IO_FIELDS(SrtRead, a, b)
|
KYTY_IO_FIELDS(SrtRead, a, b)
|
||||||
KYTY_IO_FIELDS(ResourceBlock, a, b, c, d)
|
KYTY_IO_FIELDS(ResourceBlock, a, b, c, d, e)
|
||||||
KYTY_IO_FIELDS(UniformFill, a, b, c, d, e, f)
|
KYTY_IO_FIELDS(UniformFill, a, b, c, d, e, f)
|
||||||
KYTY_IO_FIELDS(UniformFillPlan, a, b, c, d)
|
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::Buffer, a, b, c, d, e, f, g, h)
|
||||||
|
|||||||
@@ -251,6 +251,8 @@ enum class StageInputKind {
|
|||||||
LocalInvocationId,
|
LocalInvocationId,
|
||||||
LocalInvocationIndex,
|
LocalInvocationIndex,
|
||||||
GlobalInvocationId,
|
GlobalInvocationId,
|
||||||
|
BaseVertex,
|
||||||
|
BaseInstance,
|
||||||
Parameter,
|
Parameter,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -572,6 +574,13 @@ struct DescriptorSource {
|
|||||||
|
|
||||||
bool operator==(const CountedRows& other) const = default;
|
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 source = UINT32_MAX;
|
||||||
uint32_t stride = 0;
|
uint32_t stride = 0;
|
||||||
uint32_t offset = 0;
|
uint32_t offset = 0;
|
||||||
@@ -579,10 +588,12 @@ struct DescriptorSource {
|
|||||||
uint32_t bit_offset = 0;
|
uint32_t bit_offset = 0;
|
||||||
uint32_t bit_mask = UINT32_MAX;
|
uint32_t bit_mask = UINT32_MAX;
|
||||||
bool global = false;
|
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;
|
bool bitset = false;
|
||||||
std::optional<CountedRows> rows;
|
std::optional<CountedRows> rows;
|
||||||
std::vector<std::pair<uint32_t, uint32_t>> byte_ranges;
|
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;
|
bool operator==(const SelectorRead& other) const = default;
|
||||||
};
|
};
|
||||||
@@ -606,8 +617,12 @@ struct DescriptorSource {
|
|||||||
bool indexed = false;
|
bool indexed = false;
|
||||||
bool signed_count = false;
|
bool signed_count = false;
|
||||||
uint32_t workgroup_axis = UINT32_MAX;
|
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;
|
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_first;
|
||||||
Value selector_mask;
|
Value selector_mask;
|
||||||
std::vector<uint32_t> sources;
|
std::vector<uint32_t> sources;
|
||||||
@@ -634,6 +649,7 @@ struct ResourceBlock {
|
|||||||
Value condition;
|
Value condition;
|
||||||
std::vector<uint32_t> successors;
|
std::vector<uint32_t> successors;
|
||||||
std::vector<uint32_t> sources;
|
std::vector<uint32_t> sources;
|
||||||
|
std::vector<uint32_t> images;
|
||||||
std::vector<uint32_t> srt_reads;
|
std::vector<uint32_t> srt_reads;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,22 +25,6 @@ Value Value::F32(float value) {
|
|||||||
return Value(Type::F32, std::bit_cast<uint32_t>(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 {
|
Type Value::GetType() const {
|
||||||
if (IsPhi()) {
|
if (IsPhi()) {
|
||||||
return inst->Flags<Type>();
|
return inst->Flags<Type>();
|
||||||
@@ -56,19 +40,6 @@ Inst* Value::Instruction() const {
|
|||||||
return inst;
|
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 {
|
ScalarReg Value::ScalarRegister() const {
|
||||||
EXIT_IF(type != Type::ScalarReg);
|
EXIT_IF(type != Type::ScalarReg);
|
||||||
return scalar_reg;
|
return scalar_reg;
|
||||||
@@ -94,11 +65,6 @@ uint16_t Value::U16() const {
|
|||||||
return imm_u16;
|
return imm_u16;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Value::U32() const {
|
|
||||||
EXIT_IF(type != Type::U32);
|
|
||||||
return imm_u32;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t Value::U64() const {
|
uint64_t Value::U64() const {
|
||||||
EXIT_IF(type != Type::U64);
|
EXIT_IF(type != Type::U64);
|
||||||
return imm_u64;
|
return imm_u64;
|
||||||
@@ -152,10 +118,6 @@ Inst::~Inst() {
|
|||||||
ClearArgs();
|
ClearArgs();
|
||||||
}
|
}
|
||||||
|
|
||||||
ValueOpcode Inst::GetOpcode() const {
|
|
||||||
return opcode;
|
|
||||||
}
|
|
||||||
|
|
||||||
Type Inst::GetType() const {
|
Type Inst::GetType() const {
|
||||||
if (opcode == ValueOpcode::Phi) {
|
if (opcode == ValueOpcode::Phi) {
|
||||||
return static_cast<Type>(flags);
|
return static_cast<Type>(flags);
|
||||||
@@ -178,51 +140,25 @@ size_t Inst::UseCount() const {
|
|||||||
return uses.size();
|
return uses.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t Inst::NumArgs() const {
|
|
||||||
return num_args == PhiArity ? phi_args.size() : num_args;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t Inst::NumPhiBlocks() const {
|
size_t Inst::NumPhiBlocks() const {
|
||||||
return num_args == PhiArity ? phi_args.size() : 0;
|
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 {
|
Block* Inst::PhiBlock(size_t index) const {
|
||||||
EXIT_IF(opcode != ValueOpcode::Phi || index >= phi_args.size());
|
EXIT_IF(opcode != ValueOpcode::Phi || index >= phi_args.size());
|
||||||
return phi_args[index].first;
|
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) {
|
void Inst::SetParent(Block* block) {
|
||||||
parent = block;
|
parent = block;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Inst::SetArg(size_t index, Value value) {
|
void Inst::SetArg(size_t index, Value value) {
|
||||||
const auto old = Arg(index);
|
auto& arg = ArgSlot(*this, index);
|
||||||
if (auto* old_inst = old.TryInstruction(); old_inst != nullptr) {
|
if (auto* old_inst = arg.TryInstruction(); old_inst != nullptr) {
|
||||||
RemoveUse(old_inst, index);
|
RemoveUse(old_inst, index);
|
||||||
}
|
}
|
||||||
if (num_args <= InlineArity) {
|
arg = value;
|
||||||
fixed_args[index] = value;
|
|
||||||
} else if (num_args == PhiArity) {
|
|
||||||
phi_args[index].second = value;
|
|
||||||
} else {
|
|
||||||
large_args[index] = value;
|
|
||||||
}
|
|
||||||
if (auto* new_inst = value.TryInstruction(); new_inst != nullptr) {
|
if (auto* new_inst = value.TryInstruction(); new_inst != nullptr) {
|
||||||
AddUse(new_inst, index);
|
AddUse(new_inst, index);
|
||||||
}
|
}
|
||||||
@@ -238,11 +174,15 @@ void Inst::AddPhiOperand(Block* predecessor, Value value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Inst::ReplaceUsesWith(Value replacement, bool preserve) {
|
void Inst::ReplaceUsesWith(Value replacement, bool preserve) {
|
||||||
const auto old_uses = uses;
|
// Detach the arguments first (a phi can use itself), then move every use over in order.
|
||||||
for (const auto& use: old_uses) {
|
|
||||||
use.user->SetArg(use.operand, replacement);
|
|
||||||
}
|
|
||||||
Invalidate();
|
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) {
|
if (preserve) {
|
||||||
opcode = ValueOpcode::Identity;
|
opcode = ValueOpcode::Identity;
|
||||||
num_args = 1;
|
num_args = 1;
|
||||||
@@ -256,9 +196,6 @@ void Inst::Invalidate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Inst::AddUse(Inst* used, size_t operand) {
|
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});
|
used->uses.push_back({this, operand});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,6 +212,10 @@ void Inst::ClearArgs() {
|
|||||||
RemoveUse(value_inst, index);
|
RemoveUse(value_inst, index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DropArgs();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Inst::DropArgs() {
|
||||||
if (num_args == PhiArity) {
|
if (num_args == PhiArity) {
|
||||||
std::destroy_at(&phi_args);
|
std::destroy_at(&phi_args);
|
||||||
std::construct_at(&fixed_args);
|
std::construct_at(&fixed_args);
|
||||||
|
|||||||
@@ -33,14 +33,14 @@ public:
|
|||||||
static Value F16(uint16_t bits);
|
static Value F16(uint16_t bits);
|
||||||
static Value F32(float value);
|
static Value F32(float value);
|
||||||
|
|
||||||
[[nodiscard]] bool IsEmpty() const;
|
[[nodiscard]] bool IsEmpty() const { return type == Type::Void; }
|
||||||
[[nodiscard]] bool IsImmediate() const;
|
[[nodiscard]] bool IsImmediate() const { return type != Type::Opaque; }
|
||||||
[[nodiscard]] bool IsIdentity() const;
|
[[nodiscard]] bool IsIdentity() const;
|
||||||
[[nodiscard]] bool IsPhi() const;
|
[[nodiscard]] bool IsPhi() const;
|
||||||
[[nodiscard]] Type GetType() const;
|
[[nodiscard]] Type GetType() const;
|
||||||
|
|
||||||
[[nodiscard]] Inst* Instruction() const;
|
[[nodiscard]] Inst* Instruction() const;
|
||||||
[[nodiscard]] Inst* TryInstruction() const;
|
[[nodiscard]] Inst* TryInstruction() const { return type == Type::Opaque ? inst : nullptr; }
|
||||||
[[nodiscard]] Inst* ResolveInstruction() const;
|
[[nodiscard]] Inst* ResolveInstruction() const;
|
||||||
[[nodiscard]] Value Resolve() const;
|
[[nodiscard]] Value Resolve() const;
|
||||||
[[nodiscard]] ScalarReg ScalarRegister() const;
|
[[nodiscard]] ScalarReg ScalarRegister() const;
|
||||||
@@ -112,7 +112,7 @@ public:
|
|||||||
Inst(Inst&&) = delete;
|
Inst(Inst&&) = delete;
|
||||||
Inst& operator=(Inst&&) = delete;
|
Inst& operator=(Inst&&) = delete;
|
||||||
|
|
||||||
[[nodiscard]] ValueOpcode GetOpcode() const;
|
[[nodiscard]] ValueOpcode GetOpcode() const { return opcode; }
|
||||||
[[nodiscard]] Type GetType() const;
|
[[nodiscard]] Type GetType() const;
|
||||||
[[nodiscard]] bool MayHaveSideEffects() const;
|
[[nodiscard]] bool MayHaveSideEffects() const;
|
||||||
[[nodiscard]] bool HasUses() const;
|
[[nodiscard]] bool HasUses() const;
|
||||||
@@ -121,8 +121,8 @@ public:
|
|||||||
[[nodiscard]] size_t NumPhiBlocks() const;
|
[[nodiscard]] size_t NumPhiBlocks() const;
|
||||||
[[nodiscard]] Value Arg(size_t index) const;
|
[[nodiscard]] Value Arg(size_t index) const;
|
||||||
[[nodiscard]] Block* PhiBlock(size_t index) const;
|
[[nodiscard]] Block* PhiBlock(size_t index) const;
|
||||||
[[nodiscard]] Block* Parent() const;
|
[[nodiscard]] Block* Parent() const { return parent; }
|
||||||
[[nodiscard]] const std::vector<Use>& Uses() const;
|
[[nodiscard]] const std::vector<Use>& Uses() const { return uses; }
|
||||||
// SRT walker output index, assigned by the resource plan that owns this instruction.
|
// SRT walker output index, assigned by the resource plan that owns this instruction.
|
||||||
[[nodiscard]] uint32_t WalkOutput() const { return walk_output; }
|
[[nodiscard]] uint32_t WalkOutput() const { return walk_output; }
|
||||||
void SetWalkOutput(uint32_t index) const { walk_output = index; }
|
void SetWalkOutput(uint32_t index) const { walk_output = index; }
|
||||||
@@ -138,6 +138,8 @@ public:
|
|||||||
void AddPhiOperand(Block* predecessor, Value value);
|
void AddPhiOperand(Block* predecessor, Value value);
|
||||||
void ReplaceUsesWith(Value replacement, bool preserve = true);
|
void ReplaceUsesWith(Value replacement, bool preserve = true);
|
||||||
void Invalidate();
|
void Invalidate();
|
||||||
|
// Drops the arguments without updating their use lists, for teardown that destroys both sides.
|
||||||
|
void DropArgs();
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
requires(sizeof(T) <= sizeof(uint64_t) && std::is_trivially_copyable_v<T>)
|
requires(sizeof(T) <= sizeof(uint64_t) && std::is_trivially_copyable_v<T>)
|
||||||
@@ -157,6 +159,8 @@ public:
|
|||||||
private:
|
private:
|
||||||
friend void EliminateDeadCode(const std::vector<Block*>& blocks);
|
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 AddUse(Inst* used, size_t operand);
|
||||||
void RemoveUse(Inst* used, size_t operand);
|
void RemoveUse(Inst* used, size_t operand);
|
||||||
void ClearArgs();
|
void ClearArgs();
|
||||||
@@ -181,4 +185,44 @@ private:
|
|||||||
|
|
||||||
static_assert(sizeof(Inst) <= 120, "Inst operand and backend storage unintentionally increased");
|
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
|
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||||
@@ -123,6 +123,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
|
|||||||
case ValueOpcode::BufferAtomicISub32:
|
case ValueOpcode::BufferAtomicISub32:
|
||||||
case ValueOpcode::BufferAtomicSMin32:
|
case ValueOpcode::BufferAtomicSMin32:
|
||||||
case ValueOpcode::BufferAtomicUMin32:
|
case ValueOpcode::BufferAtomicUMin32:
|
||||||
|
case ValueOpcode::BufferAtomicUMin64:
|
||||||
case ValueOpcode::BufferAtomicSMax32:
|
case ValueOpcode::BufferAtomicSMax32:
|
||||||
case ValueOpcode::BufferAtomicUMax32:
|
case ValueOpcode::BufferAtomicUMax32:
|
||||||
case ValueOpcode::BufferAtomicAnd32:
|
case ValueOpcode::BufferAtomicAnd32:
|
||||||
@@ -140,6 +141,7 @@ uint32_t BufferComponentCount(ValueOpcode opcode) {
|
|||||||
switch (opcode) {
|
switch (opcode) {
|
||||||
case ValueOpcode::BufferAtomicSwap64:
|
case ValueOpcode::BufferAtomicSwap64:
|
||||||
case ValueOpcode::BufferAtomicIAdd64:
|
case ValueOpcode::BufferAtomicIAdd64:
|
||||||
|
case ValueOpcode::BufferAtomicUMin64:
|
||||||
case ValueOpcode::BufferAtomicAnd64:
|
case ValueOpcode::BufferAtomicAnd64:
|
||||||
case ValueOpcode::BufferAtomicOr64:
|
case ValueOpcode::BufferAtomicOr64:
|
||||||
case ValueOpcode::LoadBufferU32x2:
|
case ValueOpcode::LoadBufferU32x2:
|
||||||
|
|||||||
@@ -269,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(BufferAtomicISub32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||||
VALUE_OPCODE(BufferAtomicSMin32, 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(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(BufferAtomicSMax32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||||
VALUE_OPCODE(BufferAtomicUMax32, 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)
|
VALUE_OPCODE(BufferAtomicAnd32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||||
|
|||||||
@@ -331,6 +331,18 @@ bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::In
|
|||||||
keys.resize(count);
|
keys.resize(count);
|
||||||
return true;
|
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) {
|
if (selector->rows) {
|
||||||
const auto& rows = *selector->rows;
|
const auto& rows = *selector->rows;
|
||||||
DescriptorValue count_value;
|
DescriptorValue count_value;
|
||||||
@@ -377,19 +389,21 @@ bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::In
|
|||||||
}
|
}
|
||||||
} else if (selector->bitset) {
|
} else if (selector->bitset) {
|
||||||
ShaderBufferResource material;
|
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;
|
uint32_t first = 0, count = 0;
|
||||||
if (!DecodeBufferDescriptor(material_value, material) ||
|
if (!DecodeBufferDescriptor(material_value, material) ||
|
||||||
!WalkedValue(program, indirect.selector_first, first) ||
|
!WalkedValue(program, indirect.selector_first, first) ||
|
||||||
!EvaluateIndirectCount(program, indirect, runtime, count) || count != masks.size() * 32u ||
|
!EvaluateIndirectCount(program, indirect, runtime, count) || count != masks.size() * 32u ||
|
||||||
selector->stride != sizeof(masks) || selector->offset != 0u ||
|
selector->stride != masks.size_bytes() || selector->offset != 0u ||
|
||||||
material.Type() != 0u || material.Stride() != sizeof(masks) ||
|
material.Type() != 0u || material.Stride() != selector->stride ||
|
||||||
material.SwizzleEnabled() || material.AddTid() || material.OutOfBounds() != 0u ||
|
material.SwizzleEnabled() || material.AddTid() || material.OutOfBounds() != 0u ||
|
||||||
(material.Base48() & 3u) != 0u) return false;
|
(material.Base48() & 3u) != 0u) return false;
|
||||||
if (material.Format() != Prospero::BufferFormat::kInvalid) {
|
if (material.Format() != Prospero::BufferFormat::kInvalid) {
|
||||||
const auto words = material.GetSize() / sizeof(uint32_t);
|
const auto words = material.GetSize() / sizeof(uint32_t);
|
||||||
// Bitmap storage can grow with resolution while its candidate domain stays
|
// Bitmap storage can grow with resolution while its candidate domain stays at 32 keys
|
||||||
// at 128 keys. Bound scratch memory, preserving each coherent read's capture.
|
// 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()) {
|
for (uint64_t first_word = 0; first_word < words; first_word += keys.size()) {
|
||||||
keys.resize(std::min<uint64_t>(words - first_word, MaxIndirectDescriptorProbes));
|
keys.resize(std::min<uint64_t>(words - first_word, MaxIndirectDescriptorProbes));
|
||||||
if (!ReadScalarTable(material.Base48(), material.GetSize(), first_word * 4u,
|
if (!ReadScalarTable(material.Base48(), material.GetSize(), first_word * 4u,
|
||||||
@@ -445,12 +459,35 @@ bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::In
|
|||||||
uint64_t {first} + count > material.NumRecords())) return false;
|
uint64_t {first} + count > material.NumRecords())) return false;
|
||||||
const auto last = count != 0u ? uint64_t {first} + count - 1u : 0u;
|
const auto last = count != 0u ? uint64_t {first} + count - 1u : 0u;
|
||||||
if (last > UINT32_MAX) return false;
|
if (last > UINT32_MAX) return false;
|
||||||
|
// Out-of-bounds scalar reads return zero.
|
||||||
const auto end = count != 0u ? last * step + selector->offset + 4u : 0u;
|
const auto end = count != 0u ? last * step + selector->offset + 4u : 0u;
|
||||||
if (end > uint64_t {UINT32_MAX} + 1u ||
|
if (end > uint64_t {UINT32_MAX} + 1u) return false;
|
||||||
(indirect.workgroup_axis == UINT32_MAX && end > size))
|
const auto start = uint64_t {first} * step + selector->offset;
|
||||||
return false;
|
if (selector->ranks) {
|
||||||
if (!read_keys(base, size, uint64_t {first} * step + selector->offset, step, count))
|
// 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;
|
return false;
|
||||||
|
}
|
||||||
} else if (!indirect.selector_mask.IsEmpty()) {
|
} else if (!indirect.selector_mask.IsEmpty()) {
|
||||||
uint32_t mask = 0;
|
uint32_t mask = 0;
|
||||||
uint32_t count = 0;
|
uint32_t count = 0;
|
||||||
@@ -462,18 +499,8 @@ bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::In
|
|||||||
if (count < 32u) mask &= (1u << count) - 1u;
|
if (count < 32u) mask &= (1u << count) - 1u;
|
||||||
const auto material_base =
|
const auto material_base =
|
||||||
(static_cast<uint64_t>(material_value.dwords[1]) << 32u) | material_value.dwords[0];
|
(static_cast<uint64_t>(material_value.dwords[1]) << 32u) | material_value.dwords[0];
|
||||||
keys.reserve(std::popcount(mask));
|
if (!read_ranks(material_base, UINT64_MAX, selector->offset, selector->stride, mask))
|
||||||
while (mask != 0u) {
|
return false;
|
||||||
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;
|
|
||||||
}
|
|
||||||
} else if (selector->stride == 0u) {
|
} else if (selector->stride == 0u) {
|
||||||
ShaderBufferResource material;
|
ShaderBufferResource material;
|
||||||
if (!DecodeBufferDescriptor(material_value, material) ||
|
if (!DecodeBufferDescriptor(material_value, material) ||
|
||||||
@@ -503,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)
|
if (selector->bit_offset != 0u || selector->bit_mask != UINT32_MAX || selector->maximum != UINT32_MAX)
|
||||||
for (auto& key: keys)
|
for (auto& key: keys)
|
||||||
key = std::min((key >> selector->bit_offset) & selector->bit_mask, selector->maximum);
|
key = std::min((key >> selector->bit_offset) & selector->bit_mask, selector->maximum);
|
||||||
|
if (selector->excluded) std::erase(keys, *selector->excluded);
|
||||||
std::ranges::sort(keys);
|
std::ranges::sort(keys);
|
||||||
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
||||||
}
|
}
|
||||||
@@ -756,6 +784,12 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
|||||||
std::iota(keys.begin(), keys.end(), first_key);
|
std::iota(keys.begin(), keys.end(), first_key);
|
||||||
if (!keys.empty() && keys.back() < keys.front())
|
if (!keys.empty() && keys.back() < keys.front())
|
||||||
std::rotate(keys.begin(), keys.begin() + (UINT32_MAX - first_key + 1ull), keys.end());
|
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 {
|
} else {
|
||||||
if (!indirect.selector_mask.IsEmpty() && table_value.dword_count != 2u) return false;
|
if (!indirect.selector_mask.IsEmpty() && table_value.dword_count != 2u) return false;
|
||||||
if (!selector_keys->resolved) {
|
if (!selector_keys->resolved) {
|
||||||
@@ -1041,6 +1075,26 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
|||||||
return dimension == Decoder::ImageDimension::Dim1D ||
|
return dimension == Decoder::ImageDimension::Dim1D ||
|
||||||
dimension == Decoder::ImageDimension::Dim2D;
|
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++) {
|
for (uint32_t candidate = 0; candidate < specialization.images.size(); candidate++) {
|
||||||
auto& image = specialization.images[candidate];
|
auto& image = specialization.images[candidate];
|
||||||
if (image.indirect_root != root_index) {
|
if (image.indirect_root != root_index) {
|
||||||
@@ -1060,8 +1114,10 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
|||||||
base.dimension == Decoder::ImageDimension::Dim2D &&
|
base.dimension == Decoder::ImageDimension::Dim2D &&
|
||||||
!image.cube && !image_class.cube && is_1d_or_2d(image.dimension) &&
|
!image.cube && !image_class.cube && is_1d_or_2d(image.dimension) &&
|
||||||
is_1d_or_2d(image_class.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)) &&
|
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 ||
|
image.conversion_format != image_class.conversion_format ||
|
||||||
(!storage_swizzle && image.shader_swizzle != image_class.shader_swizzle)) {
|
(!storage_swizzle && image.shader_swizzle != image_class.shader_swizzle)) {
|
||||||
return SpecializationFail(
|
return SpecializationFail(
|
||||||
@@ -1164,14 +1220,16 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, co
|
|||||||
block.sources.push_back(program.info.buffers.at(memory.resource).source);
|
block.sources.push_back(program.info.buffers.at(memory.resource).source);
|
||||||
} else {
|
} else {
|
||||||
block.sources.push_back(program.info.images.at(memory.resource).source);
|
block.sources.push_back(program.info.images.at(memory.resource).source);
|
||||||
|
block.images.push_back(memory.resource);
|
||||||
if (image.needs_sampler) {
|
if (image.needs_sampler) {
|
||||||
block.sources.push_back(program.info.samplers.at(memory.sampler).source);
|
block.sources.push_back(program.info.samplers.at(memory.sampler).source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::ranges::sort(block.sources);
|
for (auto* list: {&block.sources, &block.images}) {
|
||||||
block.sources.erase(std::unique(block.sources.begin(), block.sources.end()),
|
std::ranges::sort(*list);
|
||||||
block.sources.end());
|
list->erase(std::unique(list->begin(), list->end()), list->end());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (auto& block: blocks) block.condition = predicate(block.condition);
|
for (auto& block: blocks) block.condition = predicate(block.condition);
|
||||||
if (std::ranges::none_of(
|
if (std::ranges::none_of(
|
||||||
@@ -1645,7 +1703,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
|||||||
}
|
}
|
||||||
if (source->indirect_descriptor.has_value()) {
|
if (source->indirect_descriptor.has_value()) {
|
||||||
snapshot.images[i] = {.dword_count = 8u};
|
snapshot.images[i] = {.dword_count = 8u};
|
||||||
if (!WalkedActive(program, image.source)) {
|
if (!WalkedActive(program, image.source) || !WalkedImageActive(program, i)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const auto& indirect = *source->indirect_descriptor;
|
const auto& indirect = *source->indirect_descriptor;
|
||||||
@@ -1665,7 +1723,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
|||||||
if (!evaluate(image.source, snapshot.images[i])) {
|
if (!evaluate(image.source, snapshot.images[i])) {
|
||||||
return false;
|
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);
|
snapshot.images[i].dwords.fill(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1711,8 +1771,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t ImageHeapKind(const ImageResource& root, DescriptorValue& value) {
|
uint32_t ImageHeapKind(const ImageResource& root, DescriptorValue& value) {
|
||||||
// Mirrors BuildResourceSpecialization for sampled roots: entries it would reject, and
|
// Mirrors BuildResourceSpecialization for sampled roots: entries it would reject, entries of
|
||||||
// entries that need shader-side conversion, read as null instead of failing the draw.
|
// 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 format = static_cast<Prospero::BufferFormat>((value.dwords[1] >> 20u) & 0x1ffu);
|
||||||
const auto tile = static_cast<Prospero::TileMode>((value.dwords[3] >> 20u) & 0x1fu);
|
const auto tile = static_cast<Prospero::TileMode>((value.dwords[3] >> 20u) & 0x1fu);
|
||||||
const auto numeric_class =
|
const auto numeric_class =
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <bit>
|
#include <bit>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
@@ -1014,7 +1016,9 @@ private:
|
|||||||
(!shared_image_heap && (a.table_offset != b.table_offset ||
|
(!shared_image_heap && (a.table_offset != b.table_offset ||
|
||||||
a.table_immediate != b.table_immediate ||
|
a.table_immediate != b.table_immediate ||
|
||||||
a.table_stride != b.table_stride)) || a.indexed != b.indexed ||
|
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 ||
|
a.signed_count != b.signed_count ||
|
||||||
!EquivalentValue(m_program, a.key_count, b.key_count) ||
|
!EquivalentValue(m_program, a.key_count, b.key_count) ||
|
||||||
a.selector_first.IsEmpty() != b.selector_first.IsEmpty() ||
|
a.selector_first.IsEmpty() != b.selector_first.IsEmpty() ||
|
||||||
@@ -1136,31 +1140,63 @@ private:
|
|||||||
LaneQuantifier lanes = LaneQuantifier::All;
|
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 {
|
bool NonzeroOnEntry(Value value, const Block* block) const {
|
||||||
if (block == nullptr) return false;
|
if (block == nullptr) return false;
|
||||||
bool witnessed = false;
|
bool witnessed = false;
|
||||||
const bool guarded = GuardedOnEntry(block, [](const Block* current) {
|
const bool guarded = GuardedOnEntry(block, [](const Block* current) {
|
||||||
return current->ImmPredecessors().size() != 1u;
|
return current->ImmPredecessors().size() != 1u;
|
||||||
}, [&](const EdgePredicate& edge) {
|
}, [&](const EdgePredicate& edge) {
|
||||||
if (edge.lanes != LaneQuantifier::All) return false;
|
return ExcludedBy(edge, value) == 0u && (witnessed = true);
|
||||||
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 guarded && witnessed;
|
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();
|
const auto* builtin = key.Resolve().TryInstruction();
|
||||||
uint32_t axis = UINT32_MAX;
|
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 ||
|
if (m_program.stage != ShaderType::Compute || builtin == nullptr ||
|
||||||
builtin->GetOpcode() != ValueOpcode::GetBuiltin || builtin->NumArgs() != 2u ||
|
builtin->GetOpcode() != ValueOpcode::GetBuiltin || builtin->NumArgs() != 2u ||
|
||||||
builtin->Arg(0) != Value(static_cast<uint32_t>(StageInputKind::WorkgroupId)) ||
|
builtin->Arg(0) != Value(static_cast<uint32_t>(StageInputKind::WorkgroupId)) ||
|
||||||
@@ -1168,6 +1204,27 @@ private:
|
|||||||
return axis;
|
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 {
|
bool MatchTableOffset(Value value, Value& key, uint32_t& offset, uint32_t& stride) const {
|
||||||
offset = 0;
|
offset = 0;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -1672,6 +1729,8 @@ private:
|
|||||||
uint32_t initial_arm;
|
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 {
|
std::optional<Induction> MatchInduction(Value key, const auto& accepts_step) const {
|
||||||
key = key.Resolve();
|
key = key.Resolve();
|
||||||
const auto* phi = key.TryInstruction();
|
const auto* phi = key.TryInstruction();
|
||||||
@@ -1733,8 +1792,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t PostTestLoopMaximum(Value key) const {
|
uint32_t PostTestLoopMaximum(Value key) const {
|
||||||
const auto induction = MatchInduction(key,
|
const auto induction = MatchInduction(key, ZeroBased);
|
||||||
[](uint32_t initial, uint32_t step) { return initial == 0u && step == 1u; });
|
|
||||||
if (!induction) return UINT32_MAX;
|
if (!induction) return UINT32_MAX;
|
||||||
const auto [phi, step, initial_arm] = *induction;
|
const auto [phi, step, initial_arm] = *induction;
|
||||||
const auto* incoming = phi->PhiBlock(initial_arm ^ 1u);
|
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,
|
const Inst* BoundedLoop(Value key, const Block* use, const auto& accepts_bound,
|
||||||
LaneQuantifier lanes = LaneQuantifier::Any) const {
|
LaneQuantifier lanes = LaneQuantifier::Any) const {
|
||||||
return BoundedInduction(key, use,
|
return BoundedInduction(key, use, ZeroBased,
|
||||||
[](uint32_t initial, uint32_t step) { return initial == 0u && step == 1u; },
|
|
||||||
[&](const Inst& compare, uint32_t initial) {
|
[&](const Inst& compare, uint32_t initial) {
|
||||||
return (compare.GetOpcode() == ValueOpcode::SLessThan32 ||
|
return (compare.GetOpcode() == ValueOpcode::SLessThan32 ||
|
||||||
compare.GetOpcode() == ValueOpcode::ULessThan32) &&
|
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 ActiveValue(Value value, Value guard) const {
|
||||||
value = value.Resolve();
|
value = value.Resolve();
|
||||||
while (const auto* select = value.TryInstruction()) {
|
while (const auto* select = value.TryInstruction()) {
|
||||||
@@ -1829,6 +1909,10 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Dominates(const Block* initial, const Block* use, Value witness = {}) const {
|
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};
|
std::vector<const Block*> pending {use};
|
||||||
for (size_t index = 0; index < pending.size(); ++index) {
|
for (size_t index = 0; index < pending.size(); ++index) {
|
||||||
const auto* current = pending[index];
|
const auto* current = pending[index];
|
||||||
@@ -1837,9 +1921,9 @@ private:
|
|||||||
for (const auto* previous: current->ImmPredecessors()) {
|
for (const auto* previous: current->ImmPredecessors()) {
|
||||||
if (!witness.IsEmpty()) {
|
if (!witness.IsEmpty()) {
|
||||||
const auto edge = ConditionalEdge(previous, current);
|
const auto edge = ConditionalEdge(previous, current);
|
||||||
// The proven local-zero invocation makes an all-inactive bypass impossible.
|
if (edge && edge->lanes == LaneQuantifier::All &&
|
||||||
if (edge && edge->lanes == LaneQuantifier::All && !edge->positive &&
|
(edge->positive ? nonzero && Implies(edge->condition, negated->Arg(0))
|
||||||
Implies(witness, edge->condition)) continue;
|
: Implies(witness, edge->condition))) continue;
|
||||||
}
|
}
|
||||||
if (std::ranges::find(pending, previous) == pending.end()) pending.push_back(previous);
|
if (std::ranges::find(pending, previous) == pending.end()) pending.push_back(previous);
|
||||||
}
|
}
|
||||||
@@ -1948,7 +2032,10 @@ private:
|
|||||||
|
|
||||||
bool LocalZeroWitness(Value guard) const {
|
bool LocalZeroWitness(Value guard) const {
|
||||||
const auto* equal = SimplifyGuard(guard).TryInstruction();
|
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) {
|
for (uint32_t arg = 0; arg < 2u; ++arg) {
|
||||||
if (equal->Arg(arg).Resolve() != Value(0u)) continue;
|
if (equal->Arg(arg).Resolve() != Value(0u)) continue;
|
||||||
Value induction;
|
Value induction;
|
||||||
@@ -1982,6 +2069,10 @@ private:
|
|||||||
EquivalentValue(m_program, test->Arg(0), value)) return test->Arg(1);
|
EquivalentValue(m_program, test->Arg(0), value)) return test->Arg(1);
|
||||||
if ((positive ? op == ValueOpcode::UGreaterThan32 : op == ValueOpcode::ULessThanEqual32) &&
|
if ((positive ? op == ValueOpcode::UGreaterThan32 : op == ValueOpcode::ULessThanEqual32) &&
|
||||||
EquivalentValue(m_program, test->Arg(1), value)) return test->Arg(0);
|
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();
|
const auto* scan = value.Resolve().TryInstruction();
|
||||||
if (finite_bitscan && scan != nullptr && scan->GetOpcode() == ValueOpcode::FindILsb32 &&
|
if (finite_bitscan && scan != nullptr && scan->GetOpcode() == ValueOpcode::FindILsb32 &&
|
||||||
(positive ? op == ValueOpcode::INotEqual32 : op == ValueOpcode::IEqual32)) {
|
(positive ? op == ValueOpcode::INotEqual32 : op == ValueOpcode::IEqual32)) {
|
||||||
@@ -2056,7 +2147,7 @@ private:
|
|||||||
const auto* read = ActiveValue(value, active).TryInstruction();
|
const auto* read = ActiveValue(value, active).TryInstruction();
|
||||||
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadSharedU32 ||
|
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadSharedU32 ||
|
||||||
m_program.stage != ShaderType::Compute ||
|
m_program.stage != ShaderType::Compute ||
|
||||||
read->Arg(1).Resolve() != Value(true)) return {};
|
!Implies(active, read->Arg(1))) return {};
|
||||||
uint64_t group_size = 1u;
|
uint64_t group_size = 1u;
|
||||||
for (const auto dimension: m_input.compute->threads_num) {
|
for (const auto dimension: m_input.compute->threads_num) {
|
||||||
if (dimension == 0u || dimension > m_input.compute->wave_size) return {};
|
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 auto* limit = BoundedLoop(read_offset.index, read->Parent(),
|
||||||
[](const Inst& compare, uint32_t) { return compare.GetOpcode() == ValueOpcode::ULessThan32; });
|
[](const Inst& compare, uint32_t) { return compare.GetOpcode() == ValueOpcode::ULessThan32; });
|
||||||
if (limit == nullptr) return {};
|
if (limit == nullptr) return {};
|
||||||
|
auto counter_active = active;
|
||||||
const auto* counter_read = ActiveValue(limit->Arg(1), active).TryInstruction();
|
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;
|
uint32_t consumed_counter = 0;
|
||||||
if (counter_read == nullptr || counter_read->GetOpcode() != ValueOpcode::LoadSharedU32 ||
|
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 {};
|
!constant_address(*counter_read, consumed_counter)) return {};
|
||||||
|
|
||||||
struct Initializer { const Inst* inst; uint32_t address; uint32_t words; };
|
struct Initializer { const Inst* inst; uint32_t address; uint32_t words; };
|
||||||
@@ -2304,25 +2401,23 @@ private:
|
|||||||
((offset | stride | memory->offset) & 3u) == 0u &&
|
((offset | stride | memory->offset) & 3u) == 0u &&
|
||||||
uint64_t {offset} + memory->offset <= UINT32_MAX;
|
uint64_t {offset} + memory->offset <= UINT32_MAX;
|
||||||
if (affine) {
|
if (affine) {
|
||||||
workgroup_axis = WorkgroupAxis(index);
|
workgroup_axis = WorkgroupAxis(index, &first);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const auto* bound = BoundedLoop(index, read->Parent());
|
const auto* bound = BoundedLoop(index, read->Parent());
|
||||||
if (bound != nullptr && bound->GetOpcode() == ValueOpcode::ULessThan32) {
|
if (bound != nullptr && bound->GetOpcode() == ValueOpcode::ULessThan32) {
|
||||||
count = bound->Arg(1);
|
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.
|
// Raw pointers have no V# bounds for enumerating an unbounded selector domain.
|
||||||
if (memory->kind == ResourceKind::ScalarAddress && count.IsEmpty() &&
|
if (memory->kind == ResourceKind::ScalarAddress && count.IsEmpty() &&
|
||||||
@@ -2498,7 +2593,8 @@ private:
|
|||||||
// Each equation is an inductive subset obligation. Phis require every incoming
|
// Each equation is an inductive subset obligation. Phis require every incoming
|
||||||
// value; AND needs either operand. Grounding rejects cycles without an initial
|
// value; AND needs either operand. Grounding rejects cycles without an initial
|
||||||
// value, then the greatest fixed point checks every initial and carried edge.
|
// 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 {
|
struct State {
|
||||||
Value value;
|
Value value;
|
||||||
Value guard;
|
Value guard;
|
||||||
@@ -2510,7 +2606,10 @@ private:
|
|||||||
bool all = true;
|
bool all = true;
|
||||||
std::vector<size_t> terms;
|
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<State> states(2);
|
||||||
std::vector<Equation> equations(2);
|
std::vector<Equation> equations(2);
|
||||||
const auto add = [&](State state) {
|
const auto add = [&](State state) {
|
||||||
@@ -2521,9 +2620,9 @@ private:
|
|||||||
equations.emplace_back();
|
equations.emplace_back();
|
||||||
return states.size() - 1u;
|
return states.size() - 1u;
|
||||||
};
|
};
|
||||||
std::array<size_t, 5> roots {add({.value = active})};
|
std::vector<size_t> roots {add({.value = active})};
|
||||||
for (uint32_t component = 0; component < 4u; ++component)
|
for (uint32_t component = 0; component < words; ++component)
|
||||||
roots[component + 1u] = add({mask, active, component, component});
|
roots.push_back(add({mask, active, component, component}));
|
||||||
for (size_t index = 2; index < states.size(); ++index) {
|
for (size_t index = 2; index < states.size(); ++index) {
|
||||||
const auto state = states[index];
|
const auto state = states[index];
|
||||||
const auto* inst = state.value.TryInstruction();
|
const auto* inst = state.value.TryInstruction();
|
||||||
@@ -2573,11 +2672,14 @@ private:
|
|||||||
switch (inst->GetOpcode()) {
|
switch (inst->GetOpcode()) {
|
||||||
case ValueOpcode::CompositeExtractU32x4: {
|
case ValueOpcode::CompositeExtractU32x4: {
|
||||||
uint32_t component;
|
uint32_t component;
|
||||||
if (inst->Arg(0).Resolve().TryInstruction() == &read &&
|
if (inst->Arg(0).Resolve().TryInstruction() == reads[state.component / 4u] &&
|
||||||
ImmediateU32(inst->Arg(1), component) && component == state.component)
|
ImmediateU32(inst->Arg(1), component) && component == state.component % 4u)
|
||||||
equation.terms = {1u};
|
equation.terms = {1u};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case ValueOpcode::ReadConstBuffer:
|
||||||
|
if (scalar && inst == reads[state.component]) equation.terms = {1u};
|
||||||
|
break;
|
||||||
case ValueOpcode::Phi:
|
case ValueOpcode::Phi:
|
||||||
// The predecessor may be an earlier outer iteration. Establish
|
// The predecessor may be an earlier outer iteration. Establish
|
||||||
// this register's invariant without the current word or EXEC.
|
// this register's invariant without the current word or EXEC.
|
||||||
@@ -2598,17 +2700,17 @@ private:
|
|||||||
const auto* masked = key.TryInstruction();
|
const auto* masked = key.TryInstruction();
|
||||||
uint32_t bound = 0;
|
uint32_t bound = 0;
|
||||||
if (masked != nullptr && masked->GetOpcode() == ValueOpcode::BitwiseAnd32 &&
|
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) {
|
for (uint32_t side = 0; side < 2u; ++side) {
|
||||||
uint32_t bits;
|
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();
|
key = masked->Arg(side ^ 1u).Resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return EquivalentValue(m_program, key, word);
|
return EquivalentValue(m_program, key, word);
|
||||||
});
|
});
|
||||||
uint32_t literal;
|
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));
|
equation.terms.push_back(term(inst->Arg(1), inst->Arg(0), selected_word));
|
||||||
}
|
}
|
||||||
if (!selected || !*selected)
|
if (!selected || !*selected)
|
||||||
@@ -2646,7 +2748,53 @@ private:
|
|||||||
return std::ranges::all_of(roots, [&](size_t root) { return valid[root]; });
|
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) {
|
DescriptorSource::IndirectDescriptor& indirect) {
|
||||||
const auto selected_value = [&](Value value) {
|
const auto selected_value = [&](Value value) {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -2670,7 +2818,7 @@ private:
|
|||||||
}
|
}
|
||||||
const auto* word_inst = word.TryInstruction();
|
const auto* word_inst = word.TryInstruction();
|
||||||
if (word_inst == nullptr || std::ranges::none_of(bounds, [&](const auto& bound) {
|
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;
|
})) return false;
|
||||||
// FFBH's leading-zero count is inverted again to recover the set-bit rank.
|
// FFBH's leading-zero count is inverted again to recover the set-bit rank.
|
||||||
for (uint32_t inverse = 0; inverse < 2u; ++inverse) {
|
for (uint32_t inverse = 0; inverse < 2u; ++inverse) {
|
||||||
@@ -2682,21 +2830,34 @@ private:
|
|||||||
const auto* scan = bit.TryInstruction();
|
const auto* scan = bit.TryInstruction();
|
||||||
if (scan == nullptr || scan->GetOpcode() != ValueOpcode::FindUMsb32 ||
|
if (scan == nullptr || scan->GetOpcode() != ValueOpcode::FindUMsb32 ||
|
||||||
!ImpliesNonzero(active, scan->Arg(0))) return false;
|
!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* block: m_program.blocks) {
|
||||||
for (const auto& read: *block) {
|
for (const auto& read: *block) {
|
||||||
if (read.GetOpcode() != ValueOpcode::LoadBufferU32x4 || InCycle(block)) continue;
|
if (!bitmap_load(read, 0u)) continue;
|
||||||
const auto index = read.Flags<MemoryFlags>().index;
|
// An eight-word record continues in a second four-word load of the same buffer and EXEC.
|
||||||
if (index >= m_program.memory_info.size()) continue;
|
std::array<const Inst*, 2> reads {&read};
|
||||||
const auto& memory = m_program.memory_info[index];
|
if (words == 8u)
|
||||||
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
|
for (const auto* other: m_program.blocks)
|
||||||
!memory.idxen || memory.offen || memory.offset != 0u || memory.data_bits != 32u ||
|
for (const auto& load: *other)
|
||||||
memory.data_dwords != 4u || read.Arg(2).Resolve() != Value(0u) ||
|
if (reads[1] == nullptr && bitmap_load(load, 16u) &&
|
||||||
read.Arg(3).Resolve() != Value(0u) ||
|
EquivalentValue(m_program, load.Arg(0), read.Arg(0)) &&
|
||||||
!BitsetMaskSubset(scan->Arg(0), active, word, read)) continue;
|
EquivalentValue(m_program, load.Arg(4), read.Arg(4))) reads[1] = &load;
|
||||||
|
const auto parts = std::span(reads).first(words / 4u);
|
||||||
DescriptorSource source;
|
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 {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2726,7 +2887,8 @@ private:
|
|||||||
first.SetArg(1, Value(maximum));
|
first.SetArg(1, Value(maximum));
|
||||||
indirect.selector_first = Value(&first);
|
indirect.selector_first = Value(&first);
|
||||||
indirect.key_count = Value(maximum + 1u);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2993,6 +3155,47 @@ private:
|
|||||||
return true;
|
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,
|
bool TryMakeTableDescriptor(Inst& handle, const DescriptorSource& descriptor,
|
||||||
IndirectDescriptorPlan& plan) {
|
IndirectDescriptorPlan& plan) {
|
||||||
const bool sampler = handle.GetOpcode() == ValueOpcode::GetSamplerResource;
|
const bool sampler = handle.GetOpcode() == ValueOpcode::GetSamplerResource;
|
||||||
@@ -3019,6 +3222,7 @@ private:
|
|||||||
} else if (!plan.table_indexed &&
|
} else if (!plan.table_indexed &&
|
||||||
MatchScalarMaterialKey(key, indirect, material_source,
|
MatchScalarMaterialKey(key, indirect, material_source,
|
||||||
!sampler && table_source.dword_count == 4u, sampler)) {
|
!sampler && table_source.dword_count == 4u, sampler)) {
|
||||||
|
indirect.selector->excluded = ExcludedKeyOnEntry(key, handle.Parent());
|
||||||
// Keep live descriptor payloads, but project descriptor-only SMEM onto
|
// Keep live descriptor payloads, but project descriptor-only SMEM onto
|
||||||
// the GPU key using the same plan as other indirect image reads.
|
// the GPU key using the same plan as other indirect image reads.
|
||||||
plan.retain_reads |= table_source.dword_count == 4u;
|
plan.retain_reads |= table_source.dword_count == 4u;
|
||||||
@@ -3080,36 +3284,42 @@ private:
|
|||||||
if (indirect.key_count.IsEmpty() &&
|
if (indirect.key_count.IsEmpty() &&
|
||||||
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, false) &&
|
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, false) &&
|
||||||
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, true) &&
|
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, true) &&
|
||||||
!MatchReversedImageRange(key, indirect)) {
|
!MatchReversedImageRange(key, indirect) && !MatchWorkgroupKey(key, indirect)) {
|
||||||
const auto maximum = SelectorMaximum(key);
|
auto maximum = SelectorMaximum(key);
|
||||||
if (maximum >= MaxIndirectDescriptorProbes) return false;
|
if (maximum >= MaxIndirectDescriptorProbes) {
|
||||||
indirect.key_count = Value(maximum + 1u);
|
// 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;
|
if ((table_offset & 3u) != 0u) return false;
|
||||||
} else {
|
} else {
|
||||||
// Keep the GPU selector and descriptor reads, and preserve the proven scalar
|
// Keep the GPU selector and descriptor reads, and preserve the proven scalar
|
||||||
// offset domain within the bounded image table.
|
// offset domain within the bounded image table.
|
||||||
if (sampler || plan.split_offsets) return false;
|
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;
|
bool shared_selector = false;
|
||||||
if (!plan.table_indexed) {
|
if (!plan.table_indexed) {
|
||||||
for (const auto& owner: m_indirect_descriptors) {
|
for (const auto& owner: m_indirect_descriptors) {
|
||||||
const auto& selected = *m_sources[owner.source].indirect_descriptor;
|
const auto& selected = *m_sources[owner.source].indirect_descriptor;
|
||||||
if (!selected.selector || !EquivalentValue(m_program, key, owner.key) ||
|
if (!selected.selector || !EquivalentValue(m_program, key, owner.key) ||
|
||||||
!std::ranges::all_of(handle.Uses(), [&](const Use& target) {
|
!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() &&
|
return !target_active.IsEmpty() &&
|
||||||
std::ranges::any_of(owner.handle->Uses(), [&](const Use& witness) {
|
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);
|
owner.handle->GetOpcode() == ValueOpcode::GetSamplerResource);
|
||||||
return !owner_active.IsEmpty() && AvailableBefore(*witness.user, *target.user) &&
|
return !owner_active.IsEmpty() && AvailableBefore(*witness.user, *target.user) &&
|
||||||
Implies(target_active, owner_active);
|
Implies(target_active, owner_active);
|
||||||
@@ -3430,6 +3640,42 @@ private:
|
|||||||
return result;
|
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,
|
uint32_t SelectorMaximum(Value value, SelectorBounds bounds = {}, uint32_t depth = 0,
|
||||||
Value guard = {}) const {
|
Value guard = {}) const {
|
||||||
if (depth > 32u) return UINT32_MAX;
|
if (depth > 32u) return UINT32_MAX;
|
||||||
@@ -3445,9 +3691,13 @@ private:
|
|||||||
switch (inst->GetOpcode()) {
|
switch (inst->GetOpcode()) {
|
||||||
case ValueOpcode::ReadFirstLane:
|
case ValueOpcode::ReadFirstLane:
|
||||||
return SelectedSelectorDomain(*inst, guard).maximum;
|
return SelectedSelectorDomain(*inst, guard).maximum;
|
||||||
case ValueOpcode::ReadLane:
|
case ValueOpcode::ReadLane: {
|
||||||
// ReadLane ignores EXEC; only context-free bounds on every source lane apply.
|
// ReadLane ignores EXEC; only context-free bounds on every source lane apply,
|
||||||
return PossibleU32Bits(inst->Arg(0));
|
// 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: {
|
case ValueOpcode::Phi: {
|
||||||
const auto initial = InitialCandidateMask(value);
|
const auto initial = InitialCandidateMask(value);
|
||||||
if (!initial.IsEmpty()) return SelectorMaximum(initial, bounds, depth + 1u, guard);
|
if (!initial.IsEmpty()) return SelectorMaximum(initial, bounds, depth + 1u, guard);
|
||||||
@@ -3474,6 +3724,18 @@ private:
|
|||||||
NonzeroOnEntry(inst->Arg(0), inst->Parent()) ? 31u : UINT32_MAX;
|
NonzeroOnEntry(inst->Arg(0), inst->Parent()) ? 31u : UINT32_MAX;
|
||||||
case ValueOpcode::UMin32:
|
case ValueOpcode::UMin32:
|
||||||
case ValueOpcode::BitwiseAnd32: return std::min(maximum(0), maximum(1));
|
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:
|
case ValueOpcode::SelectU32:
|
||||||
if (!guard.IsEmpty() && Implies(guard, inst->Arg(0))) return maximum(1);
|
if (!guard.IsEmpty() && Implies(guard, inst->Arg(0))) return maximum(1);
|
||||||
return SelectorPredicateFalse(inst->Arg(0), bounds, depth + 1u, guard)
|
return SelectorPredicateFalse(inst->Arg(0), bounds, depth + 1u, guard)
|
||||||
|
|||||||
@@ -175,6 +175,8 @@ void AddBuiltinInput(ShaderInfo& info, StageInputKind kind) {
|
|||||||
case StageInputKind::GlobalInvocationId:
|
case StageInputKind::GlobalInvocationId:
|
||||||
AddInput(info, kind, 0, 3, "gl_GlobalInvocationID");
|
AddInput(info, kind, 0, 3, "gl_GlobalInvocationID");
|
||||||
break;
|
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::PackedAncillary:
|
||||||
case StageInputKind::Parameter: break;
|
case StageInputKind::Parameter: break;
|
||||||
}
|
}
|
||||||
@@ -444,6 +446,8 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
|
|||||||
case StageInputKind::InstanceIndex:
|
case StageInputKind::InstanceIndex:
|
||||||
case StageInputKind::FrontFacing:
|
case StageInputKind::FrontFacing:
|
||||||
case StageInputKind::LocalInvocationIndex:
|
case StageInputKind::LocalInvocationIndex:
|
||||||
|
case StageInputKind::BaseVertex:
|
||||||
|
case StageInputKind::BaseInstance:
|
||||||
if (component != 0u) {
|
if (component != 0u) {
|
||||||
return Fail("typed scalar builtin component is out of range");
|
return Fail("typed scalar builtin component is out of range");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
|
|||||||
case ValueOpcode::BitCastU32F32:
|
case ValueOpcode::BitCastU32F32:
|
||||||
case ValueOpcode::BitCastF32U32:
|
case ValueOpcode::BitCastF32U32:
|
||||||
case ValueOpcode::ConvertU32F32:
|
case ValueOpcode::ConvertU32F32:
|
||||||
|
case ValueOpcode::ConvertS32F32:
|
||||||
case ValueOpcode::ConvertF32U32:
|
case ValueOpcode::ConvertF32U32:
|
||||||
case ValueOpcode::CompositeConstructU64:
|
case ValueOpcode::CompositeConstructU64:
|
||||||
case ValueOpcode::CompositeExtractU64:
|
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
|
// 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
|
// activity byte per source, then per image, then one qword per walked value (all ones when
|
||||||
// walker's value slots, captured addresses, reachability and states follow.
|
// unknown). The walker's value slots, captured addresses, reachability and states follow.
|
||||||
constexpr uint32_t DescriptorStride = 9u * sizeof(uint32_t);
|
constexpr uint32_t DescriptorStride = 9u * sizeof(uint32_t);
|
||||||
|
|
||||||
uint32_t ActiveOffset(const ResourcePlan& plan) {
|
uint32_t ActiveOffset(const ResourcePlan& plan) {
|
||||||
@@ -156,7 +157,8 @@ uint32_t ActiveOffset(const ResourcePlan& plan) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t OutputOffset(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) {
|
const uint8_t* Scratch(const ResourcePlan& plan) {
|
||||||
@@ -1038,6 +1040,23 @@ void WalkerCompiler::EmitStrict(const Node& node) {
|
|||||||
c.L(done);
|
c.L(done);
|
||||||
break;
|
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:
|
case ValueOpcode::FPMul32:
|
||||||
c.movd(c.xmm0, c.eax);
|
c.movd(c.xmm0, c.eax);
|
||||||
c.movd(c.xmm1, c.ecx);
|
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);
|
for (const auto slot: blocks[block].srt_reads) flat(slot);
|
||||||
c.L(skip);
|
c.L(skip);
|
||||||
}
|
}
|
||||||
// A source no block lists is always active.
|
// A source or image no block lists is always active.
|
||||||
for (uint32_t source = 0; source < sources.size(); ++source) {
|
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;
|
bool listed = false;
|
||||||
for (uint32_t block = 0; block < blocks.size(); ++block) {
|
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) {
|
if (listed) {
|
||||||
c.or_(c.al, c.byte[c.rdx + m_reach + block]);
|
c.or_(c.al, c.byte[c.rdx + m_reach + block]);
|
||||||
} else {
|
} else {
|
||||||
@@ -1284,7 +1306,7 @@ void WalkerCompiler::Compile(std::span<const Value> values) {
|
|||||||
}
|
}
|
||||||
listed = true;
|
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) {
|
if (listed) {
|
||||||
c.mov(active, c.al);
|
c.mov(active, c.al);
|
||||||
} else {
|
} else {
|
||||||
@@ -1391,6 +1413,10 @@ bool WalkedActive(const ResourcePlan& plan, uint32_t source) {
|
|||||||
return Scratch(plan)[ActiveOffset(plan) + source] != 0u;
|
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) {
|
bool ValidateRuntimeValue(const ResourcePlan& program, Value value, RuntimeValueType type) {
|
||||||
return RuntimeValidator(program, type).Run(value);
|
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 WalkedValue(const ResourcePlan& plan, Value value, uint32_t& result);
|
||||||
bool WalkedDescriptor(const ResourcePlan& plan, uint32_t source, DescriptorValue& result);
|
bool WalkedDescriptor(const ResourcePlan& plan, uint32_t source, DescriptorValue& result);
|
||||||
bool WalkedActive(const ResourcePlan& plan, uint32_t source);
|
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.
|
// 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.
|
// The exception handler completes such a load as a zero read.
|
||||||
uint32_t SrtWalkerLoadLength(uint64_t rip);
|
uint32_t SrtWalkerLoadLength(uint64_t rip);
|
||||||
|
|||||||
@@ -348,14 +348,20 @@ void VisitInstruction(Pass& pass, Block* block, Inst& inst) {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void RewriteToSsa(const BlockList& blocks) {
|
void RewriteToSsa(const BlockList& blocks) {
|
||||||
Pass pass;
|
Pass pass;
|
||||||
|
std::unordered_map<Block*, size_t> filled_predecessors;
|
||||||
for (auto* block: blocks) {
|
for (auto* block: blocks) {
|
||||||
|
if (block->ImmPredecessors().empty()) {
|
||||||
|
pass.Seal(block);
|
||||||
|
}
|
||||||
for (auto& inst: *block) {
|
for (auto& inst: *block) {
|
||||||
VisitInstruction(pass, block, inst);
|
VisitInstruction(pass, block, inst);
|
||||||
}
|
}
|
||||||
}
|
for (auto* successor: block->ImmSuccessors()) {
|
||||||
for (auto* block: blocks) {
|
if (++filled_predecessors[successor] == successor->ImmPredecessors().size()) {
|
||||||
pass.Seal(block);
|
pass.Seal(successor);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -291,10 +291,9 @@ static bool ShaderGetStaticVertexInputInfo(uint64_t shader_addr, const HW::UserS
|
|||||||
const auto& metadata = data.vertex;
|
const auto& metadata = data.vertex;
|
||||||
|
|
||||||
if (metadata.vertex_attrib_reg >= 0) {
|
if (metadata.vertex_attrib_reg >= 0) {
|
||||||
info.fetch_external = false;
|
info.has_dynamic_vertex_fetch = true;
|
||||||
info.fetch_embedded = true;
|
info.fetch_attrib_reg = metadata.vertex_attrib_reg;
|
||||||
info.fetch_attrib_reg = metadata.vertex_attrib_reg;
|
info.fetch_buffer_reg = metadata.vertex_buffer_reg;
|
||||||
info.fetch_buffer_reg = metadata.vertex_buffer_reg;
|
|
||||||
|
|
||||||
const auto* attrib = reinterpret_cast<const uint32_t*>(
|
const auto* attrib = reinterpret_cast<const uint32_t*>(
|
||||||
static_cast<uint64_t>(user_sgpr.value[metadata.vertex_attrib_reg]) |
|
static_cast<uint64_t>(user_sgpr.value[metadata.vertex_attrib_reg]) |
|
||||||
@@ -413,7 +412,7 @@ static void ShaderGetStaticInputInfoCS(const HW::ComputeShaderInfo& regs,
|
|||||||
void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t>& key) {
|
void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t>& key) {
|
||||||
EXIT_IF(info.resources_num < 0 || info.resources_num > ShaderVertexInputInfo::RES_MAX);
|
EXIT_IF(info.resources_num < 0 || info.resources_num > ShaderVertexInputInfo::RES_MAX);
|
||||||
key.clear();
|
key.clear();
|
||||||
key.push_back(static_cast<uint32_t>(info.fetch_embedded));
|
key.push_back(static_cast<uint32_t>(info.has_dynamic_vertex_fetch));
|
||||||
key.push_back(static_cast<uint32_t>(info.fetch_attrib_reg));
|
key.push_back(static_cast<uint32_t>(info.fetch_attrib_reg));
|
||||||
key.push_back(static_cast<uint32_t>(info.fetch_buffer_reg));
|
key.push_back(static_cast<uint32_t>(info.fetch_buffer_reg));
|
||||||
key.push_back(info.resources_num);
|
key.push_back(info.resources_num);
|
||||||
@@ -422,6 +421,8 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
|
|||||||
key.push_back(info.lds_size_dwords);
|
key.push_back(info.lds_size_dwords);
|
||||||
key.push_back(info.pa_cl_vs_out_cntl);
|
key.push_back(info.pa_cl_vs_out_cntl);
|
||||||
key.push_back(info.user_clip_planes);
|
key.push_back(info.user_clip_planes);
|
||||||
|
key.push_back(info.indirect_offsets.vertex | (uint32_t {info.indirect_offsets.instance} << 8u) |
|
||||||
|
(uint32_t {info.indirect_draw} << 16u));
|
||||||
key.push_back(static_cast<uint32_t>(info.clip_space.enabled));
|
key.push_back(static_cast<uint32_t>(info.clip_space.enabled));
|
||||||
if (info.clip_space.enabled) {
|
if (info.clip_space.enabled) {
|
||||||
for (const float value: info.clip_space.scale) {
|
for (const float value: info.clip_space.scale) {
|
||||||
@@ -692,9 +693,7 @@ void ShaderDbgDumpInputInfo(const ShaderVertexInputInfo& info) {
|
|||||||
|
|
||||||
LOGF("ShaderDbgDumpInputInfo()\n");
|
LOGF("ShaderDbgDumpInputInfo()\n");
|
||||||
|
|
||||||
LOGF("\t fetch_external = %s\n"
|
LOGF("\t has_dynamic_vertex_fetch = %s\n", info.has_dynamic_vertex_fetch ? "true" : "false");
|
||||||
"\t fetch_embedded = %s\n",
|
|
||||||
info.fetch_external ? "true" : "false", info.fetch_embedded ? "true" : "false");
|
|
||||||
|
|
||||||
for (int i = 0; i < info.resources_num; i++) {
|
for (int i = 0; i < info.resources_num; i++) {
|
||||||
LOGF("\t input %d\n", i);
|
LOGF("\t input %d\n", i);
|
||||||
|
|||||||
@@ -124,6 +124,17 @@ struct ShaderTessellationInputInfo {
|
|||||||
uint32_t output_topology = 0;
|
uint32_t output_topology = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// GS user-data slots an indirect draw packet writes its record's base vertex and first instance
|
||||||
|
// to. A packet can leave both slots unused even when its record contains nonzero offsets.
|
||||||
|
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 {
|
struct ShaderVertexInputInfo {
|
||||||
static constexpr int RES_MAX = 32;
|
static constexpr int RES_MAX = 32;
|
||||||
|
|
||||||
@@ -146,8 +157,11 @@ struct ShaderVertexInputInfo {
|
|||||||
ShaderClipSpaceTransform clip_space;
|
ShaderClipSpaceTransform clip_space;
|
||||||
ShaderMeshInputInfo mesh;
|
ShaderMeshInputInfo mesh;
|
||||||
ShaderTessellationInputInfo tess;
|
ShaderTessellationInputInfo tess;
|
||||||
bool fetch_external = false;
|
ShaderIndirectOffsets indirect_offsets;
|
||||||
bool fetch_embedded = false;
|
// GPU-read records add Vulkan bases even when the packet patches no user SGPRs.
|
||||||
|
bool indirect_draw = false;
|
||||||
|
// Attribute-table metadata enables dynamic fetch detection; loads are matched later.
|
||||||
|
bool has_dynamic_vertex_fetch = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ShaderComputeInputInfo: ShaderWorkgroupInputInfo {
|
struct ShaderComputeInputInfo: ShaderWorkgroupInputInfo {
|
||||||
|
|||||||
+505
-1103
File diff suppressed because it is too large.
Load diff
+12
-4
@@ -26,9 +26,10 @@ struct DebugStats {
|
|||||||
uint64_t flexible_total = 0;
|
uint64_t flexible_total = 0;
|
||||||
uint64_t flexible_used = 0;
|
uint64_t flexible_used = 0;
|
||||||
uint64_t pool_committed = 0;
|
uint64_t pool_committed = 0;
|
||||||
uint64_t cpu_page_entries = 0; // 2 MiB page-table entries in use.
|
uint64_t cpu_page_entries = 0; // 2 MiB page-table entries in use.
|
||||||
uint64_t gpu_page_entries = 0;
|
uint64_t gpu_page_entries = 0;
|
||||||
uint64_t page_entries_total = 0; // Per side.
|
uint64_t cpu_page_entries_total = 0;
|
||||||
|
uint64_t gpu_page_entries_total = 0;
|
||||||
};
|
};
|
||||||
[[nodiscard]] DebugStats GetDebugStats();
|
[[nodiscard]] DebugStats GetDebugStats();
|
||||||
|
|
||||||
@@ -125,7 +126,6 @@ static_assert(sizeof(KernelMemoryPoolBlockStats) == 16,
|
|||||||
"KernelMemoryPoolBlockStats struct size is incorrect");
|
"KernelMemoryPoolBlockStats struct size is incorrect");
|
||||||
|
|
||||||
void RegisterCallbacks(callback_func_t alloc_func, callback_func_t free_func);
|
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 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 memory_type, int64_t* phys_addr_out, bool automatic = false);
|
||||||
int MapAutomaticMemory(uint64_t vaddr, size_t size, int type, int prot);
|
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,
|
uint64_t AllocateProgramMemory(uint64_t search_addr, uint64_t size,
|
||||||
Common::VirtualMemory::Mode mode, const char* name);
|
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);
|
void SetProgramMemoryProtection(uint64_t vaddr, uint64_t size, Common::VirtualMemory::Mode mode);
|
||||||
uint64_t AllocateRuntimeMemory(uint64_t search_addr, uint64_t size,
|
uint64_t AllocateRuntimeMemory(uint64_t search_addr, uint64_t size,
|
||||||
Common::VirtualMemory::Mode mode, const char* name,
|
Common::VirtualMemory::Mode mode, const char* name,
|
||||||
@@ -222,6 +225,11 @@ bool TestPlaceholderRangeIsFree(uint64_t vaddr, uint64_t size);
|
|||||||
bool TestGuestAddressRangeIsOwned(uint64_t vaddr, uint64_t size);
|
bool TestGuestAddressRangeIsOwned(uint64_t vaddr, uint64_t size);
|
||||||
bool TestGuestBackingOutsideAddressSpace();
|
bool TestGuestBackingOutsideAddressSpace();
|
||||||
uint64_t TestGuestBackingSize();
|
uint64_t TestGuestBackingSize();
|
||||||
|
uint64_t TestFlexibleBackingHighWater();
|
||||||
|
uint64_t TestGuestFreeRangeVisits();
|
||||||
|
uint64_t TestDirectMappingCount();
|
||||||
|
uint64_t TestFlexibleMappingCount();
|
||||||
|
uint64_t TestBackingBytesZeroed();
|
||||||
bool TestGuestFreeRangeBounds();
|
bool TestGuestFreeRangeBounds();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -203,6 +203,9 @@ public:
|
|||||||
}
|
}
|
||||||
std::lock_guard lock(m_mutex);
|
std::lock_guard lock(m_mutex);
|
||||||
std::memset(m_backing_base + backing_offset, 0, size);
|
std::memset(m_backing_base + backing_offset, 0, size);
|
||||||
|
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||||
|
g_test_backing_bytes_zeroed += size;
|
||||||
|
#endif
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +290,18 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ProtectViews(uint64_t vaddr, uint64_t size, VirtualMemory::Mode mode) {
|
||||||
|
std::shared_lock lock(m_mutex);
|
||||||
|
bool ok = true;
|
||||||
|
ForEachSparseRange(
|
||||||
|
m_maps, vaddr, size,
|
||||||
|
[&](const auto&, uint64_t start, uint64_t bytes) {
|
||||||
|
ok = ok && VirtualMemory::Protect(start, bytes, mode);
|
||||||
|
},
|
||||||
|
[&](uint64_t, uint64_t) { ok = false; });
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
bool MapFixed(uint64_t vaddr, uint64_t size, uint64_t backing_offset,
|
bool MapFixed(uint64_t vaddr, uint64_t size, uint64_t backing_offset,
|
||||||
VirtualMemory::Mode mode, FailureReason* failure_reason = nullptr) {
|
VirtualMemory::Mode mode, FailureReason* failure_reason = nullptr) {
|
||||||
if (!IsAvailable() || !RangeOk(backing_offset, size)) {
|
if (!IsAvailable() || !RangeOk(backing_offset, size)) {
|
||||||
@@ -324,15 +339,11 @@ public:
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Unmap(uint64_t vaddr, uint64_t size, bool* placeholder_preserved = nullptr) {
|
bool Unmap(uint64_t vaddr, uint64_t size) {
|
||||||
if (size == 0 || UINT64_MAX - vaddr < size) {
|
if (size == 0 || UINT64_MAX - vaddr < size) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (placeholder_preserved != nullptr) {
|
|
||||||
*placeholder_preserved = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto end = vaddr + size;
|
const auto end = vaddr + size;
|
||||||
std::vector<MapEntry> targets;
|
std::vector<MapEntry> targets;
|
||||||
{
|
{
|
||||||
@@ -350,42 +361,36 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<MapEntry> removed;
|
size_t removed = 0;
|
||||||
removed.reserve(targets.size());
|
auto rollback = [&] {
|
||||||
|
while (removed != 0) {
|
||||||
|
const auto& previous = targets[--removed];
|
||||||
|
EXIT_IF(!MapFixed(previous.vaddr, previous.size, previous.backing_offset,
|
||||||
|
previous.mode));
|
||||||
|
}
|
||||||
|
};
|
||||||
for (const auto& entry: targets) {
|
for (const auto& entry: targets) {
|
||||||
bool part_placeholder_preserved = false;
|
if (!UnmapOne(entry.vaddr, entry.size)) {
|
||||||
if (!UnmapOne(entry.vaddr, entry.size, &part_placeholder_preserved)) {
|
rollback();
|
||||||
for (auto it = removed.rbegin(); it != removed.rend(); ++it) {
|
|
||||||
EXIT_IF(!MapFixed(it->vaddr, it->size, it->backing_offset, it->mode));
|
|
||||||
}
|
|
||||||
if (placeholder_preserved != nullptr) {
|
|
||||||
*placeholder_preserved = false;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
EXIT_IF(!part_placeholder_preserved);
|
++removed;
|
||||||
removed.push_back(entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||||
// Each unmapped file view becomes a separate Windows placeholder. The address-space owner
|
// Each unmapped file view becomes a separate Windows placeholder. The address-space owner
|
||||||
// records this contiguous guest span as one free range, so normalize fragmented backing
|
// records this contiguous guest span as one free range, so normalize fragmented backing
|
||||||
// views into one placeholder before publishing it there.
|
// views into one placeholder before publishing it there.
|
||||||
if (removed.size() > 1 && !CoalescePlaceholders(vaddr, size)) {
|
if (removed > 1 && !CoalescePlaceholders(vaddr, size)) {
|
||||||
for (auto it = removed.rbegin(); it != removed.rend(); ++it) {
|
rollback();
|
||||||
EXIT_IF(!MapFixed(it->vaddr, it->size, it->backing_offset, it->mode));
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (placeholder_preserved != nullptr) {
|
|
||||||
*placeholder_preserved = true;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UnmapOne(uint64_t vaddr, uint64_t size, bool* placeholder_preserved = nullptr) {
|
bool UnmapOne(uint64_t vaddr, uint64_t size) {
|
||||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||||
if (g_test_backing_store_unmaps_before_failure == 0) {
|
if (g_test_backing_store_unmaps_before_failure == 0) {
|
||||||
g_test_backing_store_unmaps_before_failure = UINT32_MAX;
|
g_test_backing_store_unmaps_before_failure = UINT32_MAX;
|
||||||
@@ -428,9 +433,7 @@ public:
|
|||||||
m_maps.emplace(old.vaddr, old);
|
m_maps.emplace(old.vaddr, old);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (placeholder_preserved != nullptr) {
|
|
||||||
*placeholder_preserved = true;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,9 +454,6 @@ public:
|
|||||||
ok;
|
ok;
|
||||||
}
|
}
|
||||||
if (ok) {
|
if (ok) {
|
||||||
if (placeholder_preserved != nullptr) {
|
|
||||||
*placeholder_preserved = true;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,9 +481,6 @@ public:
|
|||||||
std::lock_guard lock(m_mutex);
|
std::lock_guard lock(m_mutex);
|
||||||
RemoveMappingsUnlocked(vaddr, size);
|
RemoveMappingsUnlocked(vaddr, size);
|
||||||
|
|
||||||
if (placeholder_preserved != nullptr) {
|
|
||||||
*placeholder_preserved = true;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -634,8 +631,6 @@ private:
|
|||||||
|
|
||||||
HANDLE m_handle = nullptr;
|
HANDLE m_handle = nullptr;
|
||||||
#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX
|
#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX
|
||||||
[[nodiscard]] static uint64_t GetMappableTestSize() { return 0x4000; }
|
|
||||||
|
|
||||||
static int GetProtect(VirtualMemory::Mode mode) {
|
static int GetProtect(VirtualMemory::Mode mode) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case VirtualMemory::Mode::Read: return PROT_READ;
|
case VirtualMemory::Mode::Read: return PROT_READ;
|
||||||
@@ -760,7 +755,7 @@ public:
|
|||||||
if (size == 0 || UINT64_MAX - vaddr < size) {
|
if (size == 0 || UINT64_MAX - vaddr < size) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std::lock_guard lock(m_mutex);
|
std::shared_lock lock(m_mutex);
|
||||||
return std::any_of(m_owned.begin(), m_owned.end(), [vaddr, size](const auto& range) {
|
return std::any_of(m_owned.begin(), m_owned.end(), [vaddr, size](const auto& range) {
|
||||||
return VirtualRangesOverlap(vaddr, size, range.first, range.second);
|
return VirtualRangesOverlap(vaddr, size, range.first, range.second);
|
||||||
});
|
});
|
||||||
@@ -864,11 +859,7 @@ public:
|
|||||||
if (!ContainsMappedUnlocked(vaddr, size, MappingType::Backing)) {
|
if (!ContainsMappedUnlocked(vaddr, size, MappingType::Backing)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
bool placeholder_preserved = false;
|
if (!m_backing->Unmap(vaddr, size)) {
|
||||||
if (!m_backing->Unmap(vaddr, size, &placeholder_preserved)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!placeholder_preserved) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
EXIT_IF(!RemoveMappedUnlocked(vaddr, size, MappingType::Backing));
|
EXIT_IF(!RemoveMappedUnlocked(vaddr, size, MappingType::Backing));
|
||||||
@@ -876,12 +867,13 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reservations live in VirtualRanges; these free-range checks only read the host placeholders.
|
||||||
uint64_t ReserveAligned(uint64_t search_addr, uint64_t size, uint64_t alignment) {
|
uint64_t ReserveAligned(uint64_t search_addr, uint64_t size, uint64_t alignment) {
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
alignment = std::max<uint64_t>(alignment != 0 ? alignment : PageSize(), PageSize());
|
alignment = std::max<uint64_t>(alignment != 0 ? alignment : PageSize(), PageSize());
|
||||||
std::lock_guard lock(m_mutex);
|
std::shared_lock lock(m_mutex);
|
||||||
if (search_addr == 0) {
|
if (search_addr == 0) {
|
||||||
auto addr = FindFreeAlignedUnlocked(HOST_SYSTEM_MANAGED_MIN,
|
auto addr = FindFreeAlignedUnlocked(HOST_SYSTEM_MANAGED_MIN,
|
||||||
HOST_SYSTEM_MANAGED_MAX + 1u, size, alignment);
|
HOST_SYSTEM_MANAGED_MAX + 1u, size, alignment);
|
||||||
@@ -898,7 +890,7 @@ public:
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
alignment = std::max<uint64_t>(alignment, PageSize());
|
alignment = std::max<uint64_t>(alignment, PageSize());
|
||||||
std::lock_guard lock(m_mutex);
|
std::shared_lock lock(m_mutex);
|
||||||
return FindFreeAlignedUnlocked(search_start, search_end, size, alignment);
|
return FindFreeAlignedUnlocked(search_start, search_end, size, alignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -906,19 +898,19 @@ public:
|
|||||||
if (vaddr == 0 || size == 0 || !IsPageAligned(vaddr) || !IsPageAligned(size)) {
|
if (vaddr == 0 || size == 0 || !IsPageAligned(vaddr) || !IsPageAligned(size)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std::lock_guard lock(m_mutex);
|
std::shared_lock lock(m_mutex);
|
||||||
return FindContaining(vaddr, size) != m_free.end();
|
return FindContaining(vaddr, size) != m_free.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||||
bool TestContainsFree(uint64_t vaddr, uint64_t size) {
|
bool TestContainsFree(uint64_t vaddr, uint64_t size) {
|
||||||
std::lock_guard lock(m_mutex);
|
std::shared_lock lock(m_mutex);
|
||||||
return FindContaining(vaddr, size) != m_free.end();
|
return FindContaining(vaddr, size) != m_free.end();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool ReleaseFree(uint64_t vaddr, uint64_t size) {
|
bool ReleaseFree(uint64_t vaddr, uint64_t size) {
|
||||||
std::lock_guard lock(m_mutex);
|
std::shared_lock lock(m_mutex);
|
||||||
return FindContaining(vaddr, size) != m_free.end();
|
return FindContaining(vaddr, size) != m_free.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1041,10 +1033,6 @@ private:
|
|||||||
return (value & (PageSize() - 1u)) == 0;
|
return (value & (PageSize() - 1u)) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool IsGranularityAligned(uint64_t value) const {
|
|
||||||
return (value & (m_granularity - 1u)) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ProtectMappedUnlocked(uint64_t vaddr, uint64_t size, VirtualMemory::Mode mode) {
|
bool ProtectMappedUnlocked(uint64_t vaddr, uint64_t size, VirtualMemory::Mode mode) {
|
||||||
const auto end = vaddr + size;
|
const auto end = vaddr + size;
|
||||||
auto it = m_mapped.upper_bound(vaddr);
|
auto it = m_mapped.upper_bound(vaddr);
|
||||||
@@ -1055,8 +1043,20 @@ private:
|
|||||||
const auto& region = it->second;
|
const auto& region = it->second;
|
||||||
const auto range_start = std::max(vaddr, region.vaddr);
|
const auto range_start = std::max(vaddr, region.vaddr);
|
||||||
const auto range_end = std::min(end, region.vaddr + region.size);
|
const auto range_end = std::min(end, region.vaddr + region.size);
|
||||||
if (range_start < range_end &&
|
if (range_start >= range_end) {
|
||||||
!VirtualMemory::Protect(range_start, range_end - range_start, mode)) {
|
continue;
|
||||||
|
}
|
||||||
|
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||||
|
// Rollback can split a backing view without changing the owner interval.
|
||||||
|
// Windows protection calls must stay within each actual view.
|
||||||
|
if (region.type == MappingType::Backing) {
|
||||||
|
if (!m_backing->ProtectViews(range_start, range_end - range_start, mode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (!VirtualMemory::Protect(range_start, range_end - range_start, mode)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1312,12 +1312,21 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint64_t FindFreeAlignedUnlocked(uint64_t search_start, uint64_t search_end, uint64_t size,
|
uint64_t FindFreeAlignedUnlocked(uint64_t search_start, uint64_t search_end, uint64_t size,
|
||||||
uint64_t alignment) {
|
uint64_t alignment) const {
|
||||||
if (search_start >= search_end || size > search_end - search_start) {
|
if (search_start >= search_end || size > search_end - search_start) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
for (const auto& [base, range_size]: m_free) {
|
// Include the predecessor because search_start can lie inside a free range.
|
||||||
const auto candidate = AlignUp(std::max(base, search_start), alignment);
|
auto it = m_free.upper_bound(search_start);
|
||||||
|
if (it != m_free.begin()) {
|
||||||
|
--it;
|
||||||
|
}
|
||||||
|
for (; it != m_free.end() && it->first < search_end; ++it) {
|
||||||
|
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||||
|
g_test_free_range_visits.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
#endif
|
||||||
|
const auto& [base, range_size] = *it;
|
||||||
|
const auto candidate = AlignUp(std::max(base, search_start), alignment);
|
||||||
if (candidate < search_end && size <= search_end - candidate &&
|
if (candidate < search_end && size <= search_end - candidate &&
|
||||||
GuestFreeRangeContains(base, range_size, candidate, size)) {
|
GuestFreeRangeContains(base, range_size, candidate, size)) {
|
||||||
return candidate;
|
return candidate;
|
||||||
@@ -1331,9 +1340,8 @@ private:
|
|||||||
if (next == m_free.begin()) {
|
if (next == m_free.begin()) {
|
||||||
return m_free.end();
|
return m_free.end();
|
||||||
}
|
}
|
||||||
auto it = std::prev(next);
|
auto it = std::prev(next);
|
||||||
auto end = it->first + it->second;
|
return GuestFreeRangeContains(it->first, it->second, vaddr, size) ? it : m_free.end();
|
||||||
return (vaddr >= it->first && vaddr + size <= end ? it : m_free.end());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SplitUnlocked(std::map<uint64_t, uint64_t>::iterator it, uint64_t vaddr, uint64_t size) {
|
bool SplitUnlocked(std::map<uint64_t, uint64_t>::iterator it, uint64_t vaddr, uint64_t size) {
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ if(APPLE)
|
|||||||
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
|
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
|
||||||
install(TARGETS kyty_updater 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_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")
|
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.
|
# 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)
|
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
|
||||||
|
|||||||
@@ -115,7 +115,17 @@
|
|||||||
</item>
|
</item>
|
||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
<widget class="QComboBox" name="comboBox_screen_resolution">
|
<widget class="QComboBox" name="comboBox_screen_resolution">
|
||||||
<property name="toolTip"><string>Window resolution</string></property>
|
<property name="toolTip"><string>Window resolution as WIDTHxHEIGHT; pick a preset or type a custom size. Non-16:9 sizes such as 21:9 show the game with black bars unless Stretch to window is on.</string></property>
|
||||||
|
<property name="editable"><bool>true</bool></property>
|
||||||
|
<property name="insertPolicy"><enum>QComboBox::NoInsert</enum></property>
|
||||||
|
<item><property name="text"><string notr="true">1280x720</string></property></item>
|
||||||
|
<item><property name="text"><string notr="true">1920x1080</string></property></item>
|
||||||
|
<item><property name="text"><string notr="true">2560x1440</string></property></item>
|
||||||
|
<item><property name="text"><string notr="true">3840x2160</string></property></item>
|
||||||
|
<item><property name="text"><string notr="true">2560x1080</string></property></item>
|
||||||
|
<item><property name="text"><string notr="true">3440x1440</string></property></item>
|
||||||
|
<item><property name="text"><string notr="true">3840x1600</string></property></item>
|
||||||
|
<item><property name="text"><string notr="true">5120x2160</string></property></item>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
@@ -256,6 +266,12 @@
|
|||||||
<property name="text"><string>Auto-hide cursor</string></property>
|
<property name="text"><string>Auto-hide cursor</string></property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="checkBox_stretch">
|
||||||
|
<property name="toolTip"><string>Stretch the game image to fill the window instead of keeping its aspect ratio with black bars</string></property>
|
||||||
|
<property name="text"><string>Stretch to window</string></property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="checkBox_perf_overlay">
|
<widget class="QCheckBox" name="checkBox_perf_overlay">
|
||||||
<property name="toolTip"><string>Show FPS, frame times, CPU/GPU usage, RAM and memory pressure in the game window. Press F2 (Cmd+P on macOS) to toggle it, and add Shift for the detailed view.</string></property>
|
<property name="toolTip"><string>Show FPS, frame times, CPU/GPU usage, RAM and memory pressure in the game window. Press F2 (Cmd+P on macOS) to toggle it, and add Shift for the detailed view.</string></property>
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ inline QStringList EnumToList() {
|
|||||||
auto me = QMetaEnum::fromType<T>();
|
auto me = QMetaEnum::fromType<T>();
|
||||||
int count = me.keyCount();
|
int count = me.keyCount();
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
auto key = QString(me.key(i));
|
ret << QString(me.key(i));
|
||||||
ret << (key.startsWith('R') && key.size() > 2 && key.at(1).isDigit()
|
|
||||||
? key.remove('R').toLower()
|
|
||||||
: key);
|
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -32,16 +29,12 @@ inline QStringList EnumToList() {
|
|||||||
template <class T>
|
template <class T>
|
||||||
T TextToEnum(const QString& text) {
|
T TextToEnum(const QString& text) {
|
||||||
auto me = QMetaEnum::fromType<T>();
|
auto me = QMetaEnum::fromType<T>();
|
||||||
return static_cast<T>(me.keyToValue(
|
return static_cast<T>(me.keyToValue(text.toUtf8().constData()));
|
||||||
((text.size() > 1 && text.at(0).isDigit()) ? 'R' + text.toUpper() : text).toUtf8().data()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
template <class T>
|
template <class T>
|
||||||
QString EnumToText(T value) {
|
QString EnumToText(T value) {
|
||||||
auto me = QMetaEnum::fromType<T>();
|
return QString(QMetaEnum::fromType<T>().valueToKey(static_cast<int>(value)));
|
||||||
auto key = QString(me.valueToKey(static_cast<int>(value)));
|
|
||||||
return (key.startsWith('R') && key.size() > 2 && key.at(1).isDigit() ? key.remove('R').toLower()
|
|
||||||
: key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ControllerSettings {
|
struct ControllerSettings {
|
||||||
@@ -80,13 +73,10 @@ public:
|
|||||||
static constexpr int DEFAULT_CONSOLE_LANGUAGE = 1;
|
static constexpr int DEFAULT_CONSOLE_LANGUAGE = 1;
|
||||||
static constexpr int MAX_CONSOLE_LANGUAGE = 29;
|
static constexpr int MAX_CONSOLE_LANGUAGE = 29;
|
||||||
|
|
||||||
enum class Resolution {
|
static constexpr int MIN_SCREEN_SIZE = 320;
|
||||||
R1280X720,
|
static constexpr int MAX_SCREEN_SIZE = 16384;
|
||||||
R1920X1080,
|
|
||||||
R2560X1440,
|
static QString NormalizeResolution(const QString& text);
|
||||||
R3840X2160,
|
|
||||||
};
|
|
||||||
Q_ENUM(Resolution)
|
|
||||||
|
|
||||||
enum class ShaderOptimizationType { None, Size, Performance };
|
enum class ShaderOptimizationType { None, Size, Performance };
|
||||||
Q_ENUM(ShaderOptimizationType)
|
Q_ENUM(ShaderOptimizationType)
|
||||||
@@ -117,7 +107,7 @@ public:
|
|||||||
// Controller preferences always come from the global configuration.
|
// Controller preferences always come from the global configuration.
|
||||||
ControllerSettings controller;
|
ControllerSettings controller;
|
||||||
|
|
||||||
Resolution screen_resolution = Resolution::R1280X720;
|
QString screen_resolution = QStringLiteral("1280x720");
|
||||||
QString user_name = "Kyty";
|
QString user_name = "Kyty";
|
||||||
int user_id = Config::DEFAULT_USER_ID;
|
int user_id = Config::DEFAULT_USER_ID;
|
||||||
QString audio_input_device;
|
QString audio_input_device;
|
||||||
@@ -126,6 +116,7 @@ public:
|
|||||||
bool fullscreen_enabled = false;
|
bool fullscreen_enabled = false;
|
||||||
bool hdr_enabled = false;
|
bool hdr_enabled = false;
|
||||||
bool hide_cursor_enabled = false;
|
bool hide_cursor_enabled = false;
|
||||||
|
bool stretch_enabled = false;
|
||||||
bool readback_linear_images = false;
|
bool readback_linear_images = false;
|
||||||
bool readback_tiled_images = false;
|
bool readback_tiled_images = false;
|
||||||
bool sync_raw_image_buffers = false;
|
bool sync_raw_image_buffers = false;
|
||||||
@@ -168,6 +159,7 @@ public:
|
|||||||
fullscreen_enabled = other.fullscreen_enabled;
|
fullscreen_enabled = other.fullscreen_enabled;
|
||||||
hdr_enabled = other.hdr_enabled;
|
hdr_enabled = other.hdr_enabled;
|
||||||
hide_cursor_enabled = other.hide_cursor_enabled;
|
hide_cursor_enabled = other.hide_cursor_enabled;
|
||||||
|
stretch_enabled = other.stretch_enabled;
|
||||||
readback_linear_images = other.readback_linear_images;
|
readback_linear_images = other.readback_linear_images;
|
||||||
readback_tiled_images = other.readback_tiled_images;
|
readback_tiled_images = other.readback_tiled_images;
|
||||||
sync_raw_image_buffers = other.sync_raw_image_buffers;
|
sync_raw_image_buffers = other.sync_raw_image_buffers;
|
||||||
|
|||||||
@@ -46,11 +46,15 @@ private:
|
|||||||
QLineEdit* m_addons_dir_edit = nullptr;
|
QLineEdit* m_addons_dir_edit = nullptr;
|
||||||
QToolButton* m_remove_game_dir_button = nullptr;
|
QToolButton* m_remove_game_dir_button = nullptr;
|
||||||
bool m_global_settings = false;
|
bool m_global_settings = false;
|
||||||
|
bool m_geometry_restored = false;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void Init(const Configuration& info);
|
void Init(const Configuration& info);
|
||||||
void InitGameDirectories();
|
void InitGameDirectories();
|
||||||
void AddGameDirectoryItem(const QString& dir);
|
void AddGameDirectoryItem(const QString& dir);
|
||||||
|
void MakeTabsScrollable();
|
||||||
|
void GrowToFitContents();
|
||||||
|
void ShowTab(QWidget* page);
|
||||||
|
|
||||||
void moveEvent(QMoveEvent* event) override;
|
void moveEvent(QMoveEvent* event) override;
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
void resizeEvent(QResizeEvent* event) override;
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
QString Configuration::NormalizeResolution(const QString& text) {
|
||||||
|
// Older settings stored the enum key, e.g. "R1920X1080".
|
||||||
|
static const QRegularExpression re(QStringLiteral("^\\s*R?(\\d+)\\s*[xX]\\s*(\\d+)\\s*$"));
|
||||||
|
const auto match = re.match(text);
|
||||||
|
if (!match.hasMatch()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const int width = match.captured(1).toInt();
|
||||||
|
const int height = match.captured(2).toInt();
|
||||||
|
if (width < MIN_SCREEN_SIZE || height < MIN_SCREEN_SIZE || width > MAX_SCREEN_SIZE ||
|
||||||
|
height > MAX_SCREEN_SIZE) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return QStringLiteral("%1x%2").arg(width).arg(height);
|
||||||
|
}
|
||||||
|
|
||||||
QVariantMap Configuration::GameSettings() const {
|
QVariantMap Configuration::GameSettings() const {
|
||||||
QVariantMap values;
|
QVariantMap values;
|
||||||
#define KYTY_CFG_SET(n) values.insert(QStringLiteral(#n), QVariant::fromValue(n).toString())
|
#define KYTY_CFG_SET(n) values.insert(QStringLiteral(#n), QVariant::fromValue(n).toString())
|
||||||
@@ -12,6 +30,7 @@ QVariantMap Configuration::GameSettings() const {
|
|||||||
KYTY_CFG_SET(fullscreen_enabled);
|
KYTY_CFG_SET(fullscreen_enabled);
|
||||||
KYTY_CFG_SET(hdr_enabled);
|
KYTY_CFG_SET(hdr_enabled);
|
||||||
KYTY_CFG_SET(hide_cursor_enabled);
|
KYTY_CFG_SET(hide_cursor_enabled);
|
||||||
|
KYTY_CFG_SET(stretch_enabled);
|
||||||
KYTY_CFG_SET(readback_linear_images);
|
KYTY_CFG_SET(readback_linear_images);
|
||||||
KYTY_CFG_SET(readback_tiled_images);
|
KYTY_CFG_SET(readback_tiled_images);
|
||||||
KYTY_CFG_SET(sync_raw_image_buffers);
|
KYTY_CFG_SET(sync_raw_image_buffers);
|
||||||
@@ -48,7 +67,8 @@ QVariantMap Configuration::GameSettings() const {
|
|||||||
template <class Settings>
|
template <class Settings>
|
||||||
void Configuration::ReadGameSettingsValues(const Settings& s) {
|
void Configuration::ReadGameSettingsValues(const Settings& s) {
|
||||||
#define KYTY_CFG_GET(n) n = s.value(QStringLiteral(#n)).template value<decltype(n)>()
|
#define KYTY_CFG_GET(n) n = s.value(QStringLiteral(#n)).template value<decltype(n)>()
|
||||||
KYTY_CFG_GET(screen_resolution);
|
const auto resolution = NormalizeResolution(s.value("screen_resolution").toString());
|
||||||
|
screen_resolution = resolution.isEmpty() ? QStringLiteral("1280x720") : resolution;
|
||||||
user_name = s.value("user_name", user_name).toString();
|
user_name = s.value("user_name", user_name).toString();
|
||||||
bool user_id_ok = false;
|
bool user_id_ok = false;
|
||||||
auto saved_user_id = s.value("user_id", user_id).toInt(&user_id_ok);
|
auto saved_user_id = s.value("user_id", user_id).toInt(&user_id_ok);
|
||||||
@@ -64,6 +84,7 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
|
|||||||
KYTY_CFG_GET(fullscreen_enabled);
|
KYTY_CFG_GET(fullscreen_enabled);
|
||||||
KYTY_CFG_GET(hdr_enabled);
|
KYTY_CFG_GET(hdr_enabled);
|
||||||
KYTY_CFG_GET(hide_cursor_enabled);
|
KYTY_CFG_GET(hide_cursor_enabled);
|
||||||
|
KYTY_CFG_GET(stretch_enabled);
|
||||||
KYTY_CFG_GET(readback_linear_images);
|
KYTY_CFG_GET(readback_linear_images);
|
||||||
KYTY_CFG_GET(readback_tiled_images);
|
KYTY_CFG_GET(readback_tiled_images);
|
||||||
KYTY_CFG_GET(sync_raw_image_buffers);
|
KYTY_CFG_GET(sync_raw_image_buffers);
|
||||||
@@ -134,7 +155,9 @@ bool Configuration::SetGameSettings(const QJsonObject& settings, QString& error)
|
|||||||
if (!it.value().isString()) {
|
if (!it.value().isString()) {
|
||||||
return fail(tr("Game setting %1 must be a string.").arg(it.key()));
|
return fail(tr("Game setting %1 must be a string.").arg(it.key()));
|
||||||
}
|
}
|
||||||
values.insert(it.key(), it.value().toString());
|
const auto value = it.value().toString();
|
||||||
|
values.insert(it.key(), it.key() == "screen_resolution" ? NormalizeResolution(value)
|
||||||
|
: value);
|
||||||
}
|
}
|
||||||
|
|
||||||
ReadGameSettingsValues(values);
|
ReadGameSettingsValues(values);
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QResizeEvent>
|
#include <QResizeEvent>
|
||||||
|
#include <QScreen>
|
||||||
|
#include <QScrollArea>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QSlider>
|
#include <QSlider>
|
||||||
#include <QSpinBox>
|
#include <QSpinBox>
|
||||||
@@ -122,10 +124,10 @@ static QString GameDirectoryKey(const QString& dir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* parent)
|
ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* parent)
|
||||||
: QDialog(parent, Qt::WindowCloseButtonHint), m_ui(new Ui::ConfigurationEditDialog),
|
: QDialog(parent, Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint),
|
||||||
m_info(info) {
|
m_ui(new Ui::ConfigurationEditDialog), m_info(info) {
|
||||||
m_ui->setupUi(this);
|
m_ui->setupUi(this);
|
||||||
setMinimumWidth(width());
|
MakeTabsScrollable();
|
||||||
InitGameDirectories();
|
InitGameDirectories();
|
||||||
m_ui->controller_group->setVisible(false);
|
m_ui->controller_group->setVisible(false);
|
||||||
m_ui->settings_button->setVisible(false);
|
m_ui->settings_button->setVisible(false);
|
||||||
@@ -170,9 +172,11 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
|
|||||||
|
|
||||||
setSizeGripEnabled(true);
|
setSizeGripEnabled(true);
|
||||||
|
|
||||||
restoreGeometry(g_last_geometry);
|
|
||||||
|
|
||||||
Init(info);
|
Init(info);
|
||||||
|
m_geometry_restored = restoreGeometry(g_last_geometry);
|
||||||
|
if (!m_geometry_restored) {
|
||||||
|
GrowToFitContents();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray ConfigurationEditDialog::g_last_geometry;
|
QByteArray ConfigurationEditDialog::g_last_geometry;
|
||||||
@@ -248,7 +252,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
|
|||||||
info.audio_input_device);
|
info.audio_input_device);
|
||||||
}
|
}
|
||||||
microphone->setCurrentIndex(microphone->findData(info.audio_input_device));
|
microphone->setCurrentIndex(microphone->findData(info.audio_input_device));
|
||||||
ListInit(m_ui->comboBox_screen_resolution, info.screen_resolution);
|
m_ui->comboBox_screen_resolution->setCurrentText(info.screen_resolution);
|
||||||
ListInit(m_ui->comboBox_present_mode, info.present_mode);
|
ListInit(m_ui->comboBox_present_mode, info.present_mode);
|
||||||
m_ui->comboBox_gpu->clear();
|
m_ui->comboBox_gpu->clear();
|
||||||
m_ui->comboBox_gpu->addItem(tr("Auto"));
|
m_ui->comboBox_gpu->addItem(tr("Auto"));
|
||||||
@@ -285,6 +289,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
|
|||||||
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
|
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
|
||||||
m_ui->checkBox_hdr->setChecked(info.hdr_enabled);
|
m_ui->checkBox_hdr->setChecked(info.hdr_enabled);
|
||||||
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
|
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
|
||||||
|
m_ui->checkBox_stretch->setChecked(info.stretch_enabled);
|
||||||
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
|
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
|
||||||
m_ui->checkBox_readback_tiled->setChecked(info.readback_tiled_images);
|
m_ui->checkBox_readback_tiled->setChecked(info.readback_tiled_images);
|
||||||
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
|
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
|
||||||
@@ -413,8 +418,46 @@ void ConfigurationEditDialog::SetGlobalSettings(const QStringList& dirs, const Q
|
|||||||
m_game_dirs_group->setVisible(true);
|
m_game_dirs_group->setVisible(true);
|
||||||
m_ui->controller_group->setVisible(true);
|
m_ui->controller_group->setVisible(true);
|
||||||
update_game_directory_buttons();
|
update_game_directory_buttons();
|
||||||
|
if (!m_geometry_restored) {
|
||||||
|
GrowToFitContents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigurationEditDialog::MakeTabsScrollable() {
|
||||||
|
auto* tabs = m_ui->settings_tabs;
|
||||||
|
for (int i = 0; i < tabs->count(); i++) {
|
||||||
|
auto* page = tabs->widget(i);
|
||||||
|
auto title = tabs->tabText(i);
|
||||||
|
auto* scroll = new QScrollArea;
|
||||||
|
scroll->setWidgetResizable(true);
|
||||||
|
scroll->setFrameShape(QFrame::NoFrame);
|
||||||
|
tabs->removeTab(i);
|
||||||
|
scroll->setWidget(page);
|
||||||
|
tabs->insertTab(i, scroll, title);
|
||||||
|
}
|
||||||
|
tabs->setCurrentIndex(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigurationEditDialog::GrowToFitContents() {
|
||||||
layout()->activate();
|
layout()->activate();
|
||||||
resize(size().expandedTo(minimumSizeHint()));
|
auto* tabs = m_ui->settings_tabs;
|
||||||
|
QSize content;
|
||||||
|
for (int i = 0; i < tabs->count(); i++) {
|
||||||
|
content = content.expandedTo(
|
||||||
|
static_cast<QScrollArea*>(tabs->widget(i))->widget()->minimumSizeHint());
|
||||||
|
}
|
||||||
|
const QSize chrome = size() - tabs->currentWidget()->size();
|
||||||
|
resize(size().expandedTo(content + chrome).boundedTo(screen()->availableGeometry().size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigurationEditDialog::ShowTab(QWidget* page) {
|
||||||
|
auto* tabs = m_ui->settings_tabs;
|
||||||
|
for (int i = 0; i < tabs->count(); i++) {
|
||||||
|
if (tabs->widget(i)->isAncestorOf(page)) {
|
||||||
|
tabs->setCurrentIndex(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ConfigurationEditDialog::GetAddonsDirectory() const {
|
QString ConfigurationEditDialog::GetAddonsDirectory() const {
|
||||||
@@ -474,13 +517,14 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
|
|||||||
info.controller.keyboard_player = ui.comboBox_keyboard_player->currentData().toInt();
|
info.controller.keyboard_player = ui.comboBox_keyboard_player->currentData().toInt();
|
||||||
}
|
}
|
||||||
info.screen_resolution =
|
info.screen_resolution =
|
||||||
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
|
Configuration::NormalizeResolution(ui.comboBox_screen_resolution->currentText());
|
||||||
info.present_mode =
|
info.present_mode =
|
||||||
TextToEnum<Configuration::PresentMode>(ui.comboBox_present_mode->currentText());
|
TextToEnum<Configuration::PresentMode>(ui.comboBox_present_mode->currentText());
|
||||||
info.gpu_index = ui.comboBox_gpu->currentIndex() - 1;
|
info.gpu_index = ui.comboBox_gpu->currentIndex() - 1;
|
||||||
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
|
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
|
||||||
info.hdr_enabled = ui.checkBox_hdr->isChecked();
|
info.hdr_enabled = ui.checkBox_hdr->isChecked();
|
||||||
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
|
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
|
||||||
|
info.stretch_enabled = ui.checkBox_stretch->isChecked();
|
||||||
info.readback_linear_images = ui.checkBox_readback->isChecked();
|
info.readback_linear_images = ui.checkBox_readback->isChecked();
|
||||||
info.readback_tiled_images = ui.checkBox_readback_tiled->isChecked();
|
info.readback_tiled_images = ui.checkBox_readback_tiled->isChecked();
|
||||||
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
|
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
|
||||||
@@ -518,7 +562,7 @@ void ConfigurationEditDialog::save() {
|
|||||||
for (auto* field: {m_ui->lineEdit_shader_log_folder, m_ui->lineEdit_printf_file,
|
for (auto* field: {m_ui->lineEdit_shader_log_folder, m_ui->lineEdit_printf_file,
|
||||||
m_ui->lineEdit_cmd_dump_folder}) {
|
m_ui->lineEdit_cmd_dump_folder}) {
|
||||||
if (field->isEnabled() && field->text().isEmpty()) {
|
if (field->isEnabled() && field->text().isEmpty()) {
|
||||||
m_ui->settings_tabs->setCurrentWidget(m_ui->debug_tab);
|
ShowTab(m_ui->debug_tab);
|
||||||
field->setFocus();
|
field->setFocus();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -540,6 +584,16 @@ void ConfigurationEditDialog::save() {
|
|||||||
tr("User ID cannot be 254 (everyone) or 255 (system)"));
|
tr("User ID cannot be 254 (everyone) or 255 (system)"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (Configuration::NormalizeResolution(m_ui->comboBox_screen_resolution->currentText())
|
||||||
|
.isEmpty()) {
|
||||||
|
ShowTab(m_ui->settings_tab);
|
||||||
|
m_ui->comboBox_screen_resolution->setFocus();
|
||||||
|
QMessageBox::critical(this, tr("Save failed"),
|
||||||
|
tr("Screen resolution must be WIDTHxHEIGHT, each between %1 and %2")
|
||||||
|
.arg(Configuration::MIN_SCREEN_SIZE)
|
||||||
|
.arg(Configuration::MAX_SCREEN_SIZE));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
UpdateInfo(m_info, *m_ui, m_global_settings);
|
UpdateInfo(m_info, *m_ui, m_global_settings);
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,8 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
|
|||||||
m_ui->close_terminal->setVisible(false); // The emulator runs without a terminal.
|
m_ui->close_terminal->setVisible(false); // The emulator runs without a terminal.
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
main_dialog->setWindowFlags(Qt::Dialog /*| Qt::MSWindowsFixedSizeDialogHint*/);
|
main_dialog->setWindowFlags(Qt::Dialog | Qt::WindowMinMaxButtonsHint |
|
||||||
|
Qt::WindowCloseButtonHint);
|
||||||
|
|
||||||
connect(main_dialog, &MainDialog::Start, this, &MainDialogPrivate::FindInterpreter,
|
connect(main_dialog, &MainDialog::Start, this, &MainDialogPrivate::FindInterpreter,
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
@@ -240,7 +241,7 @@ static void AddFlag(QStringList& args, bool enabled, const char* name) {
|
|||||||
|
|
||||||
static QStringList CreateEmulatorArgs(const Configuration& info) {
|
static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||||
QStringList args;
|
QStringList args;
|
||||||
auto r = EnumToText(info.screen_resolution).split('x');
|
auto r = Configuration::NormalizeResolution(info.screen_resolution).split('x');
|
||||||
|
|
||||||
if (r.size() != 2) {
|
if (r.size() != 2) {
|
||||||
return {};
|
return {};
|
||||||
@@ -266,6 +267,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
|||||||
AddFlag(args, info.fullscreen_enabled, "--fullscreen");
|
AddFlag(args, info.fullscreen_enabled, "--fullscreen");
|
||||||
AddFlag(args, info.hdr_enabled, "--hdr");
|
AddFlag(args, info.hdr_enabled, "--hdr");
|
||||||
AddFlag(args, info.hide_cursor_enabled, "--hide-cursor");
|
AddFlag(args, info.hide_cursor_enabled, "--hide-cursor");
|
||||||
|
AddFlag(args, info.stretch_enabled, "--stretch");
|
||||||
AddFlag(args, info.readback_linear_images, "--readback-linear-images");
|
AddFlag(args, info.readback_linear_images, "--readback-linear-images");
|
||||||
AddFlag(args, info.readback_tiled_images, "--readback-tiled-images");
|
AddFlag(args, info.readback_tiled_images, "--readback-tiled-images");
|
||||||
AddFlag(args, info.sync_raw_image_buffers, "--sync-raw-image-buffers");
|
AddFlag(args, info.sync_raw_image_buffers, "--sync-raw-image-buffers");
|
||||||
|
|||||||
+42
-2
@@ -2480,6 +2480,9 @@ static constexpr uint32_t AGC_DRAW_INDIRECT_MULTI_END_TAG = 0xc6000000u;
|
|||||||
static constexpr uint32_t AGC_WAIT_USER_DATA_BEGIN_32_TAG = 0xc8010000u;
|
static constexpr uint32_t AGC_WAIT_USER_DATA_BEGIN_32_TAG = 0xc8010000u;
|
||||||
static constexpr uint32_t AGC_WAIT_USER_DATA_BEGIN_64_TAG = 0xc8020000u;
|
static constexpr uint32_t AGC_WAIT_USER_DATA_BEGIN_64_TAG = 0xc8020000u;
|
||||||
static constexpr uint32_t AGC_WAIT_USER_DATA_END_TAG = 0xc8000000u;
|
static constexpr uint32_t AGC_WAIT_USER_DATA_END_TAG = 0xc8000000u;
|
||||||
|
static constexpr uint32_t AGC_MEM_SEMAPHORE_SIGNAL_BEGIN_TAG = 0xc9010000u;
|
||||||
|
static constexpr uint32_t AGC_MEM_SEMAPHORE_WAIT_BEGIN_TAG = 0xc9020000u;
|
||||||
|
static constexpr uint32_t AGC_MEM_SEMAPHORE_END_TAG = 0xc9000000u;
|
||||||
static constexpr uint32_t AGC_WAIT_USER_DATA_BEGIN_DW = 4u;
|
static constexpr uint32_t AGC_WAIT_USER_DATA_BEGIN_DW = 4u;
|
||||||
static constexpr uint32_t AGC_INTERNAL_DATA_PACKET_SIZE_DW = 3u;
|
static constexpr uint32_t AGC_INTERNAL_DATA_PACKET_SIZE_DW = 3u;
|
||||||
|
|
||||||
@@ -3077,6 +3080,42 @@ uint32_t* KYTY_SYSV_ABI AgcAcbWaitRegMem(CommandBuffer* buf, uint8_t size, uint8
|
|||||||
poll_cycles);
|
poll_cycles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t* KYTY_SYSV_ABI AgcAcbMemSemaphore(CommandBuffer* buf, const volatile void* address,
|
||||||
|
uint8_t mailbox, uint8_t signal_type, uint8_t op) {
|
||||||
|
EXIT_NOT_IMPLEMENTED(op != 6u && op != 7u);
|
||||||
|
|
||||||
|
auto* cmd =
|
||||||
|
buf->AllocateDW(AGC_WAIT_USER_DATA_BEGIN_DW + AGC_INTERNAL_DATA_PACKET_SIZE_DW + 4u);
|
||||||
|
if (cmd == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto address_value = reinterpret_cast<uint64_t>(address);
|
||||||
|
const bool signal = (op == 6u);
|
||||||
|
cmd[0] = KYTY_PM4(AGC_WAIT_USER_DATA_BEGIN_DW, Pm4::IT_SET_UCONFIG_REG, 1u);
|
||||||
|
cmd[1] = AGC_INTERNAL_DATA_REGISTER;
|
||||||
|
cmd[2] = (signal ? AGC_MEM_SEMAPHORE_SIGNAL_BEGIN_TAG : AGC_MEM_SEMAPHORE_WAIT_BEGIN_TAG) |
|
||||||
|
static_cast<uint32_t>((address_value >> 32u) & 0xffffu);
|
||||||
|
cmd[3] = static_cast<uint32_t>(address_value);
|
||||||
|
|
||||||
|
// A signal pads with a NOP header where a wait ends with its end marker.
|
||||||
|
auto* semaphore = cmd + AGC_WAIT_USER_DATA_BEGIN_DW;
|
||||||
|
if (signal) {
|
||||||
|
semaphore[0] = KYTY_PM4(AGC_INTERNAL_DATA_PACKET_SIZE_DW, Pm4::IT_NOP, 0u);
|
||||||
|
semaphore += AGC_INTERNAL_DATA_PACKET_SIZE_DW;
|
||||||
|
}
|
||||||
|
semaphore[0] = KYTY_PM4(4, Pm4::IT_MEM_SEMAPHORE, 0u);
|
||||||
|
semaphore[1] = static_cast<uint32_t>(address_value) & ~0x7u;
|
||||||
|
semaphore[2] = static_cast<uint32_t>(address_value >> 32u);
|
||||||
|
semaphore[3] = (static_cast<uint32_t>(op) << 29u) | ((signal_type & 0x1u) << 20u) |
|
||||||
|
((mailbox & 0x1u) << 16u);
|
||||||
|
if (!signal) {
|
||||||
|
write_agc_internal_data_packet(semaphore + 4, AGC_MEM_SEMAPHORE_END_TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
uint64_t KYTY_SYSV_ABI AgcAcbWaitOnAddressGetSize(uint8_t size) {
|
uint64_t KYTY_SYSV_ABI AgcAcbWaitOnAddressGetSize(uint8_t size) {
|
||||||
PRINT_NAME();
|
PRINT_NAME();
|
||||||
|
|
||||||
@@ -3840,11 +3879,12 @@ bool AgcIsInternalDataPacket(uint32_t cmd_id, const uint32_t* payload) {
|
|||||||
const auto size_dw = KYTY_PM4_LEN(cmd_id);
|
const auto size_dw = KYTY_PM4_LEN(cmd_id);
|
||||||
if (size_dw == AGC_WAIT_USER_DATA_BEGIN_DW) {
|
if (size_dw == AGC_WAIT_USER_DATA_BEGIN_DW) {
|
||||||
const auto tag = payload[1] & 0xffff0000u;
|
const auto tag = payload[1] & 0xffff0000u;
|
||||||
return tag == AGC_WAIT_USER_DATA_BEGIN_32_TAG || tag == AGC_WAIT_USER_DATA_BEGIN_64_TAG;
|
return tag == AGC_WAIT_USER_DATA_BEGIN_32_TAG || tag == AGC_WAIT_USER_DATA_BEGIN_64_TAG ||
|
||||||
|
tag == AGC_MEM_SEMAPHORE_SIGNAL_BEGIN_TAG || tag == AGC_MEM_SEMAPHORE_WAIT_BEGIN_TAG;
|
||||||
}
|
}
|
||||||
|
|
||||||
return size_dw == AGC_INTERNAL_DATA_PACKET_SIZE_DW &&
|
return size_dw == AGC_INTERNAL_DATA_PACKET_SIZE_DW &&
|
||||||
(payload[1] == AGC_WAIT_USER_DATA_END_TAG ||
|
(payload[1] == AGC_WAIT_USER_DATA_END_TAG || payload[1] == AGC_MEM_SEMAPHORE_END_TAG ||
|
||||||
payload[1] == AGC_DRAW_INDIRECT_MULTI_BEGIN_TAG ||
|
payload[1] == AGC_DRAW_INDIRECT_MULTI_BEGIN_TAG ||
|
||||||
payload[1] == AGC_DRAW_INDIRECT_MULTI_END_TAG);
|
payload[1] == AGC_DRAW_INDIRECT_MULTI_END_TAG);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,6 +216,8 @@ uint32_t* KYTY_SYSV_ABI AgcAcbWaitRegMem(CommandBuffer* buf, uint8_t size, uint8
|
|||||||
uint8_t cache_policy, const volatile void* address,
|
uint8_t cache_policy, const volatile void* address,
|
||||||
uint64_t reference, uint64_t mask, uint32_t poll_cycles);
|
uint64_t reference, uint64_t mask, uint32_t poll_cycles);
|
||||||
uint64_t KYTY_SYSV_ABI AgcAcbWaitOnAddressGetSize(uint8_t size);
|
uint64_t KYTY_SYSV_ABI AgcAcbWaitOnAddressGetSize(uint8_t size);
|
||||||
|
uint32_t* KYTY_SYSV_ABI AgcAcbMemSemaphore(CommandBuffer* buf, const volatile void* address,
|
||||||
|
uint8_t mailbox, uint8_t signal_type, uint8_t op);
|
||||||
uint32_t* KYTY_SYSV_ABI AgcAcbDmaData(CommandBuffer* buf, uint8_t dst, uint8_t dst_cache_policy,
|
uint32_t* KYTY_SYSV_ABI AgcAcbDmaData(CommandBuffer* buf, uint8_t dst, uint8_t dst_cache_policy,
|
||||||
uint64_t dst_address_or_offset, uint8_t src,
|
uint64_t dst_address_or_offset, uint8_t src,
|
||||||
uint8_t src_cache_policy,
|
uint8_t src_cache_policy,
|
||||||
|
|||||||
@@ -519,7 +519,20 @@ void GameController::SetDevices(int active_id) {
|
|||||||
m_trigger_state = {};
|
m_trigger_state = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Input that changes no button or touch contact updates the newest unread sample, so a burst of
|
||||||
|
// stick or sensor events between two reads cannot push unread button or touch changes out.
|
||||||
void GameController::AddState() {
|
void GameController::AddState() {
|
||||||
|
if (m_states_num != 0) {
|
||||||
|
const auto newest = (m_first_state + m_states_num - 1) % STATES_MAX;
|
||||||
|
auto& last = m_states[newest];
|
||||||
|
if (!m_obtained[newest] && last.buttons == m_state.buttons &&
|
||||||
|
last.touch[0].down == m_state.touch[0].down &&
|
||||||
|
last.touch[1].down == m_state.touch[1].down) {
|
||||||
|
last = m_state;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (m_states_num >= STATES_MAX) {
|
if (m_states_num >= STATES_MAX) {
|
||||||
m_states_num = STATES_MAX - 1;
|
m_states_num = STATES_MAX - 1;
|
||||||
m_first_state = (m_first_state + 1) % STATES_MAX;
|
m_first_state = (m_first_state + 1) % STATES_MAX;
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ LIB_DEFINE(InitAgcDriver_1) {
|
|||||||
LIB_FUNC("b-oySn+G2tE", Gen5::AgcAcbJumpGetSize);
|
LIB_FUNC("b-oySn+G2tE", Gen5::AgcAcbJumpGetSize);
|
||||||
LIB_FUNC("DwICrVxerkY", Gen5::AgcAcbRewind);
|
LIB_FUNC("DwICrVxerkY", Gen5::AgcAcbRewind);
|
||||||
LIB_FUNC("htn36gPnBk4", Gen5::AgcAcbWaitRegMem);
|
LIB_FUNC("htn36gPnBk4", Gen5::AgcAcbWaitRegMem);
|
||||||
|
LIB_FUNC("q4VuU-QsLOE", Gen5::AgcAcbMemSemaphore);
|
||||||
LIB_FUNC("idlaArvdXEs", Gen5::AgcAcbWaitOnAddressGetSize);
|
LIB_FUNC("idlaArvdXEs", Gen5::AgcAcbWaitOnAddressGetSize);
|
||||||
LIB_FUNC("-RnpfpxIhec", Gen5::AgcAcbDmaData);
|
LIB_FUNC("-RnpfpxIhec", Gen5::AgcAcbDmaData);
|
||||||
LIB_FUNC("qzMN2XKGA4k", Gen5::AgcAcbCopyData);
|
LIB_FUNC("qzMN2XKGA4k", Gen5::AgcAcbCopyData);
|
||||||
|
|||||||
+119
-88
@@ -13,6 +13,7 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <span>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace Libs::Audio {
|
namespace Libs::Audio {
|
||||||
@@ -168,17 +169,12 @@ struct AudioOut2MasteringStatesV2 {
|
|||||||
|
|
||||||
static std::atomic_uint64_t g_audioout2_next_context {1};
|
static std::atomic_uint64_t g_audioout2_next_context {1};
|
||||||
static std::atomic_uint64_t g_audioout2_next_port {1};
|
static std::atomic_uint64_t g_audioout2_next_port {1};
|
||||||
static AudioOut2UserHandle g_audioout2_next_user = 1;
|
static AudioOut2UserHandle g_audioout2_next_user = 1;
|
||||||
|
|
||||||
struct AudioOut2ContextState {
|
static constexpr uint32_t AUDIOOUT2_CHANNEL_PORTS_MAX = 32;
|
||||||
bool used = false;
|
static constexpr uint32_t AUDIOOUT2_OBJECT_PORTS_MAX = 512;
|
||||||
AudioOut2ContextHandle handle = 0;
|
static constexpr uint32_t AUDIOOUT2_PORT_SLOT_BITS = 10;
|
||||||
uint32_t queue_depth = 4;
|
static constexpr uint32_t AUDIOOUT2_CONTEXT_SLOT_BITS = 4;
|
||||||
uint32_t queued = 0;
|
|
||||||
uint32_t num_grains = 512;
|
|
||||||
uint64_t last_update = 0;
|
|
||||||
int object_audio_handle = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct AudioOut2PortStateEntry {
|
struct AudioOut2PortStateEntry {
|
||||||
bool used = false;
|
bool used = false;
|
||||||
@@ -196,6 +192,19 @@ struct AudioOut2PortStateEntry {
|
|||||||
bool attributes_locked = false;
|
bool attributes_locked = false;
|
||||||
std::vector<uint8_t> pcm_data;
|
std::vector<uint8_t> pcm_data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct AudioOut2ContextState {
|
||||||
|
bool used = false;
|
||||||
|
AudioOut2ContextHandle handle = 0;
|
||||||
|
uint32_t queue_depth = 4;
|
||||||
|
uint32_t queued = 0;
|
||||||
|
uint32_t num_grains = 512;
|
||||||
|
uint64_t last_update = 0;
|
||||||
|
int object_audio_handle = 0;
|
||||||
|
uint32_t max_ports = 0;
|
||||||
|
// Channel slots precede object slots; each partition has its own creation limit.
|
||||||
|
std::vector<AudioOut2PortStateEntry> ports;
|
||||||
|
};
|
||||||
struct AudioOut2SpeakerArrayState {
|
struct AudioOut2SpeakerArrayState {
|
||||||
bool used = false;
|
bool used = false;
|
||||||
uint32_t num_speakers = 0;
|
uint32_t num_speakers = 0;
|
||||||
@@ -223,13 +232,16 @@ struct AudioOut2Submission {
|
|||||||
static Common::Mutex g_audioout2_state_mutex;
|
static Common::Mutex g_audioout2_state_mutex;
|
||||||
static std::array<AudioOut2ContextState, 16> g_audioout2_contexts;
|
static std::array<AudioOut2ContextState, 16> g_audioout2_contexts;
|
||||||
static std::array<AudioOut2Submission, 16> g_audioout2_submissions;
|
static std::array<AudioOut2Submission, 16> g_audioout2_submissions;
|
||||||
static std::array<AudioOut2PortStateEntry, 256> g_audioout2_ports;
|
static std::vector<AudioOut2UserHandle> g_audioout2_users;
|
||||||
static std::vector<AudioOut2UserHandle> g_audioout2_users;
|
|
||||||
static Common::Mutex g_audioout2_speaker_array_mutex;
|
static Common::Mutex g_audioout2_speaker_array_mutex;
|
||||||
static std::array<AudioOut2SpeakerArrayState, 32> g_audioout2_speaker_arrays;
|
static std::array<AudioOut2SpeakerArrayState, 32> g_audioout2_speaker_arrays;
|
||||||
static Common::Mutex g_audioout2_latency_mutex;
|
static Common::Mutex g_audioout2_latency_mutex;
|
||||||
static std::array<AudioOut2LatencyState, 16> g_audioout2_latencies;
|
static std::array<AudioOut2LatencyState, 16> g_audioout2_latencies;
|
||||||
|
|
||||||
|
static_assert(AUDIOOUT2_CHANNEL_PORTS_MAX + AUDIOOUT2_OBJECT_PORTS_MAX <=
|
||||||
|
(1u << AUDIOOUT2_PORT_SLOT_BITS));
|
||||||
|
static_assert(g_audioout2_contexts.size() == (1u << AUDIOOUT2_CONTEXT_SLOT_BITS));
|
||||||
|
|
||||||
static constexpr int AUDIO_OUT2_ERROR_NOT_READY = -2144960504; /* 0x80268008 */
|
static constexpr int AUDIO_OUT2_ERROR_NOT_READY = -2144960504; /* 0x80268008 */
|
||||||
static constexpr int AUDIO_OUT2_ERROR_PORT_FULL = -2144960494; /* 0x80268012 */
|
static constexpr int AUDIO_OUT2_ERROR_PORT_FULL = -2144960494; /* 0x80268012 */
|
||||||
static constexpr int AUDIO_OUT2_ERROR_INVALID_PARAM = -2144960511; /* 0x80268001 */
|
static constexpr int AUDIO_OUT2_ERROR_INVALID_PARAM = -2144960511; /* 0x80268001 */
|
||||||
@@ -262,6 +274,11 @@ static AudioOut2Submission& audioout2_submission(const AudioOut2ContextState& co
|
|||||||
return g_audioout2_submissions[&context - g_audioout2_contexts.data()];
|
return g_audioout2_submissions[&context - g_audioout2_contexts.data()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ticks of a sixth of a microsecond: one 48 kHz sample lasts exactly 125 of them.
|
||||||
|
static uint64_t audioout2_clock() {
|
||||||
|
return LibKernel::KernelGetProcessTime() * 6;
|
||||||
|
}
|
||||||
|
|
||||||
static uint32_t audioout2_grain_micros(uint32_t grains) {
|
static uint32_t audioout2_grain_micros(uint32_t grains) {
|
||||||
const auto sample_count = (grains == 0 ? 512u : grains);
|
const auto sample_count = (grains == 0 ? 512u : grains);
|
||||||
return std::max<uint32_t>((sample_count * 1000000u) / 48000u, 1000u);
|
return std::max<uint32_t>((sample_count * 1000000u) / 48000u, 1000u);
|
||||||
@@ -331,49 +348,51 @@ static void audioout2_update_context_locked(AudioOut2ContextState* state) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto now = LibKernel::KernelGetProcessTime();
|
const auto now = audioout2_clock();
|
||||||
if (state->last_update == 0 || state->queued == 0) {
|
if (state->last_update == 0 || state->queued == 0) {
|
||||||
state->last_update = now;
|
state->last_update = now;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (now <= state->last_update) {
|
||||||
const auto grain_micros = static_cast<uint64_t>(audioout2_grain_micros(state->num_grains));
|
|
||||||
if (now <= state->last_update || grain_micros == 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto elapsed = now - state->last_update;
|
const auto grain = uint64_t {state->num_grains} * 125;
|
||||||
const auto drained = std::min<uint64_t>(state->queued, elapsed / grain_micros);
|
const auto drained = std::min<uint64_t>(state->queued, (now - state->last_update) / grain);
|
||||||
if (drained == 0) {
|
if (drained == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state->queued -= static_cast<uint32_t>(drained);
|
state->queued -= static_cast<uint32_t>(drained);
|
||||||
state->last_update += drained * grain_micros;
|
state->last_update += drained * grain;
|
||||||
if (state->queued == 0) {
|
if (state->queued == 0) {
|
||||||
state->last_update = now;
|
state->last_update = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static AudioOut2PortStateEntry* audioout2_find_port_locked(AudioOut2PortHandle port) {
|
static AudioOut2PortStateEntry* audioout2_find_port_locked(AudioOut2PortHandle port) {
|
||||||
for (auto& state: g_audioout2_ports) {
|
const auto context_slot =
|
||||||
if (state.used && state.handle == port) {
|
(port >> AUDIOOUT2_PORT_SLOT_BITS) & ((1u << AUDIOOUT2_CONTEXT_SLOT_BITS) - 1u);
|
||||||
return &state;
|
const auto port_slot = port & ((1u << AUDIOOUT2_PORT_SLOT_BITS) - 1u);
|
||||||
}
|
auto& context = g_audioout2_contexts[context_slot];
|
||||||
|
if (context.used && port_slot < context.ports.size()) {
|
||||||
|
auto& state = context.ports[port_slot];
|
||||||
|
if (state.used && state.handle == port) return &state;
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
static size_t audioout2_pcm_size(const AudioOut2PortStateEntry& state) {
|
static size_t audioout2_pcm_size(const AudioOut2PortStateEntry& state) {
|
||||||
const auto bytes_per_sample = (state.data_format & 0x7fu) == 1 ? sizeof(int16_t) : sizeof(float);
|
const auto bytes_per_sample =
|
||||||
|
(state.data_format & 0x7fu) == 1 ? sizeof(int16_t) : sizeof(float);
|
||||||
return static_cast<size_t>(state.samples_num) *
|
return static_cast<size_t>(state.samples_num) *
|
||||||
audioout2_data_format_channels(state.data_format) * bytes_per_sample;
|
audioout2_data_format_channels(state.data_format) * bytes_per_sample;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool audioout2_context_has_queueable_device(const AudioOut2ContextState& context) {
|
static bool audioout2_context_has_queueable_device(const AudioOut2ContextState& context) {
|
||||||
for (const auto& state: g_audioout2_ports) {
|
for (const auto& state: context.ports) {
|
||||||
if (state.used && state.context == context.handle && !state.pcm_data.empty()) {
|
if (state.used && !state.pcm_data.empty()) {
|
||||||
const int handle = audioout2_port_type_is_object(state.port_type)
|
const int handle = audioout2_port_type_is_object(state.port_type)
|
||||||
? context.object_audio_handle
|
? context.object_audio_handle
|
||||||
: state.audio_handle;
|
: state.audio_handle;
|
||||||
@@ -427,18 +446,21 @@ static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, AudioOut2S
|
|||||||
out.object_mix.clear();
|
out.object_mix.clear();
|
||||||
std::vector<AudioInternal::OutputParam> params;
|
std::vector<AudioInternal::OutputParam> params;
|
||||||
params.reserve(AudioInternal::OUT_PORTS_MAX);
|
params.reserve(AudioInternal::OUT_PORTS_MAX);
|
||||||
for (const auto& state: g_audioout2_ports) {
|
// A port's PCM covers one grain: the push consumes it, so a port the game stops feeding
|
||||||
if (!state.used || state.context != ctx || state.pcm_data.empty()) {
|
// falls silent.
|
||||||
|
for (auto& state: context->ports) {
|
||||||
|
if (!state.used || state.pcm_data.empty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (audioout2_port_type_is_object(state.port_type)) {
|
if (audioout2_port_type_is_object(state.port_type)) {
|
||||||
audioout2_mix_object(out.object_mix, context->num_grains, state);
|
audioout2_mix_object(out.object_mix, context->num_grains, state);
|
||||||
} else if (state.audio_handle > 0 && params.size() < AudioInternal::OUT_PORTS_MAX) {
|
} else if (state.audio_handle > 0 && params.size() < AudioInternal::OUT_PORTS_MAX) {
|
||||||
const auto i = params.size();
|
const auto i = params.size();
|
||||||
out.pcm[i] = state.pcm_data;
|
out.pcm[i].swap(state.pcm_data);
|
||||||
out.gains[i] = state.gains;
|
out.gains[i] = state.gains;
|
||||||
params.push_back({state.audio_handle, out.pcm[i].data(), out.gains[i].data()});
|
params.push_back({state.audio_handle, out.pcm[i].data(), out.gains[i].data()});
|
||||||
}
|
}
|
||||||
|
state.pcm_data.clear();
|
||||||
}
|
}
|
||||||
if (!out.object_mix.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
|
if (!out.object_mix.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
|
||||||
params.push_back({context->object_audio_handle, out.object_mix.data()});
|
params.push_back({context->object_audio_handle, out.object_mix.data()});
|
||||||
@@ -467,22 +489,28 @@ int KYTY_SYSV_ABI AudioOut2ContextResetParam(AudioOut2ContextParam* params) {
|
|||||||
EXIT_NOT_IMPLEMENTED(params == nullptr);
|
EXIT_NOT_IMPLEMENTED(params == nullptr);
|
||||||
|
|
||||||
std::memset(params, 0, sizeof(AudioOut2ContextParam));
|
std::memset(params, 0, sizeof(AudioOut2ContextParam));
|
||||||
params->max_ports = 256;
|
params->max_ports = 8;
|
||||||
params->max_object_ports = 256;
|
params->queue_depth = 1;
|
||||||
params->guarantee_object_ports = 0;
|
params->num_grains = 256;
|
||||||
params->queue_depth = 4;
|
|
||||||
params->num_grains = 512;
|
|
||||||
params->flags = 1;
|
|
||||||
|
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool audioout2_valid_context_capacity(const AudioOut2ContextParam& params) {
|
||||||
|
return params.max_ports <= AUDIOOUT2_CHANNEL_PORTS_MAX &&
|
||||||
|
params.max_object_ports <= AUDIOOUT2_OBJECT_PORTS_MAX &&
|
||||||
|
params.guarantee_object_ports <= params.max_object_ports;
|
||||||
|
}
|
||||||
|
|
||||||
int KYTY_SYSV_ABI AudioOut2ContextQueryMemory(const AudioOut2ContextParam* params,
|
int KYTY_SYSV_ABI AudioOut2ContextQueryMemory(const AudioOut2ContextParam* params,
|
||||||
size_t* memory_size) {
|
size_t* memory_size) {
|
||||||
PRINT_NAME();
|
PRINT_NAME();
|
||||||
|
|
||||||
EXIT_NOT_IMPLEMENTED(params == nullptr);
|
EXIT_NOT_IMPLEMENTED(params == nullptr);
|
||||||
EXIT_NOT_IMPLEMENTED(memory_size == nullptr);
|
EXIT_NOT_IMPLEMENTED(memory_size == nullptr);
|
||||||
|
if (!audioout2_valid_context_capacity(*params)) {
|
||||||
|
return AUDIO_OUT2_ERROR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
const auto queue_depth = (params->queue_depth == 0 ? 4u : params->queue_depth);
|
const auto queue_depth = (params->queue_depth == 0 ? 4u : params->queue_depth);
|
||||||
*memory_size = 0x10000u + static_cast<size_t>(queue_depth) * 0x590u;
|
*memory_size = 0x10000u + static_cast<size_t>(queue_depth) * 0x590u;
|
||||||
@@ -498,6 +526,9 @@ int KYTY_SYSV_ABI AudioOut2ContextCreate(const AudioOut2ContextParam* params, vo
|
|||||||
|
|
||||||
EXIT_NOT_IMPLEMENTED(params == nullptr);
|
EXIT_NOT_IMPLEMENTED(params == nullptr);
|
||||||
EXIT_NOT_IMPLEMENTED(ctx == nullptr);
|
EXIT_NOT_IMPLEMENTED(ctx == nullptr);
|
||||||
|
if (!audioout2_valid_context_capacity(*params)) {
|
||||||
|
return AUDIO_OUT2_ERROR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
const auto num_grains = params->num_grains == 0 ? 512u : params->num_grains;
|
const auto num_grains = params->num_grains == 0 ? 512u : params->num_grains;
|
||||||
const int object_audio_handle =
|
const int object_audio_handle =
|
||||||
@@ -524,8 +555,10 @@ int KYTY_SYSV_ABI AudioOut2ContextCreate(const AudioOut2ContextParam* params, vo
|
|||||||
state->handle = *ctx;
|
state->handle = *ctx;
|
||||||
state->queue_depth = params->queue_depth == 0 ? 4u : params->queue_depth;
|
state->queue_depth = params->queue_depth == 0 ? 4u : params->queue_depth;
|
||||||
state->num_grains = num_grains;
|
state->num_grains = num_grains;
|
||||||
state->last_update = LibKernel::KernelGetProcessTime();
|
state->last_update = audioout2_clock();
|
||||||
state->object_audio_handle = object_audio_handle;
|
state->object_audio_handle = object_audio_handle;
|
||||||
|
state->max_ports = params->max_ports;
|
||||||
|
state->ports.resize(params->max_ports + params->max_object_ports);
|
||||||
g_audioout2_state_mutex.Unlock();
|
g_audioout2_state_mutex.Unlock();
|
||||||
|
|
||||||
LOGF("\t buffer = 0x%016" PRIx64 "\n"
|
LOGF("\t buffer = 0x%016" PRIx64 "\n"
|
||||||
@@ -541,8 +574,8 @@ int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
|
|||||||
PRINT_NAME();
|
PRINT_NAME();
|
||||||
LOGF("\t ctx = 0x%016" PRIx64 "\n", ctx);
|
LOGF("\t ctx = 0x%016" PRIx64 "\n", ctx);
|
||||||
|
|
||||||
std::array<int, 257> audio_handles {};
|
std::array<int, AUDIOOUT2_CHANNEL_PORTS_MAX + 1> audio_handles {};
|
||||||
size_t audio_handles_num = 0;
|
size_t audio_handles_num = 0;
|
||||||
|
|
||||||
g_audioout2_state_mutex.Lock();
|
g_audioout2_state_mutex.Lock();
|
||||||
auto* state = audioout2_find_context_locked(ctx);
|
auto* state = audioout2_find_context_locked(ctx);
|
||||||
@@ -550,15 +583,12 @@ int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
|
|||||||
if (state->object_audio_handle > 0) {
|
if (state->object_audio_handle > 0) {
|
||||||
audio_handles[audio_handles_num++] = state->object_audio_handle;
|
audio_handles[audio_handles_num++] = state->object_audio_handle;
|
||||||
}
|
}
|
||||||
*state = AudioOut2ContextState {};
|
for (const auto& port_state: state->ports) {
|
||||||
}
|
if (port_state.used && port_state.audio_handle > 0) {
|
||||||
for (auto& port_state: g_audioout2_ports) {
|
|
||||||
if (port_state.used && port_state.context == ctx) {
|
|
||||||
if (port_state.audio_handle > 0 && audio_handles_num < audio_handles.size()) {
|
|
||||||
audio_handles[audio_handles_num++] = port_state.audio_handle;
|
audio_handles[audio_handles_num++] = port_state.audio_handle;
|
||||||
}
|
}
|
||||||
port_state = AudioOut2PortStateEntry {};
|
|
||||||
}
|
}
|
||||||
|
*state = AudioOut2ContextState {};
|
||||||
}
|
}
|
||||||
g_audioout2_state_mutex.Unlock();
|
g_audioout2_state_mutex.Unlock();
|
||||||
|
|
||||||
@@ -586,9 +616,7 @@ int KYTY_SYSV_ABI AudioOut2ContextAdvance(AudioOut2ContextHandle ctx) {
|
|||||||
g_audioout2_state_mutex.Lock();
|
g_audioout2_state_mutex.Lock();
|
||||||
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
|
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
|
||||||
audioout2_update_context_locked(state);
|
audioout2_update_context_locked(state);
|
||||||
}
|
for (auto& port: state->ports) {
|
||||||
for (auto& port: g_audioout2_ports) {
|
|
||||||
if (port.used && port.context == ctx) {
|
|
||||||
port.attributes_locked = false;
|
port.attributes_locked = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -612,7 +640,7 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
|
|||||||
sleep_micros = audioout2_grain_micros(state->num_grains);
|
sleep_micros = audioout2_grain_micros(state->num_grains);
|
||||||
if (state->queued < state->queue_depth || use_device_clock) {
|
if (state->queued < state->queue_depth || use_device_clock) {
|
||||||
if (state->queued == 0) {
|
if (state->queued == 0) {
|
||||||
state->last_update = LibKernel::KernelGetProcessTime();
|
state->last_update = audioout2_clock();
|
||||||
}
|
}
|
||||||
if (state->queued < state->queue_depth) {
|
if (state->queued < state->queue_depth) {
|
||||||
state->queued++;
|
state->queued++;
|
||||||
@@ -663,21 +691,19 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
|
|||||||
EXIT_NOT_IMPLEMENTED(params == nullptr);
|
EXIT_NOT_IMPLEMENTED(params == nullptr);
|
||||||
EXIT_NOT_IMPLEMENTED(port == nullptr);
|
EXIT_NOT_IMPLEMENTED(port == nullptr);
|
||||||
|
|
||||||
const auto next_port = g_audioout2_next_port.fetch_add(1, std::memory_order_relaxed);
|
|
||||||
const auto audio_format = audioout2_data_format_to_audio_format(params->data_format);
|
const auto audio_format = audioout2_data_format_to_audio_format(params->data_format);
|
||||||
const auto audio_type = audioout2_port_type_to_audio_out_type(params->port_type);
|
const auto audio_type = audioout2_port_type_to_audio_out_type(params->port_type);
|
||||||
|
const bool is_object = audioout2_port_type_is_object(params->port_type);
|
||||||
|
|
||||||
g_audioout2_state_mutex.Lock();
|
g_audioout2_state_mutex.Lock();
|
||||||
const auto* context_state = audioout2_find_context_locked(ctx);
|
auto* context_state = audioout2_find_context_locked(ctx);
|
||||||
if (context_state == nullptr) {
|
if (context_state == nullptr) {
|
||||||
g_audioout2_state_mutex.Unlock();
|
g_audioout2_state_mutex.Unlock();
|
||||||
return AUDIO_OUT2_ERROR_INVALID_PARAM;
|
return AUDIO_OUT2_ERROR_INVALID_PARAM;
|
||||||
}
|
}
|
||||||
if (audioout2_port_type_is_object(params->port_type) &&
|
if (is_object && (params->port_type != 0x100 || params->sampling_freq != 48000 ||
|
||||||
(params->port_type != 0x100 || context_state->object_audio_handle == 0 ||
|
(audio_format != AudioInternal::Format::FloatMono &&
|
||||||
params->sampling_freq != 48000 ||
|
audio_format != AudioInternal::Format::Signed16bitMono))) {
|
||||||
(audio_format != AudioInternal::Format::FloatMono &&
|
|
||||||
audio_format != AudioInternal::Format::Signed16bitMono))) {
|
|
||||||
g_audioout2_state_mutex.Unlock();
|
g_audioout2_state_mutex.Unlock();
|
||||||
return AUDIO_OUT2_ERROR_INVALID_PARAM;
|
return AUDIO_OUT2_ERROR_INVALID_PARAM;
|
||||||
}
|
}
|
||||||
@@ -688,42 +714,45 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
|
|||||||
g_audioout2_state_mutex.Unlock();
|
g_audioout2_state_mutex.Unlock();
|
||||||
return AUDIO_OUT2_ERROR_INVALID_PARAM;
|
return AUDIO_OUT2_ERROR_INVALID_PARAM;
|
||||||
}
|
}
|
||||||
AudioOut2PortStateEntry* port_state = nullptr;
|
auto slots = std::span(context_state->ports);
|
||||||
for (auto& candidate: g_audioout2_ports) {
|
slots =
|
||||||
if (!candidate.used) {
|
is_object ? slots.subspan(context_state->max_ports) : slots.first(context_state->max_ports);
|
||||||
port_state = &candidate;
|
const auto available = std::find_if(slots.begin(), slots.end(),
|
||||||
break;
|
[](const auto& candidate) { return !candidate.used; });
|
||||||
}
|
if (available == slots.end()) {
|
||||||
}
|
g_audioout2_state_mutex.Unlock();
|
||||||
if (port_state != nullptr) {
|
|
||||||
*port_state = AudioOut2PortStateEntry {};
|
|
||||||
port_state->used = true;
|
|
||||||
port_state->handle = next_port;
|
|
||||||
port_state->context = ctx;
|
|
||||||
port_state->user = params->user_handle;
|
|
||||||
port_state->port_type = params->port_type;
|
|
||||||
port_state->data_format = params->data_format;
|
|
||||||
port_state->sampling_freq = params->sampling_freq;
|
|
||||||
port_state->samples_num = samples_num;
|
|
||||||
port_state->audio_format = audio_format;
|
|
||||||
port_state->gains.fill(1.0f);
|
|
||||||
}
|
|
||||||
g_audioout2_state_mutex.Unlock();
|
|
||||||
|
|
||||||
if (port_state == nullptr) {
|
|
||||||
return AUDIO_OUT2_ERROR_PORT_FULL;
|
return AUDIO_OUT2_ERROR_PORT_FULL;
|
||||||
}
|
}
|
||||||
|
const auto serial = g_audioout2_next_port.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
const auto context_slot = static_cast<uint64_t>(context_state - g_audioout2_contexts.data());
|
||||||
|
const auto port_slot = static_cast<uint64_t>(&*available - context_state->ports.data());
|
||||||
|
const auto next_port = (serial << (AUDIOOUT2_PORT_SLOT_BITS + AUDIOOUT2_CONTEXT_SLOT_BITS)) |
|
||||||
|
(context_slot << AUDIOOUT2_PORT_SLOT_BITS) | port_slot;
|
||||||
|
auto* port_state = &*available;
|
||||||
|
*port_state = AudioOut2PortStateEntry {};
|
||||||
|
port_state->used = true;
|
||||||
|
port_state->handle = next_port;
|
||||||
|
port_state->context = ctx;
|
||||||
|
port_state->user = params->user_handle;
|
||||||
|
port_state->port_type = params->port_type;
|
||||||
|
port_state->data_format = params->data_format;
|
||||||
|
port_state->sampling_freq = params->sampling_freq;
|
||||||
|
port_state->samples_num = samples_num;
|
||||||
|
port_state->audio_format = audio_format;
|
||||||
|
port_state->gains.fill(1.0f);
|
||||||
|
g_audioout2_state_mutex.Unlock();
|
||||||
|
|
||||||
int audio_handle = 0;
|
int audio_handle = 0;
|
||||||
|
|
||||||
if (audio_format != AudioInternal::Format::Unknown &&
|
if (audio_format != AudioInternal::Format::Unknown && !is_object) {
|
||||||
!audioout2_port_type_is_object(params->port_type)) {
|
|
||||||
audio_handle = AudioInternal::AudioOutOpen(audio_type, samples_num, params->sampling_freq,
|
audio_handle = AudioInternal::AudioOutOpen(audio_type, samples_num, params->sampling_freq,
|
||||||
audio_format);
|
audio_format);
|
||||||
}
|
}
|
||||||
|
|
||||||
g_audioout2_state_mutex.Lock();
|
g_audioout2_state_mutex.Lock();
|
||||||
const bool reserved = port_state->used && port_state->handle == next_port;
|
// Context destruction may have freed or replaced the slots while the backend opened.
|
||||||
|
port_state = audioout2_find_port_locked(next_port);
|
||||||
|
const bool reserved = port_state != nullptr;
|
||||||
if (reserved) {
|
if (reserved) {
|
||||||
port_state->audio_handle = audio_handle;
|
port_state->audio_handle = audio_handle;
|
||||||
}
|
}
|
||||||
@@ -735,7 +764,7 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
|
|||||||
|
|
||||||
*port = next_port;
|
*port = next_port;
|
||||||
|
|
||||||
if (next_port <= 16 || (next_port % 600) == 0) {
|
if (serial <= 16 || (serial % 600) == 0) {
|
||||||
PRINT_NAME();
|
PRINT_NAME();
|
||||||
LOGF("\t ctx = 0x%016" PRIx64 "\n"
|
LOGF("\t ctx = 0x%016" PRIx64 "\n"
|
||||||
"\t port = 0x%016" PRIx64 "\n"
|
"\t port = 0x%016" PRIx64 "\n"
|
||||||
@@ -901,21 +930,23 @@ int KYTY_SYSV_ABI AudioOut2UserDestroy(AudioOut2UserHandle handle) {
|
|||||||
PRINT_NAME();
|
PRINT_NAME();
|
||||||
LOGF("\t handle = 0x%016" PRIx64 "\n", static_cast<uint64_t>(handle));
|
LOGF("\t handle = 0x%016" PRIx64 "\n", static_cast<uint64_t>(handle));
|
||||||
Common::LockGuard lock(g_audioout2_state_mutex);
|
Common::LockGuard lock(g_audioout2_state_mutex);
|
||||||
const auto it = std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle);
|
const auto it = std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle);
|
||||||
if (it == g_audioout2_users.end()) {
|
if (it == g_audioout2_users.end()) {
|
||||||
return AUDIO_OUT2_ERROR_INVALID_PARAM;
|
return AUDIO_OUT2_ERROR_INVALID_PARAM;
|
||||||
}
|
}
|
||||||
if (std::any_of(g_audioout2_ports.begin(), g_audioout2_ports.end(),
|
for (const auto& context: g_audioout2_contexts) {
|
||||||
[handle](const auto& port) { return port.used && port.user == handle; })) {
|
if (std::any_of(context.ports.begin(), context.ports.end(),
|
||||||
return AUDIO_OUT2_ERROR_BUSY;
|
[handle](const auto& port) { return port.used && port.user == handle; })) {
|
||||||
|
return AUDIO_OUT2_ERROR_BUSY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
g_audioout2_users.erase(it);
|
g_audioout2_users.erase(it);
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int KYTY_SYSV_ABI AudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle,
|
int KYTY_SYSV_ABI AudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle,
|
||||||
uint32_t* context_attributes,
|
uint32_t* context_attributes,
|
||||||
uint32_t* port_attributes) {
|
uint32_t* port_attributes) {
|
||||||
if (context_attributes == nullptr || port_attributes == nullptr) {
|
if (context_attributes == nullptr || port_attributes == nullptr) {
|
||||||
return AUDIO_OUT2_ERROR_INVALID_PARAM;
|
return AUDIO_OUT2_ERROR_INVALID_PARAM;
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-10
@@ -38,8 +38,8 @@ struct Global {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct Shadow {
|
struct Shadow {
|
||||||
uintptr_t start = 0;
|
uintptr_t start = 0;
|
||||||
int count = 0;
|
uint64_t covered_bytes = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AsanContext {
|
struct AsanContext {
|
||||||
@@ -50,6 +50,14 @@ struct AsanContext {
|
|||||||
static uint32_t g_fake_stack = 0;
|
static uint32_t g_fake_stack = 0;
|
||||||
static AsanContext* g_ctx = nullptr;
|
static AsanContext* g_ctx = nullptr;
|
||||||
|
|
||||||
|
// Allocation and release callbacks can partition the same guest range differently.
|
||||||
|
static uint64_t ShadowCoverage(uintptr_t page, uintptr_t min_addr, uintptr_t max_addr) {
|
||||||
|
const auto guest_start = (page - 0x10000000000ull) << 3u;
|
||||||
|
const auto guest_end = guest_start + 0x80000ull - 1;
|
||||||
|
return std::min<uintptr_t>(max_addr, guest_end) - std::max<uintptr_t>(min_addr, guest_start) +
|
||||||
|
1;
|
||||||
|
}
|
||||||
|
|
||||||
static void AllocShadow(uintptr_t min_addr, uintptr_t max_addr) {
|
static void AllocShadow(uintptr_t min_addr, uintptr_t max_addr) {
|
||||||
EXIT_IF(g_ctx == nullptr);
|
EXIT_IF(g_ctx == nullptr);
|
||||||
|
|
||||||
@@ -67,10 +75,11 @@ static void AllocShadow(uintptr_t min_addr, uintptr_t max_addr) {
|
|||||||
static_cast<uint64_t>(min_shadow), static_cast<uint64_t>(size));
|
static_cast<uint64_t>(min_shadow), static_cast<uint64_t>(size));
|
||||||
|
|
||||||
for (uintptr_t page = min_shadow; page <= max_shadow; page += page_size) {
|
for (uintptr_t page = min_shadow; page <= max_shadow; page += page_size) {
|
||||||
const auto it = std::find_if(g_ctx->shadows.begin(), g_ctx->shadows.end(),
|
const auto bytes = ShadowCoverage(page, min_addr, max_addr);
|
||||||
[page](const Shadow& s) { return s.start == page; });
|
const auto it = std::find_if(g_ctx->shadows.begin(), g_ctx->shadows.end(),
|
||||||
|
[page](const Shadow& s) { return s.start == page; });
|
||||||
if (it != g_ctx->shadows.end()) {
|
if (it != g_ctx->shadows.end()) {
|
||||||
it->count++;
|
it->covered_bytes += bytes;
|
||||||
} else {
|
} else {
|
||||||
if (!Common::VirtualMemory::AllocFixed(page, page_size,
|
if (!Common::VirtualMemory::AllocFixed(page, page_size,
|
||||||
Common::VirtualMemory::Mode::ReadWrite)) {
|
Common::VirtualMemory::Mode::ReadWrite)) {
|
||||||
@@ -78,8 +87,8 @@ static void AllocShadow(uintptr_t min_addr, uintptr_t max_addr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Shadow s;
|
Shadow s;
|
||||||
s.start = page;
|
s.start = page;
|
||||||
s.count = 1;
|
s.covered_bytes = bytes;
|
||||||
g_ctx->shadows.push_back(s);
|
g_ctx->shadows.push_back(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,14 +114,15 @@ static void FreeShadow(uintptr_t min_addr, uintptr_t max_addr) {
|
|||||||
const auto it = std::find_if(g_ctx->shadows.begin(), g_ctx->shadows.end(),
|
const auto it = std::find_if(g_ctx->shadows.begin(), g_ctx->shadows.end(),
|
||||||
[page](const Shadow& s) { return s.start == page; });
|
[page](const Shadow& s) { return s.start == page; });
|
||||||
if (it != g_ctx->shadows.end()) {
|
if (it != g_ctx->shadows.end()) {
|
||||||
it->count--;
|
const auto bytes = ShadowCoverage(page, min_addr, max_addr);
|
||||||
|
if (bytes >= it->covered_bytes) {
|
||||||
if (it->count <= 0) {
|
|
||||||
if (!Common::VirtualMemory::Free(page)) {
|
if (!Common::VirtualMemory::Free(page)) {
|
||||||
EXIT("can't free shadow memory\n");
|
EXIT("can't free shadow memory\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
g_ctx->shadows.erase(it);
|
g_ctx->shadows.erase(it);
|
||||||
|
} else {
|
||||||
|
it->covered_bytes -= bytes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,6 +136,14 @@ static void KernelFree(uintptr_t addr, size_t size) {
|
|||||||
FreeShadow(addr, addr + size - 1);
|
FreeShadow(addr, addr + size - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||||
|
void TestRegisterMemoryCallbacks() {
|
||||||
|
EXIT_IF(g_ctx != nullptr);
|
||||||
|
g_ctx = new AsanContext;
|
||||||
|
LibKernel::Memory::RegisterCallbacks(KernelAlloc, KernelFree);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
static void KYTY_SYSV_ABI asan_init() {
|
static void KYTY_SYSV_ABI asan_init() {
|
||||||
PRINT_NAME();
|
PRINT_NAME();
|
||||||
|
|
||||||
|
|||||||
+20
-46
@@ -2,10 +2,12 @@
|
|||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/common.h"
|
#include "common/common.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
#include "common/stringUtils.h"
|
||||||
#include "libs/errno.h"
|
#include "libs/errno.h"
|
||||||
#include "libs/libs.h"
|
#include "libs/libs.h"
|
||||||
#include "loader/symbolDatabase.h"
|
#include "loader/symbolDatabase.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
@@ -675,19 +677,6 @@ static bool font_is_italic(const FontState* font) {
|
|||||||
font->render_effect_slant != 0.0f;
|
font->render_effect_slant != 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char* fallback_font_file_name(bool bold, bool italic) {
|
|
||||||
if (bold && italic) {
|
|
||||||
return "Roboto-BoldItalic.ttf";
|
|
||||||
}
|
|
||||||
if (bold) {
|
|
||||||
return "Roboto-Bold.ttf";
|
|
||||||
}
|
|
||||||
if (italic) {
|
|
||||||
return "Roboto-Italic.ttf";
|
|
||||||
}
|
|
||||||
return "Roboto-Regular.ttf";
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::vector<uint8_t> read_file_bytes(const std::filesystem::path& path) {
|
static std::vector<uint8_t> read_file_bytes(const std::filesystem::path& path) {
|
||||||
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
@@ -709,49 +698,34 @@ static std::vector<uint8_t> read_file_bytes(const std::filesystem::path& path) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::vector<std::filesystem::path> fallback_font_candidates(const char* file_name) {
|
static std::vector<uint8_t> load_fallback_font_data(const char* file_name) {
|
||||||
const auto rel =
|
const char* base_path = SDL_GetBasePath();
|
||||||
std::filesystem::path("3rdparty") / "tracy" / "profiler" / "src" / "font" / file_name;
|
if (base_path == nullptr) {
|
||||||
std::vector<std::filesystem::path> paths;
|
LOGF("Font: SDL_GetBasePath failed: %s\n", SDL_GetError());
|
||||||
|
return {};
|
||||||
auto source_root = std::filesystem::path(__FILE__).parent_path().parent_path().parent_path();
|
|
||||||
paths.push_back(source_root / rel);
|
|
||||||
|
|
||||||
std::error_code ec;
|
|
||||||
auto cwd = std::filesystem::current_path(ec);
|
|
||||||
if (!ec) {
|
|
||||||
paths.push_back(cwd / rel);
|
|
||||||
paths.push_back(cwd / ".." / rel);
|
|
||||||
paths.push_back(cwd / ".." / ".." / rel);
|
|
||||||
}
|
}
|
||||||
|
// SDL returns UTF-8; path(const char*) would read it in the Windows ANSI code page.
|
||||||
return paths;
|
const auto path = Common::PathFromUtf8(base_path) / "assets" / "fonts" / file_name;
|
||||||
}
|
auto data = read_file_bytes(path);
|
||||||
|
if (data.empty()) {
|
||||||
static std::vector<uint8_t> load_fallback_font_data(bool bold, bool italic) {
|
LOGF("Font: fallback font not found: %s\n", Common::PathToString(path).c_str());
|
||||||
for (const auto& path: fallback_font_candidates(fallback_font_file_name(bold, italic))) {
|
|
||||||
auto data = read_file_bytes(path);
|
|
||||||
if (!data.empty()) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return data;
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static const std::vector<uint8_t>& fallback_font_data(bool bold, bool italic) {
|
static const std::vector<uint8_t>& fallback_font_data(bool bold, bool italic) {
|
||||||
static const auto regular = load_fallback_font_data(false, false);
|
static const auto regular = load_fallback_font_data("NotoSans-Regular.ttf");
|
||||||
static const auto bold_data = load_fallback_font_data(true, false);
|
static const auto bold_data = load_fallback_font_data("NotoSans-Bold.ttf");
|
||||||
static const auto italic_data = load_fallback_font_data(false, true);
|
static const auto italic_data = load_fallback_font_data("NotoSans-Italic.ttf");
|
||||||
static const auto bold_italic = load_fallback_font_data(true, true);
|
static const auto bold_italic = load_fallback_font_data("NotoSans-BoldItalic.ttf");
|
||||||
|
|
||||||
if (bold && italic && !bold_italic.empty()) {
|
if (bold && italic) {
|
||||||
return bold_italic;
|
return bold_italic;
|
||||||
}
|
}
|
||||||
if (bold && !bold_data.empty()) {
|
if (bold) {
|
||||||
return bold_data;
|
return bold_data;
|
||||||
}
|
}
|
||||||
if (italic && !italic_data.empty()) {
|
if (italic) {
|
||||||
return italic_data;
|
return italic_data;
|
||||||
}
|
}
|
||||||
return regular;
|
return regular;
|
||||||
|
|||||||
+31
-14
@@ -615,7 +615,12 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
|
|||||||
Common::LockGuard lock(g_mount_mutex);
|
Common::LockGuard lock(g_mount_mutex);
|
||||||
const std::string title_id = cond->title_id != nullptr ? cond->title_id->data : get_title_id();
|
const std::string title_id = cond->title_id != nullptr ? cond->title_id->data : get_title_id();
|
||||||
const auto root = save_user_directory(title_id, cond->user_id);
|
const auto root = save_user_directory(title_id, cond->user_id);
|
||||||
std::vector<std::string> dir_list;
|
struct Found {
|
||||||
|
uint64_t key = 0;
|
||||||
|
std::string name;
|
||||||
|
auto operator<=>(const Found&) const = default;
|
||||||
|
};
|
||||||
|
std::vector<Found> dir_list;
|
||||||
|
|
||||||
if (Common::File::IsDirectoryExisting(root)) {
|
if (Common::File::IsDirectoryExisting(root)) {
|
||||||
for (const auto& entry: Common::File::GetDirEntries(root)) {
|
for (const auto& entry: Common::File::GetDirEntries(root)) {
|
||||||
@@ -625,22 +630,34 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
|
|||||||
if (cond->dir_name == nullptr || cond->dir_name->data[0] == '\0' ||
|
if (cond->dir_name == nullptr || cond->dir_name->data[0] == '\0' ||
|
||||||
dir_name_match(Common::ToLower(entry.name).c_str(),
|
dir_name_match(Common::ToLower(entry.name).c_str(),
|
||||||
Common::ToLower(std::string(cond->dir_name->data)).c_str())) {
|
Common::ToLower(std::string(cond->dir_name->data)).c_str())) {
|
||||||
dir_list.push_back(entry.name);
|
dir_list.push_back({.name = entry.name});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::sort(dir_list.begin(), dir_list.end(), [](const std::string& a, const std::string& b) {
|
// A save whose metadata cannot be read sorts as 0.
|
||||||
return std::strcmp(a.c_str(), b.c_str()) < 0;
|
for (auto& found: dir_list) {
|
||||||
});
|
const auto directory = root / found.name;
|
||||||
|
switch (cond->key) {
|
||||||
if (cond->order == SaveDataSortOrder::Descent) {
|
case SaveDataSortKey::UserParam: {
|
||||||
std::vector<std::string> reversed;
|
SaveDataParam param {};
|
||||||
for (size_t i = dir_list.size(); i > 0; i--) {
|
read_save_param(save_param_path(directory), ¶m);
|
||||||
reversed.push_back(dir_list[i - 1]);
|
found.key = param.user_param;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SaveDataSortKey::Blocks:
|
||||||
|
case SaveDataSortKey::FreeBlocks: read_save_blocks(directory, &found.key); break;
|
||||||
|
case SaveDataSortKey::Mtime:
|
||||||
|
found.key = static_cast<uint64_t>(newest_save_time(directory));
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
}
|
}
|
||||||
dir_list = reversed;
|
}
|
||||||
|
// Directory names break ties; descending order reverses both.
|
||||||
|
std::sort(dir_list.begin(), dir_list.end());
|
||||||
|
if (cond->order == SaveDataSortOrder::Descent) {
|
||||||
|
std::reverse(dir_list.begin(), dir_list.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto max_count =
|
auto max_count =
|
||||||
@@ -651,9 +668,9 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
|
|||||||
|
|
||||||
for (size_t i = 0; i < max_count; i++) {
|
for (size_t i = 0; i < max_count; i++) {
|
||||||
std::snprintf(result->dir_names[i].data, sizeof(result->dir_names[i].data), "%s",
|
std::snprintf(result->dir_names[i].data, sizeof(result->dir_names[i].data), "%s",
|
||||||
dir_list[i].c_str());
|
dir_list[i].name.c_str());
|
||||||
if (result->params != nullptr) {
|
if (result->params != nullptr) {
|
||||||
const int status = load_save_param(root / dir_list[i], &result->params[i]);
|
const int status = load_save_param(root / dir_list[i].name, &result->params[i]);
|
||||||
if (status != OK) {
|
if (status != OK) {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
@@ -661,7 +678,7 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
|
|||||||
if (result->infos != nullptr) {
|
if (result->infos != nullptr) {
|
||||||
auto& info = result->infos[i];
|
auto& info = result->infos[i];
|
||||||
info = {};
|
info = {};
|
||||||
const int status = read_save_blocks(root / dir_list[i], &info.blocks);
|
const int status = read_save_blocks(root / dir_list[i].name, &info.blocks);
|
||||||
if (status != OK) {
|
if (status != OK) {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1636,6 +1636,8 @@ void RuntimeLinker::DeleteTls(Program* program, int thread_id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr uint64_t GUEST_PAGE_SIZE = 0x4000;
|
||||||
|
|
||||||
static uint64_t CalcBaseSize(const Elf64_Ehdr* ehdr, const Elf64_Phdr* phdr) {
|
static uint64_t CalcBaseSize(const Elf64_Ehdr* ehdr, const Elf64_Phdr* phdr) {
|
||||||
uint64_t base_size = 0;
|
uint64_t base_size = 0;
|
||||||
for (Elf64_Half i = 0; i < ehdr->e_phnum; i++) {
|
for (Elf64_Half i = 0; i < ehdr->e_phnum; i++) {
|
||||||
@@ -1649,6 +1651,20 @@ static uint64_t CalcBaseSize(const Elf64_Ehdr* ehdr, const Elf64_Phdr* phdr) {
|
|||||||
return base_size;
|
return base_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A module's E, R and RW LOAD segments use flexible memory in 16 KiB pages; GNU_RELRO adds none
|
||||||
|
// since it lies inside an RW LOAD.
|
||||||
|
uint64_t CalcFlexibleMemorySize(const Elf64_Phdr* phdr, Elf64_Half phnum) {
|
||||||
|
uint64_t size = 0;
|
||||||
|
for (Elf64_Half i = 0; i < phnum; i++) {
|
||||||
|
const auto flags = phdr[i].p_flags;
|
||||||
|
if (phdr[i].p_type == PT_LOAD &&
|
||||||
|
(flags == PF_X || flags == PF_R || flags == (PF_R | PF_W))) {
|
||||||
|
size += AlignUp(phdr[i].p_memsz, GUEST_PAGE_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
||||||
void RuntimeLinker::LoadProgramToMemory(Program* program) {
|
void RuntimeLinker::LoadProgramToMemory(Program* program) {
|
||||||
KYTY_PROFILER_FUNCTION();
|
KYTY_PROFILER_FUNCTION();
|
||||||
@@ -1668,8 +1684,7 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
|
|||||||
|
|
||||||
EXIT_IF(phdr == nullptr || ehdr == nullptr);
|
EXIT_IF(phdr == nullptr || ehdr == nullptr);
|
||||||
|
|
||||||
program->base_size = CalcBaseSize(ehdr, phdr);
|
program->base_size = CalcBaseSize(ehdr, phdr);
|
||||||
constexpr uint64_t GUEST_PAGE_SIZE = 0x4000;
|
|
||||||
EXIT_IF(program->base_size > UINT64_MAX - (GUEST_PAGE_SIZE - 1));
|
EXIT_IF(program->base_size > UINT64_MAX - (GUEST_PAGE_SIZE - 1));
|
||||||
program->base_size_aligned = AlignUp(program->base_size, GUEST_PAGE_SIZE);
|
program->base_size_aligned = AlignUp(program->base_size, GUEST_PAGE_SIZE);
|
||||||
|
|
||||||
@@ -1694,6 +1709,8 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
|
|||||||
g_desired_base_addr, program->mapped_size, Common::VirtualMemory::Mode::ExecuteReadWrite,
|
g_desired_base_addr, program->mapped_size, Common::VirtualMemory::Mode::ExecuteReadWrite,
|
||||||
Common::PathToString(program->file_name.filename()).c_str());
|
Common::PathToString(program->file_name.filename()).c_str());
|
||||||
EXIT_IF(program->base_vaddr == 0);
|
EXIT_IF(program->base_vaddr == 0);
|
||||||
|
program->flexible_size = CalcFlexibleMemorySize(phdr, ehdr->e_phnum);
|
||||||
|
Libs::LibKernel::Memory::ChargeFlexibleMemory(program->flexible_size);
|
||||||
|
|
||||||
if (patch_guest_instructions) {
|
if (patch_guest_instructions) {
|
||||||
const auto trampoline_addr = program->base_vaddr + program->base_size_aligned;
|
const auto trampoline_addr = program->base_vaddr + program->base_size_aligned;
|
||||||
@@ -1879,6 +1896,7 @@ void RuntimeLinker::DeleteProgram(Program* p) {
|
|||||||
}
|
}
|
||||||
EXIT_IF(
|
EXIT_IF(
|
||||||
!Libs::LibKernel::Memory::FreeGuestMemory(program->base_vaddr, program->mapped_size));
|
!Libs::LibKernel::Memory::FreeGuestMemory(program->base_vaddr, program->mapped_size));
|
||||||
|
Libs::LibKernel::Memory::ReleaseFlexibleMemory(program->flexible_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ struct EntryParams {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class Elf64;
|
class Elf64;
|
||||||
|
struct Elf64_Phdr;
|
||||||
struct Elf64_Sym;
|
struct Elf64_Sym;
|
||||||
struct Elf64_Rela;
|
struct Elf64_Rela;
|
||||||
class RuntimeLinker;
|
class RuntimeLinker;
|
||||||
@@ -128,6 +129,7 @@ struct Program {
|
|||||||
uint64_t base_size = 0;
|
uint64_t base_size = 0;
|
||||||
uint64_t base_size_aligned = 0;
|
uint64_t base_size_aligned = 0;
|
||||||
uint64_t mapped_size = 0;
|
uint64_t mapped_size = 0;
|
||||||
|
uint64_t flexible_size = 0;
|
||||||
uint64_t instruction_trampoline_size = 0;
|
uint64_t instruction_trampoline_size = 0;
|
||||||
std::unique_ptr<SymbolDatabase> export_symbols;
|
std::unique_ptr<SymbolDatabase> export_symbols;
|
||||||
std::unique_ptr<SymbolDatabase> import_symbols;
|
std::unique_ptr<SymbolDatabase> import_symbols;
|
||||||
@@ -203,6 +205,8 @@ private:
|
|||||||
application_heap_posix_memalign_func_t m_application_heap_posix_memalign = nullptr;
|
application_heap_posix_memalign_func_t m_application_heap_posix_memalign = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
uint64_t CalcFlexibleMemorySize(const Elf64_Phdr* phdr, uint16_t phnum);
|
||||||
|
|
||||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||||
bool TestMainEntryUsesGuestStack();
|
bool TestMainEntryUsesGuestStack();
|
||||||
bool TestModuleRelocationUsesWritableHostMapping();
|
bool TestModuleRelocationUsesWritableHostMapping();
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public:
|
|||||||
bool GetParamInt(const char* name, int32_t* value);
|
bool GetParamInt(const char* name, int32_t* value);
|
||||||
bool GetParamString(const char* name, std::string* value);
|
bool GetParamString(const char* name, std::string* value);
|
||||||
bool GetParamString(const char* name, char* value, size_t value_size);
|
bool GetParamString(const char* name, char* value, size_t value_size);
|
||||||
uint64_t GetFlexibleMemorySize();
|
MemoryParams GetMemoryParams();
|
||||||
|
|
||||||
void DbgPrint();
|
void DbgPrint();
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ private:
|
|||||||
Common::Mutex m_mutex;
|
Common::Mutex m_mutex;
|
||||||
bool m_opened = false;
|
bool m_opened = false;
|
||||||
std::vector<Param> m_params;
|
std::vector<Param> m_params;
|
||||||
uint64_t m_flexible_memory_size = 0;
|
MemoryParams m_memory_params;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PlayGo {
|
class PlayGo {
|
||||||
@@ -172,11 +172,21 @@ void Psf::OpenJson(const std::filesystem::path& file_name) {
|
|||||||
add_int_param("userDefinedParam3", "USER_DEFINED_PARAM_3");
|
add_int_param("userDefinedParam3", "USER_DEFINED_PARAM_3");
|
||||||
add_int_param("userDefinedParam4", "USER_DEFINED_PARAM_4");
|
add_int_param("userDefinedParam4", "USER_DEFINED_PARAM_4");
|
||||||
|
|
||||||
|
auto read_size = [](const nlohmann::ordered_json& object, const char* key, uint64_t* size,
|
||||||
|
uint64_t unit = 1) {
|
||||||
|
if (auto it = object.find(key); it != object.end()) {
|
||||||
|
*size = it->get<uint64_t>() * unit;
|
||||||
|
}
|
||||||
|
};
|
||||||
if (auto kernel = param_json.find("kernel");
|
if (auto kernel = param_json.find("kernel");
|
||||||
kernel != param_json.end() && kernel->is_object()) {
|
kernel != param_json.end() && kernel->is_object()) {
|
||||||
if (auto it = kernel->find("flexibleMemorySize"); it != kernel->end()) {
|
read_size(*kernel, "flexibleMemorySize", &m_memory_params.flexible_size);
|
||||||
m_flexible_memory_size = it->get<uint64_t>();
|
read_size(*kernel, "cpuPageTableSize", &m_memory_params.cpu_table_memory);
|
||||||
}
|
read_size(*kernel, "gpuPageTableSize", &m_memory_params.gpu_table_memory);
|
||||||
|
}
|
||||||
|
if (auto amm = param_json.find("amm"); amm != param_json.end() && amm->is_object()) {
|
||||||
|
read_size(*amm, "pagetableMemorySizeInMib", &m_memory_params.amm_table_memory,
|
||||||
|
1024ull * 1024ull);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto localized = param_json.find("localizedParameters");
|
auto localized = param_json.find("localizedParameters");
|
||||||
@@ -227,8 +237,8 @@ void Psf::Close() {
|
|||||||
Common::LockGuard lock(m_mutex);
|
Common::LockGuard lock(m_mutex);
|
||||||
|
|
||||||
m_params.clear();
|
m_params.clear();
|
||||||
m_flexible_memory_size = 0;
|
m_memory_params = {};
|
||||||
m_opened = false;
|
m_opened = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Psf::IsValid() {
|
bool Psf::IsValid() {
|
||||||
@@ -339,10 +349,10 @@ bool Psf::GetParamString(const char* name, char* value, size_t value_size) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t Psf::GetFlexibleMemorySize() {
|
MemoryParams Psf::GetMemoryParams() {
|
||||||
Common::LockGuard lock(m_mutex);
|
Common::LockGuard lock(m_mutex);
|
||||||
|
|
||||||
return m_flexible_memory_size;
|
return m_memory_params;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayGo::~PlayGo() {
|
PlayGo::~PlayGo() {
|
||||||
@@ -646,10 +656,10 @@ bool SystemContentParamSfoGetString(const char* name, char* value, size_t value_
|
|||||||
return sc->psf.GetParamString(name, value, value_size);
|
return sc->psf.GetParamString(name, value, value_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t SystemContentGetFlexibleMemorySize() {
|
MemoryParams SystemContentGetMemoryParams() {
|
||||||
auto* sc = Common::Singleton<SystemContent>::Instance();
|
auto* sc = Common::Singleton<SystemContent>::Instance();
|
||||||
|
|
||||||
return sc->psf.GetFlexibleMemorySize();
|
return sc->psf.GetMemoryParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SystemContentGetIconPath(std::filesystem::path* path) {
|
bool SystemContentGetIconPath(std::filesystem::path* path) {
|
||||||
|
|||||||
@@ -14,7 +14,16 @@ void SystemContentLoadParamSfo(const std::filesystem::path& file_name);
|
|||||||
bool SystemContentParamSfoGetInt(const char* name, int32_t* value);
|
bool SystemContentParamSfoGetInt(const char* name, int32_t* value);
|
||||||
bool SystemContentParamSfoGetString(const char* name, std::string* value);
|
bool SystemContentParamSfoGetString(const char* name, std::string* value);
|
||||||
bool SystemContentParamSfoGetString(const char* name, char* value, size_t value_size);
|
bool SystemContentParamSfoGetString(const char* name, char* value, size_t value_size);
|
||||||
uint64_t SystemContentGetFlexibleMemorySize();
|
// Memory sizes a game's param.json sets, in bytes; 0 where it sets none.
|
||||||
|
struct MemoryParams {
|
||||||
|
uint64_t flexible_size = 0;
|
||||||
|
uint64_t cpu_table_memory = 0;
|
||||||
|
uint64_t gpu_table_memory = 0;
|
||||||
|
uint64_t amm_table_memory = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
MemoryParams SystemContentGetMemoryParams();
|
||||||
|
|
||||||
bool SystemContentGetIconPath(std::filesystem::path* path);
|
bool SystemContentGetIconPath(std::filesystem::path* path);
|
||||||
bool SystemContentGetChunksNum(uint32_t* num);
|
bool SystemContentGetChunksNum(uint32_t* num);
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ static void PrintUsage() {
|
|||||||
::printf(" --hdr Enable HDR10 output when supported.\n");
|
::printf(" --hdr Enable HDR10 output when supported.\n");
|
||||||
::printf(
|
::printf(
|
||||||
" --hide-cursor Hide the cursor after 2 s idle. Default: off.\n");
|
" --hide-cursor Hide the cursor after 2 s idle. Default: off.\n");
|
||||||
|
::printf(
|
||||||
|
" --stretch Stretch the image to the window. Default: off.\n");
|
||||||
::printf(" --no-splash Disable the startup splash screen.\n");
|
::printf(" --no-splash Disable the startup splash screen.\n");
|
||||||
::printf(" --vr Enable the virtual VR headset.\n");
|
::printf(" --vr Enable the virtual VR headset.\n");
|
||||||
::printf(" --amd-cpu Apply AMD CPU instruction patches.\n");
|
::printf(" --amd-cpu Apply AMD CPU instruction patches.\n");
|
||||||
@@ -199,6 +201,7 @@ static constexpr Flag FLAGS[] = {
|
|||||||
{"--fullscreen", &ConfigOptions::fullscreen_enabled},
|
{"--fullscreen", &ConfigOptions::fullscreen_enabled},
|
||||||
{"--hdr", &ConfigOptions::hdr_enabled},
|
{"--hdr", &ConfigOptions::hdr_enabled},
|
||||||
{"--hide-cursor", &ConfigOptions::hide_cursor_enabled},
|
{"--hide-cursor", &ConfigOptions::hide_cursor_enabled},
|
||||||
|
{"--stretch", &ConfigOptions::stretch_enabled},
|
||||||
{"--no-splash", &ConfigOptions::splash_enabled, false},
|
{"--no-splash", &ConfigOptions::splash_enabled, false},
|
||||||
{"--vr", &ConfigOptions::vr_enabled},
|
{"--vr", &ConfigOptions::vr_enabled},
|
||||||
{"--amd-cpu", &ConfigOptions::amd_cpu_enabled},
|
{"--amd-cpu", &ConfigOptions::amd_cpu_enabled},
|
||||||
|
|||||||
+4
-1
@@ -65,6 +65,8 @@ else()
|
|||||||
SET(CMAKE_NINJA_FORCE_RESPONSE_FILE 1 CACHE INTERNAL "")
|
SET(CMAKE_NINJA_FORCE_RESPONSE_FILE 1 CACHE INTERNAL "")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Guest code runs natively and needs x86-64-v3 (AVX2, BMI2, FMA), so every host has it.
|
||||||
|
# No FMA contraction: a * b + c keeps both roundings.
|
||||||
if(KYTY_CLANG_CL)
|
if(KYTY_CLANG_CL)
|
||||||
if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]")
|
if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]")
|
||||||
string(REGEX REPLACE "/W[0-4]" "/W3" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
|
string(REGEX REPLACE "/W[0-4]" "/W3" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
|
||||||
@@ -79,7 +81,7 @@ if(KYTY_CLANG_CL)
|
|||||||
string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}")
|
string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}")
|
||||||
string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
|
string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
|
||||||
|
|
||||||
set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} /utf-8 /Oy- /wd4244 /wd4305 /wd4800 /wd4345")
|
set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} /utf-8 /Oy- /clang:-march=x86-64-v3 /clang:-ffp-contract=off /wd4244 /wd4305 /wd4800 /wd4345")
|
||||||
|
|
||||||
if(KYTY_WARNINGS_ARE_ERRORS)
|
if(KYTY_WARNINGS_ARE_ERRORS)
|
||||||
#set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} /WX")
|
#set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} /WX")
|
||||||
@@ -103,6 +105,7 @@ elseif(CLANG OR GCC)
|
|||||||
else()
|
else()
|
||||||
set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} -fno-exceptions -fdiagnostics-color=always -finput-charset=UTF-8 -fexec-charset=UTF-8 -static-libgcc -static-libstdc++ -g -fno-strict-aliasing -fno-omit-frame-pointer -Wall -Wno-unused-value -fmessage-length=0")
|
set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} -fno-exceptions -fdiagnostics-color=always -finput-charset=UTF-8 -fexec-charset=UTF-8 -static-libgcc -static-libstdc++ -g -fno-strict-aliasing -fno-omit-frame-pointer -Wall -Wno-unused-value -fmessage-length=0")
|
||||||
endif()
|
endif()
|
||||||
|
set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} -march=x86-64-v3 -ffp-contract=off")
|
||||||
|
|
||||||
if(KYTY_WARNINGS_ARE_ERRORS)
|
if(KYTY_WARNINGS_ARE_ERRORS)
|
||||||
#set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} -Werror")
|
#set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} -Werror")
|
||||||
|
|||||||
+225
-36
@@ -35,6 +35,8 @@ int g_open_waiters = 0;
|
|||||||
bool g_block_opens = false;
|
bool g_block_opens = false;
|
||||||
bool g_block_outputs = false;
|
bool g_block_outputs = false;
|
||||||
std::vector<int> g_blocked_output;
|
std::vector<int> g_blocked_output;
|
||||||
|
std::atomic_uint64_t g_process_time {0};
|
||||||
|
std::atomic_bool g_process_time_frozen {false};
|
||||||
|
|
||||||
void Check(bool value, const char* text) {
|
void Check(bool value, const char* text) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -109,8 +111,10 @@ PortParam MakeParam(uint32_t data_format = 0x200) {
|
|||||||
return param;
|
return param;
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4, uint32_t max_object_ports = 0) {
|
AudioOut2::AudioOut2ContextHandle
|
||||||
|
CreateContext(uint32_t queue_depth = 4, uint32_t max_object_ports = 0, uint32_t max_ports = 8) {
|
||||||
ContextParam param {};
|
ContextParam param {};
|
||||||
|
param.max_ports = max_ports;
|
||||||
param.max_object_ports = max_object_ports;
|
param.max_object_ports = max_object_ports;
|
||||||
param.queue_depth = queue_depth;
|
param.queue_depth = queue_depth;
|
||||||
param.num_grains = 512;
|
param.num_grains = 512;
|
||||||
@@ -279,13 +283,139 @@ void TestSlotReuse() {
|
|||||||
AudioOut2::AudioOut2ContextDestroy(context);
|
AudioOut2::AudioOut2ContextDestroy(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestObjectPortCapacityPerContext() {
|
||||||
|
constexpr int port_full = static_cast<int32_t>(0x80268012u);
|
||||||
|
constexpr int invalid_port = static_cast<int32_t>(0x80268009u);
|
||||||
|
std::array<AudioOut2::AudioOut2ContextHandle, 3> contexts {};
|
||||||
|
std::array<AudioOut2::AudioOut2PortHandle, 3> last_ports {};
|
||||||
|
auto object = MakeParam(0x100);
|
||||||
|
object.port_type = 0x100;
|
||||||
|
for (size_t i = 0; i < contexts.size(); i++) {
|
||||||
|
contexts[i] = CreateContext(4, 512, 32);
|
||||||
|
CreatePort(contexts[i], MakeParam());
|
||||||
|
for (size_t j = 0; j < 512; j++) {
|
||||||
|
AudioOut2::AudioOut2PortHandle port = 0;
|
||||||
|
Check(AudioOut2::AudioOut2PortCreate(contexts[i], AsParam(&object), &port) == OK,
|
||||||
|
"context could not create all 512 object ports");
|
||||||
|
last_ports[i] = port;
|
||||||
|
}
|
||||||
|
AudioOut2::AudioOut2PortHandle overflow = UINT64_MAX;
|
||||||
|
Check(AudioOut2::AudioOut2PortCreate(contexts[i], AsParam(&object), &overflow) ==
|
||||||
|
port_full &&
|
||||||
|
overflow == UINT64_MAX,
|
||||||
|
"object capacity did not reject the 513th port without changing its output");
|
||||||
|
CreatePort(contexts[i], MakeParam());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < contexts.size(); i++) {
|
||||||
|
const auto stale = last_ports[i];
|
||||||
|
AudioOut2::AudioOut2PortDestroy(stale);
|
||||||
|
last_ports[i] = CreatePort(contexts[i], object);
|
||||||
|
Check(last_ports[i] != stale &&
|
||||||
|
AudioOut2::AudioOut2PortSetAttributes(stale, nullptr, 0) == invalid_port,
|
||||||
|
"reused object slot accepted its old handle");
|
||||||
|
std::array<float, 512> pcm;
|
||||||
|
pcm.fill(0.25f * static_cast<float>(i + 1));
|
||||||
|
SetPcm(last_ports[i], pcm.data());
|
||||||
|
const auto output = PushPcm(contexts[i], 512 * 2 * sizeof(float));
|
||||||
|
const float expected = pcm[0] * 0.70710678f;
|
||||||
|
Check(output.size() == 1, "last object port did not reach its context's mixer");
|
||||||
|
CheckSamples(output[0], {expected, expected}, "object audio crossed context boundaries");
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioOut2::AudioOut2ContextDestroy(contexts[0]);
|
||||||
|
Check(AudioOut2::AudioOut2PortSetAttributes(last_ports[0], nullptr, 0) == invalid_port,
|
||||||
|
"destroyed context retained its object handles");
|
||||||
|
const auto replacement = CreateContext(4, 512);
|
||||||
|
for (size_t i = 0; i < 512; i++) {
|
||||||
|
CreatePort(replacement, object);
|
||||||
|
}
|
||||||
|
Check(AudioOut2::AudioOut2PortSetAttributes(last_ports[0], nullptr, 0) == invalid_port,
|
||||||
|
"reused context accepted an old object handle");
|
||||||
|
for (size_t i = 1; i < contexts.size(); i++) {
|
||||||
|
Check(AudioOut2::AudioOut2PortSetAttributes(last_ports[i], nullptr, 0) == OK,
|
||||||
|
"destroying another context invalidated a live object port");
|
||||||
|
AudioOut2::AudioOut2ContextDestroy(contexts[i]);
|
||||||
|
}
|
||||||
|
AudioOut2::AudioOut2ContextDestroy(replacement);
|
||||||
|
Check(LiveDeviceCount() == 0, "object capacity test leaked audio devices");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestContextPortLimits() {
|
||||||
|
constexpr int port_full = static_cast<int32_t>(0x80268012u);
|
||||||
|
ContextParam param {};
|
||||||
|
param.max_ports = 1;
|
||||||
|
param.max_object_ports = 2;
|
||||||
|
param.num_grains = 512;
|
||||||
|
AudioOut2::AudioOut2ContextHandle context = 0;
|
||||||
|
Check(AudioOut2::AudioOut2ContextCreate(AsParam(¶m), nullptr, 0, &context) == OK,
|
||||||
|
"small context create failed");
|
||||||
|
auto object = MakeParam(0x100);
|
||||||
|
object.port_type = 0x100;
|
||||||
|
const auto channel = CreatePort(context, MakeParam());
|
||||||
|
const auto first_object = CreatePort(context, object);
|
||||||
|
CreatePort(context, object);
|
||||||
|
AudioOut2::AudioOut2PortHandle overflow = 0;
|
||||||
|
const auto channel_param = MakeParam();
|
||||||
|
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&channel_param), &overflow) ==
|
||||||
|
port_full &&
|
||||||
|
AudioOut2::AudioOut2PortCreate(context, AsParam(&object), &overflow) == port_full,
|
||||||
|
"declared channel/object capacities were ignored");
|
||||||
|
AudioOut2::AudioOut2PortDestroy(channel);
|
||||||
|
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&object), &overflow) == port_full,
|
||||||
|
"object port borrowed a channel slot");
|
||||||
|
CreatePort(context, channel_param);
|
||||||
|
AudioOut2::AudioOut2PortDestroy(first_object);
|
||||||
|
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&channel_param), &overflow) == port_full,
|
||||||
|
"channel port borrowed an object slot");
|
||||||
|
CreatePort(context, object);
|
||||||
|
AudioOut2::AudioOut2ContextDestroy(context);
|
||||||
|
param.max_ports = param.max_object_ports = 0;
|
||||||
|
Check(AudioOut2::AudioOut2ContextCreate(AsParam(¶m), nullptr, 0, &context) == OK,
|
||||||
|
"empty context create failed");
|
||||||
|
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&channel_param), &overflow) ==
|
||||||
|
port_full &&
|
||||||
|
AudioOut2::AudioOut2PortCreate(context, AsParam(&object), &overflow) == port_full,
|
||||||
|
"zero port capacity was ignored");
|
||||||
|
AudioOut2::AudioOut2ContextDestroy(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestContextCapacityParameters() {
|
||||||
|
constexpr int invalid_param = static_cast<int32_t>(0x80268001u);
|
||||||
|
ContextParam param;
|
||||||
|
std::memset(¶m, 0xff, sizeof(param));
|
||||||
|
Check(AudioOut2::AudioOut2ContextResetParam(
|
||||||
|
reinterpret_cast<AudioOut2::AudioOut2ContextParam*>(¶m)) == OK &&
|
||||||
|
param.max_ports == 8 && param.max_object_ports == 0 &&
|
||||||
|
param.guarantee_object_ports == 0 && param.num_grains == 256 &&
|
||||||
|
param.queue_depth == 1 && param.flags == 0 &&
|
||||||
|
std::all_of(std::begin(param.reserved), std::end(param.reserved),
|
||||||
|
[](uint32_t word) { return word == 0; }),
|
||||||
|
"context reset did not produce valid default parameters");
|
||||||
|
const auto defaults = param;
|
||||||
|
for (size_t i = 0; i < 3; i++) {
|
||||||
|
param = defaults;
|
||||||
|
if (i == 0) param.max_ports = 33;
|
||||||
|
if (i == 1) param.max_object_ports = 513;
|
||||||
|
if (i == 2) param.guarantee_object_ports = 1;
|
||||||
|
size_t memory = SIZE_MAX;
|
||||||
|
AudioOut2::AudioOut2ContextHandle context = UINT64_MAX;
|
||||||
|
Check(AudioOut2::AudioOut2ContextQueryMemory(AsParam(¶m), &memory) == invalid_param &&
|
||||||
|
memory == SIZE_MAX &&
|
||||||
|
AudioOut2::AudioOut2ContextCreate(AsParam(¶m), nullptr, 0, &context) ==
|
||||||
|
invalid_param &&
|
||||||
|
context == UINT64_MAX && LiveDeviceCount() == 0,
|
||||||
|
"invalid capacities allocated resources or modified outputs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TestFullTableRecovers() {
|
void TestFullTableRecovers() {
|
||||||
const auto context = CreateContext();
|
const auto context = CreateContext();
|
||||||
const auto param = MakeParam();
|
const auto param = MakeParam();
|
||||||
std::vector<AudioOut2::AudioOut2PortHandle> ports;
|
std::vector<AudioOut2::AudioOut2PortHandle> ports;
|
||||||
ports.reserve(256);
|
ports.reserve(8);
|
||||||
|
|
||||||
for (int i = 0; i < 256; i++) {
|
for (int i = 0; i < 8; i++) {
|
||||||
AudioOut2::AudioOut2PortHandle port = 0;
|
AudioOut2::AudioOut2PortHandle port = 0;
|
||||||
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK,
|
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK,
|
||||||
"port table filled early");
|
"port table filled early");
|
||||||
@@ -322,6 +452,11 @@ void TestConcurrentCreates() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
WaitForDeviceOpens(thread_count);
|
WaitForDeviceOpens(thread_count);
|
||||||
|
AudioOut2::AudioOut2PortHandle overflow = 0;
|
||||||
|
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &overflow) ==
|
||||||
|
static_cast<int32_t>(0x80268012u) &&
|
||||||
|
overflow == 0,
|
||||||
|
"pending creates did not reserve their channel capacity");
|
||||||
ReleaseDeviceOpens();
|
ReleaseDeviceOpens();
|
||||||
for (auto& thread: threads) {
|
for (auto& thread: threads) {
|
||||||
thread.join();
|
thread.join();
|
||||||
@@ -349,17 +484,31 @@ void TestContextDestroyCancelsPendingCreate() {
|
|||||||
[&]() { result = AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port); });
|
[&]() { result = AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port); });
|
||||||
WaitForDeviceOpens(1);
|
WaitForDeviceOpens(1);
|
||||||
AudioOut2::AudioOut2ContextDestroy(context);
|
AudioOut2::AudioOut2ContextDestroy(context);
|
||||||
|
const auto replacement = CreateContext();
|
||||||
|
AudioOut2::AudioOut2PortHandle replacement_port = 0;
|
||||||
|
int replacement_result = OK;
|
||||||
|
std::thread replacement_creator([&]() {
|
||||||
|
replacement_result =
|
||||||
|
AudioOut2::AudioOut2PortCreate(replacement, AsParam(¶m), &replacement_port);
|
||||||
|
});
|
||||||
|
WaitForDeviceOpens(2);
|
||||||
ReleaseDeviceOpens();
|
ReleaseDeviceOpens();
|
||||||
creator.join();
|
creator.join();
|
||||||
|
replacement_creator.join();
|
||||||
|
|
||||||
Check(result != OK, "destroyed context retained a pending port create");
|
Check(result != OK, "destroyed context retained a pending port create");
|
||||||
|
Check(replacement_result == OK &&
|
||||||
|
AudioOut2::AudioOut2PortSetAttributes(replacement_port, nullptr, 0) == OK &&
|
||||||
|
LiveDeviceCount() == 1,
|
||||||
|
"cancelled create corrupted the replacement context or leaked a device");
|
||||||
|
AudioOut2::AudioOut2ContextDestroy(replacement);
|
||||||
Check(LiveDeviceCount() == 0, "cancelled port create leaked a device");
|
Check(LiveDeviceCount() == 0, "cancelled port create leaked a device");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestSynchronousDevicePushBypassesModelledQueue() {
|
void TestSynchronousDevicePushBypassesModelledQueue() {
|
||||||
const auto context = CreateContext(1);
|
const auto context = CreateContext(1);
|
||||||
const auto param = MakeParam();
|
const auto param = MakeParam();
|
||||||
AudioOut2::AudioOut2PortHandle port = 0;
|
AudioOut2::AudioOut2PortHandle port = 0;
|
||||||
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK,
|
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK,
|
||||||
"device port create failed");
|
"device port create failed");
|
||||||
|
|
||||||
@@ -368,6 +517,7 @@ void TestSynchronousDevicePushBypassesModelledQueue() {
|
|||||||
ResetOutputCalls();
|
ResetOutputCalls();
|
||||||
|
|
||||||
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "first sync push failed");
|
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "first sync push failed");
|
||||||
|
SetPcm(port, pcm);
|
||||||
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK,
|
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK,
|
||||||
"device-paced sync push was blocked by modelled queue");
|
"device-paced sync push was blocked by modelled queue");
|
||||||
const auto calls = OutputCalls();
|
const auto calls = OutputCalls();
|
||||||
@@ -447,6 +597,44 @@ void TestHandleWithoutPcmDoesNotBypassQueue() {
|
|||||||
AudioOut2::AudioOut2ContextDestroy(context);
|
AudioOut2::AudioOut2ContextDestroy(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestPushConsumesPcm() {
|
||||||
|
const auto context = CreateContext();
|
||||||
|
const auto port = CreatePort(context, MakeParam());
|
||||||
|
uint32_t pcm[512] {};
|
||||||
|
SetPcm(port, pcm);
|
||||||
|
ResetOutputCalls();
|
||||||
|
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "push with PCM failed");
|
||||||
|
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "push without new PCM failed");
|
||||||
|
SetPcm(port, pcm);
|
||||||
|
SetPcm(port, nullptr);
|
||||||
|
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "push after clearing PCM failed");
|
||||||
|
Check(OutputCalls().size() == 1, "a push replayed PCM the game did not set again");
|
||||||
|
|
||||||
|
AudioOut2::AudioOut2PortDestroy(port);
|
||||||
|
AudioOut2::AudioOut2ContextDestroy(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestQueueDrainsAtExactGrainRate() {
|
||||||
|
// Three 512-sample grains play in exactly 32000 us.
|
||||||
|
const auto context = CreateContext();
|
||||||
|
g_process_time_frozen = true;
|
||||||
|
const auto start = g_process_time.load();
|
||||||
|
for (int push = 0; push < 3; push++) {
|
||||||
|
Check(AudioOut2::AudioOut2ContextPush(context, 0) == OK, "async push failed");
|
||||||
|
}
|
||||||
|
const auto queued_after = [&](uint64_t micros) {
|
||||||
|
g_process_time = start + micros;
|
||||||
|
uint32_t queued = 0;
|
||||||
|
Check(AudioOut2::AudioOut2ContextGetQueueLevel(context, &queued, nullptr) == OK,
|
||||||
|
"queue-level query failed");
|
||||||
|
return queued;
|
||||||
|
};
|
||||||
|
Check(queued_after(31999) == 1, "the third grain drained before it finished playing");
|
||||||
|
Check(queued_after(32000) == 0, "three grains did not drain in their playing time");
|
||||||
|
g_process_time_frozen = false;
|
||||||
|
AudioOut2::AudioOut2ContextDestroy(context);
|
||||||
|
}
|
||||||
|
|
||||||
void TestPcmCopiedBeforeScratchBufferReuse() {
|
void TestPcmCopiedBeforeScratchBufferReuse() {
|
||||||
const auto context = CreateContext();
|
const auto context = CreateContext();
|
||||||
const auto param = MakeParam();
|
const auto param = MakeParam();
|
||||||
@@ -486,7 +674,6 @@ void TestPortGainAndValidation() {
|
|||||||
const auto context = CreateContext();
|
const auto context = CreateContext();
|
||||||
const auto port = CreatePort(context, MakeParam());
|
const auto port = CreatePort(context, MakeParam());
|
||||||
std::vector<float> pcm(512 * 2, 0.25f);
|
std::vector<float> pcm(512 * 2, 0.25f);
|
||||||
SetPcm(port, pcm.data());
|
|
||||||
auto check_output = [&](const std::array<float, 2>& gain, float expected_pcm) {
|
auto check_output = [&](const std::array<float, 2>& gain, float expected_pcm) {
|
||||||
CheckSamples(PushPcm(context, pcm.size() * sizeof(float)).at(0), {expected_pcm},
|
CheckSamples(PushPcm(context, pcm.size() * sizeof(float)).at(0), {expected_pcm},
|
||||||
"gain modified stored PCM");
|
"gain modified stored PCM");
|
||||||
@@ -494,10 +681,11 @@ void TestPortGainAndValidation() {
|
|||||||
Check(g_output_gains.size() == 1 && g_output_gains[0] == gain,
|
Check(g_output_gains.size() == 1 && g_output_gains[0] == gain,
|
||||||
"incorrect per-channel gain forwarded to backend");
|
"incorrect per-channel gain forwarded to backend");
|
||||||
};
|
};
|
||||||
|
SetPcm(port, pcm.data());
|
||||||
check_output({1.0f, 1.0f}, 0.25f);
|
check_output({1.0f, 1.0f}, 0.25f);
|
||||||
for (const std::array<float, 2> gain: {std::array {2.0f, 0.5f}, {0.0f, 0.0f}, {1.0f, 1.0f}}) {
|
for (const std::array<float, 2> gain: {std::array {2.0f, 0.5f}, {0.0f, 0.0f}, {1.0f, 1.0f}}) {
|
||||||
SetAttribute(port, 1, gain);
|
SetAttribute(port, 1, gain);
|
||||||
check_output(gain, 0.25f);
|
SetPcm(port, pcm.data());
|
||||||
check_output(gain, 0.25f);
|
check_output(gain, 0.25f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,10 +695,11 @@ void TestPortGainAndValidation() {
|
|||||||
SetPcm(port, pcm.data());
|
SetPcm(port, pcm.data());
|
||||||
check_output(gain, 0.75f);
|
check_output(gain, 0.75f);
|
||||||
|
|
||||||
std::fill(pcm.begin(), pcm.end(), -0.25f);
|
const std::vector<float> replacement_pcm(pcm.size(), -0.25f);
|
||||||
const Pcm replacement {pcm.data()};
|
const Pcm replacement {replacement_pcm.data()};
|
||||||
for (const float invalid: {-1.0f, std::numeric_limits<float>::infinity(),
|
for (const float invalid: {-1.0f, std::numeric_limits<float>::infinity(),
|
||||||
std::numeric_limits<float>::quiet_NaN()}) {
|
std::numeric_limits<float>::quiet_NaN()}) {
|
||||||
|
SetPcm(port, pcm.data());
|
||||||
const std::array invalid_gain {1.0f, invalid};
|
const std::array invalid_gain {1.0f, invalid};
|
||||||
const Attribute attributes[] {{0, 0, &replacement, sizeof(replacement)},
|
const Attribute attributes[] {{0, 0, &replacement, sizeof(replacement)},
|
||||||
{1, 0, &invalid_gain, sizeof(invalid_gain)}};
|
{1, 0, &invalid_gain, sizeof(invalid_gain)}};
|
||||||
@@ -523,6 +712,7 @@ void TestPortGainAndValidation() {
|
|||||||
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) ==
|
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) ==
|
||||||
static_cast<int32_t>(0x80268001u), "malformed gain was accepted");
|
static_cast<int32_t>(0x80268001u), "malformed gain was accepted");
|
||||||
}
|
}
|
||||||
|
SetPcm(port, pcm.data());
|
||||||
const Attribute duplicates[] {{0, 0, &replacement, sizeof(replacement)},
|
const Attribute duplicates[] {{0, 0, &replacement, sizeof(replacement)},
|
||||||
{1, 0, &gain, sizeof(gain)}, {1, 0, &gain, sizeof(gain)}};
|
{1, 0, &gain, sizeof(gain)}, {1, 0, &gain, sizeof(gain)}};
|
||||||
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(duplicates), 3) ==
|
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(duplicates), 3) ==
|
||||||
@@ -553,12 +743,10 @@ void TestObjectGainAndAmbisonics() {
|
|||||||
std::fill(integer.begin(), integer.end(), 0);
|
std::fill(integer.begin(), integer.end(), 0);
|
||||||
SetAttribute(object, 1, 0.0f);
|
SetAttribute(object, 1, 0.0f);
|
||||||
SetAttribute(object, 8, 65u);
|
SetAttribute(object, 8, 65u);
|
||||||
for (int push = 0; push < 2; push++) {
|
const auto output = PushPcm(context, output_bytes);
|
||||||
const auto output = PushPcm(context, output_bytes);
|
Check(output.size() == 1, "object-only push did not output exactly once");
|
||||||
Check(output.size() == 1, "object-only push did not output exactly once");
|
CheckSamples(output[0], {0.5f * half_power, 0.5f * half_power},
|
||||||
CheckSamples(output[0], {0.5f * half_power, 0.5f * half_power},
|
"object PCM ownership, default gain, or attribute freeze failed");
|
||||||
"object PCM ownership, default gain, or attribute freeze failed");
|
|
||||||
}
|
|
||||||
std::fill(floating.begin(), floating.end(), 0.5f);
|
std::fill(floating.begin(), floating.end(), 0.5f);
|
||||||
std::fill(integer.begin(), integer.end(), 16384);
|
std::fill(integer.begin(), integer.end(), 16384);
|
||||||
for (const auto& test: cases) {
|
for (const auto& test: cases) {
|
||||||
@@ -569,10 +757,8 @@ void TestObjectGainAndAmbisonics() {
|
|||||||
{8, 0, &test.channel, sizeof(test.channel)}};
|
{8, 0, &test.channel, sizeof(test.channel)}};
|
||||||
Check(AudioOut2::AudioOut2PortSetAttributes(object, AsAttribute(attributes), 3) == OK,
|
Check(AudioOut2::AudioOut2PortSetAttributes(object, AsAttribute(attributes), 3) == OK,
|
||||||
"object attributes could not be set atomically with PCM first");
|
"object attributes could not be set atomically with PCM first");
|
||||||
for (int push = 0; push < 2; push++) {
|
CheckSamples(PushPcm(context, output_bytes).at(0), {0.25f * test.left, 0.25f * test.right},
|
||||||
CheckSamples(PushPcm(context, output_bytes).at(0), {0.25f * test.left, 0.25f * test.right},
|
"object gain or ambisonics decoded incorrectly or accumulated across pushes");
|
||||||
"object gain or ambisonics decoded incorrectly or accumulated across pushes");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
AudioOut2::AudioOut2ContextAdvance(context);
|
AudioOut2::AudioOut2ContextAdvance(context);
|
||||||
SetAttribute(object, 8, UINT32_MAX);
|
SetAttribute(object, 8, UINT32_MAX);
|
||||||
@@ -599,22 +785,21 @@ void TestObjectBusRouting() {
|
|||||||
SetPcm(second, bed.data());
|
SetPcm(second, bed.data());
|
||||||
auto param = MakeParam(0x100);
|
auto param = MakeParam(0x100);
|
||||||
param.port_type = 0x100;
|
param.port_type = 0x100;
|
||||||
std::vector<float> object_pcm(512, 0.25f);
|
const auto object = CreatePort(context, param);
|
||||||
SetPcm(CreatePort(context, param), object_pcm.data());
|
|
||||||
param.user_handle = other_user;
|
param.user_handle = other_user;
|
||||||
SetPcm(CreatePort(context, param), object_pcm.data());
|
const auto other_user_object = CreatePort(context, param);
|
||||||
std::fill(object_pcm.begin(), object_pcm.end(), 1.0f);
|
std::vector<float> object_pcm(512, 0.25f);
|
||||||
SetPcm(CreatePort(other_context, param), object_pcm.data());
|
SetPcm(object, object_pcm.data());
|
||||||
|
SetPcm(other_user_object, object_pcm.data());
|
||||||
|
SetPcm(CreatePort(other_context, param), std::vector<float>(512, 1.0f).data());
|
||||||
constexpr auto bytes = 512 * 2 * sizeof(float);
|
constexpr auto bytes = 512 * 2 * sizeof(float);
|
||||||
for (int push = 0; push < 2; push++) {
|
const auto output = PushPcm(context, bytes);
|
||||||
const auto output = PushPcm(context, bytes);
|
Check(output.size() == 3, "object bus was missing or queued more than once");
|
||||||
Check(output.size() == 3, "object bus was missing or queued more than once");
|
CheckSamples(output[0], {0.125f}, "objects modified first bed");
|
||||||
CheckSamples(output[0], {0.125f}, "objects modified first bed");
|
CheckSamples(output[1], {0.125f}, "objects modified second bed");
|
||||||
CheckSamples(output[1], {0.125f}, "objects modified second bed");
|
CheckSamples(output[2], {0.35355339f}, "MAIN objects failed to mix across users or leaked contexts");
|
||||||
CheckSamples(output[2], {0.35355339f}, "MAIN objects failed to mix across users or leaked contexts");
|
SetPcm(object, object_pcm.data());
|
||||||
}
|
SetPcm(other_user_object, object_pcm.data());
|
||||||
SetPcm(first, nullptr);
|
|
||||||
SetPcm(second, nullptr);
|
|
||||||
CheckSamples(PushPcm(context, bytes).at(0), {0.35355339f}, "object output depended on bed PCM");
|
CheckSamples(PushPcm(context, bytes).at(0), {0.35355339f}, "object output depended on bed PCM");
|
||||||
AudioOut2::AudioOut2ContextDestroy(context);
|
AudioOut2::AudioOut2ContextDestroy(context);
|
||||||
Check(LiveDeviceCount() == 1, "context destroy closed another context's object device");
|
Check(LiveDeviceCount() == 1, "context destroy closed another context's object device");
|
||||||
@@ -731,8 +916,7 @@ uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking)
|
|||||||
namespace Libs::LibKernel {
|
namespace Libs::LibKernel {
|
||||||
|
|
||||||
uint64_t KYTY_SYSV_ABI KernelGetProcessTime() {
|
uint64_t KYTY_SYSV_ABI KernelGetProcessTime() {
|
||||||
static std::atomic_uint64_t now {0};
|
return g_process_time_frozen ? g_process_time.load() : g_process_time.fetch_add(1000);
|
||||||
return now.fetch_add(1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Libs::LibKernel
|
} // namespace Libs::LibKernel
|
||||||
@@ -741,6 +925,9 @@ int main() {
|
|||||||
Check(AudioOut2::AudioOut2UserCreate(1000, &g_user_handle) == OK, "test user create failed");
|
Check(AudioOut2::AudioOut2UserCreate(1000, &g_user_handle) == OK, "test user create failed");
|
||||||
TestUserSupportedAttributes();
|
TestUserSupportedAttributes();
|
||||||
TestSlotReuse();
|
TestSlotReuse();
|
||||||
|
TestObjectPortCapacityPerContext();
|
||||||
|
TestContextPortLimits();
|
||||||
|
TestContextCapacityParameters();
|
||||||
TestFullTableRecovers();
|
TestFullTableRecovers();
|
||||||
TestConcurrentCreates();
|
TestConcurrentCreates();
|
||||||
TestContextDestroyCancelsPendingCreate();
|
TestContextDestroyCancelsPendingCreate();
|
||||||
@@ -748,6 +935,8 @@ int main() {
|
|||||||
TestFloat12ChannelPortOutputsPcm();
|
TestFloat12ChannelPortOutputsPcm();
|
||||||
TestAsynchronousDevicePushKeepsQueueBounded();
|
TestAsynchronousDevicePushKeepsQueueBounded();
|
||||||
TestHandleWithoutPcmDoesNotBypassQueue();
|
TestHandleWithoutPcmDoesNotBypassQueue();
|
||||||
|
TestPushConsumesPcm();
|
||||||
|
TestQueueDrainsAtExactGrainRate();
|
||||||
TestPcmCopiedBeforeScratchBufferReuse();
|
TestPcmCopiedBeforeScratchBufferReuse();
|
||||||
TestPortGainAndValidation();
|
TestPortGainAndValidation();
|
||||||
TestObjectGainAndAmbisonics();
|
TestObjectGainAndAmbisonics();
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#if !defined(__AVX2__) || !defined(__BMI2__) || !defined(__FMA__)
|
||||||
|
#error "x86-64 builds target x86-64-v3"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// (1 + 2^-23)^2 = 1 + 2^-22 + 2^-46; only a fused multiply-add keeps the 2^-46.
|
||||||
|
volatile float a = 1.0f + 0x1p-23f;
|
||||||
|
const float result = a * a - (1.0f + 0x1p-22f);
|
||||||
|
if (result != 0.0f) {
|
||||||
|
std::fprintf(stderr, "BuildFlagsTests: failed: a * a - b was contracted (%a)\n", result);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::puts("BuildFlagsTests: all cases passed");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -689,6 +689,27 @@ void TestTriggerEffectState() {
|
|||||||
check_state(0, 0, "switching controllers retained effect states");
|
check_state(0, 0, "switching controllers retained effect states");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestUnreadPressSurvivesStickBurst() {
|
||||||
|
Controller controller;
|
||||||
|
std::array<PadData, 64> data {};
|
||||||
|
SetButton(1, PAD_BUTTON_CROSS, true);
|
||||||
|
for (int value = 0; value < 100; value++) {
|
||||||
|
SetAxis(1, Axis::LeftX, value);
|
||||||
|
}
|
||||||
|
SetButton(1, PAD_BUTTON_CROSS, false);
|
||||||
|
for (int value = 100; value < 200; value++) {
|
||||||
|
SetAxis(1, Axis::LeftX, value);
|
||||||
|
}
|
||||||
|
Check(PadRead(1, data.data(), 64) == 2 && data[0].buttons == PAD_BUTTON_CROSS &&
|
||||||
|
data[0].left_stick_x == 99 && data[1].buttons == 0 && data[1].left_stick_x == 199,
|
||||||
|
"a stick burst pushed an unread press out of the pad history");
|
||||||
|
SetAxis(1, Axis::LeftX, 50);
|
||||||
|
SetButton(1, PAD_BUTTON_CROSS, true);
|
||||||
|
Check(PadRead(1, data.data(), 64) == 2 && data[0].buttons == 0 &&
|
||||||
|
data[0].left_stick_x == 50 && data[1].buttons == PAD_BUTTON_CROSS,
|
||||||
|
"a stick move after a read did not add a sample");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
@@ -702,6 +723,7 @@ int main() {
|
|||||||
TestMaskedTriggersAndValidation();
|
TestMaskedTriggersAndValidation();
|
||||||
TestIndependentOutputsAndPadSwitch();
|
TestIndependentOutputsAndPadSwitch();
|
||||||
TestTriggerEffectState();
|
TestTriggerEffectState();
|
||||||
|
TestUnreadPressSurvivesStickBurst();
|
||||||
TestMultiplayerRouting();
|
TestMultiplayerRouting();
|
||||||
TestDedicatedKeyboardOutputs();
|
TestDedicatedKeyboardOutputs();
|
||||||
Config::Shutdown();
|
Config::Shutdown();
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <iterator>
|
||||||
|
#include <type_traits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -13,6 +15,11 @@ using Table = Libs::Graphics::MultiLevelPageTable<Owners, 20, 44, 14>;
|
|||||||
using PageOwners = Libs::Graphics::InlinePageOwnerList<uint32_t, 16>;
|
using PageOwners = Libs::Graphics::InlinePageOwnerList<uint32_t, 16>;
|
||||||
using OwnerTable = Libs::Graphics::MultiLevelPageTable<PageOwners, 20, 44, 14>;
|
using OwnerTable = Libs::Graphics::MultiLevelPageTable<PageOwners, 20, 44, 14>;
|
||||||
|
|
||||||
|
// MSVC's STL and libc++ don't need these traits for std::any_of, but libstdc++ does, so only a
|
||||||
|
// Linux build would notice them missing.
|
||||||
|
static_assert(std::is_same_v<std::iterator_traits<PageOwners::const_iterator>::iterator_category,
|
||||||
|
std::input_iterator_tag>);
|
||||||
|
|
||||||
void Check(bool value, const char* text) {
|
void Check(bool value, const char* text) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
std::fprintf(stderr, "ImagePageTableTests: failed: %s\n", text);
|
std::fprintf(stderr, "ImagePageTableTests: failed: %s\n", text);
|
||||||
|
|||||||
@@ -1314,6 +1314,62 @@ void TestSignedStorageSpecialization() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One descriptor serves a store, an atomic that a user-data condition guards, and an image no
|
||||||
|
// block lists. Unreached, the atomic reads a zero descriptor instead of the RGBA8 format the
|
||||||
|
// others use; reached, it rejects that format.
|
||||||
|
void TestUnreachedImageIgnoresSharedDescriptor(bool atomic64) {
|
||||||
|
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||||
|
using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
|
||||||
|
using Libs::Graphics::Prospero::BufferFormat;
|
||||||
|
Program program;
|
||||||
|
program.stage = Libs::Graphics::ShaderType::Compute;
|
||||||
|
program.srt_plan_complete = program.resource_tracking_complete = true;
|
||||||
|
program.user_data_count = 2u;
|
||||||
|
auto &entry = AddValueBlock(program);
|
||||||
|
auto &guarded = AddValueBlock(program);
|
||||||
|
auto &done = AddValueBlock(program);
|
||||||
|
const auto user = [&](uint32_t index) {
|
||||||
|
return Value(&entry.AppendNewInst(ValueOpcode::GetUserData, {Value(ScalarReg(index))}));
|
||||||
|
};
|
||||||
|
DescriptorSource source;
|
||||||
|
source.dword_count = 8u;
|
||||||
|
source.dwords.fill(Value(0u));
|
||||||
|
source.dwords[0] = Value(0x100u);
|
||||||
|
source.dwords[1] = user(0);
|
||||||
|
source.dwords[3] = Value(uint32_t(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
|
||||||
|
program.descriptor_sources.push_back(source);
|
||||||
|
const auto dimension = Libs::Graphics::ShaderRecompiler::Decoder::ImageDimension::Dim2D;
|
||||||
|
for (const bool atomic : {false, true, false})
|
||||||
|
program.info.images.push_back({.source = 0u, .resource_class = ImageResourceClass::Storage,
|
||||||
|
.dimension = dimension, .written = true, .atomic = atomic, .atomic64 = atomic && atomic64});
|
||||||
|
for (uint32_t image = 0; image < 2u; ++image)
|
||||||
|
program.memory_info.push_back({.kind = ResourceKind::Image, .resource = image});
|
||||||
|
entry.AppendNewInst(ValueOpcode::ImageWrite, {Value(0u), Value(0u), Value(0u), Value(true)})
|
||||||
|
.SetFlags(MemoryFlags{.index = 0});
|
||||||
|
guarded.AppendNewInst(atomic64 ? ValueOpcode::ImageAtomicUMax64 : ValueOpcode::ImageAtomicUMax32,
|
||||||
|
{Value(0u), Value(0u), Value(0u), Value(true)})
|
||||||
|
.SetFlags(MemoryFlags{.index = 1});
|
||||||
|
entry.condition = Value(&entry.AppendNewInst(ValueOpcode::INotEqual32, {user(1), Value(0u)}));
|
||||||
|
entry.terminator.kind = TerminatorKind::ConditionalBranch;
|
||||||
|
entry.terminator.true_block = &guarded;
|
||||||
|
entry.terminator.false_block = &done;
|
||||||
|
guarded.terminator.kind = TerminatorKind::Branch;
|
||||||
|
guarded.terminator.true_block = &done;
|
||||||
|
done.terminator.kind = TerminatorKind::Return;
|
||||||
|
auto plan = Extract(program);
|
||||||
|
std::array<uint32_t, 2> data{uint32_t(BufferFormat::k8_8_8_8UNorm) << 20u, 0u};
|
||||||
|
const SrtRuntime runtime{.user_data = data};
|
||||||
|
ResourceSnapshot snapshot;
|
||||||
|
ResourceSpecialization specialization;
|
||||||
|
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||||
|
snapshot.images[0].dwords[1] == data[0] && snapshot.images[2].dwords[1] == data[0] &&
|
||||||
|
snapshot.images[1] == DescriptorValue{.dword_count = 8u},
|
||||||
|
"unreached atomic image kept the shared descriptor or an unlisted image lost it");
|
||||||
|
data[1] = 1u;
|
||||||
|
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||||
|
"reached atomic image accepted a format its width rejects");
|
||||||
|
}
|
||||||
|
|
||||||
void TestIndirectImageSamplerClasses(bool mixed) {
|
void TestIndirectImageSamplerClasses(bool mixed) {
|
||||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||||
using Libs::Graphics::Prospero::BufferFormat;
|
using Libs::Graphics::Prospero::BufferFormat;
|
||||||
@@ -1451,6 +1507,7 @@ int main() {
|
|||||||
TestEmptyScalarBufferSpecialization();
|
TestEmptyScalarBufferSpecialization();
|
||||||
TestMixedSamplerVariantsShareRuntimeDescriptor();
|
TestMixedSamplerVariantsShareRuntimeDescriptor();
|
||||||
TestSignedStorageSpecialization();
|
TestSignedStorageSpecialization();
|
||||||
|
for (const bool atomic64 : {false, true}) TestUnreachedImageIgnoresSharedDescriptor(atomic64);
|
||||||
for (const bool mixed : {false, true}) TestIndirectImageSamplerClasses(mixed);
|
for (const bool mixed : {false, true}) TestIndirectImageSamplerClasses(mixed);
|
||||||
TestShaderCacheRecords();
|
TestShaderCacheRecords();
|
||||||
std::puts("ResourceMaterializationTests: all cases passed");
|
std::puts("ResourceMaterializationTests: all cases passed");
|
||||||
|
|||||||
+150
-53
@@ -1282,15 +1282,18 @@ void TestIndexedPointerImageTable() {
|
|||||||
{image, sampler, fixture->ImageAddress(), parent}, fixture->AddMemory(image_memory, 0x8bcu));
|
{image, sampler, fixture->ImageAddress(), parent}, fixture->AddMemory(image_memory, 0x8bcu));
|
||||||
return fixture;
|
return fixture;
|
||||||
};
|
};
|
||||||
|
const auto nested_geometry = [](const auto& plan, uint32_t count) {
|
||||||
|
const auto& indirect = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
|
||||||
|
return indirect.table_maximum == 1u && indirect.pointer_table &&
|
||||||
|
indirect.pointer_table->indexed && indirect.pointer_table->count == count &&
|
||||||
|
indirect.pointer_table->stride == 368u && indirect.pointer_table->offset == 64u;
|
||||||
|
};
|
||||||
auto fixture = make();
|
auto fixture = make();
|
||||||
fixture->PlanAndTrack();
|
fixture->PlanAndTrack();
|
||||||
const auto plan = ExtractResourcePlan(fixture->program);
|
const auto plan = ExtractResourcePlan(fixture->program);
|
||||||
Check(plan.info.images.size() == 1u && plan.info.buffers.size() == 1u,
|
Check(plan.info.images.size() == 1u && plan.info.buffers.size() == 1u,
|
||||||
"indexed nested table expanded IR candidates or discarded the GPU pointer load");
|
"indexed nested table expanded IR candidates or discarded the GPU pointer load");
|
||||||
const auto& indirect = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
|
Check(nested_geometry(plan, 3u),
|
||||||
Check(indirect.table_maximum == 1u && indirect.pointer_table &&
|
|
||||||
indirect.pointer_table->indexed && indirect.pointer_table->count == 3u &&
|
|
||||||
indirect.pointer_table->stride == 368u && indirect.pointer_table->offset == 64u,
|
|
||||||
"nested table did not preserve separate parent and child geometry");
|
"nested table did not preserve separate parent and child geometry");
|
||||||
constexpr uint64_t high = uint64_t{1} << 32u;
|
constexpr uint64_t high = uint64_t{1} << 32u;
|
||||||
constexpr uint32_t table = 0x2000u, objects = 0x10000u, row_bytes = 1536u;
|
constexpr uint32_t table = 0x2000u, objects = 0x10000u, row_bytes = 1536u;
|
||||||
@@ -1389,8 +1392,13 @@ void TestIndexedPointerImageTable() {
|
|||||||
"explicit null pointer bypassed the specialization reader");
|
"explicit null pointer bypassed the specialization reader");
|
||||||
auto reversed = make(Case::ReverseSigned);
|
auto reversed = make(Case::ReverseSigned);
|
||||||
reversed->PlanAndTrack();
|
reversed->PlanAndTrack();
|
||||||
|
// An inclusive child limit bounds one more child record, never the exclusive count.
|
||||||
|
auto inclusive = make(Case::InclusiveChild);
|
||||||
|
inclusive->PlanAndTrack();
|
||||||
|
Check(nested_geometry(ExtractResourcePlan(inclusive->program), 4u),
|
||||||
|
"an inclusive child limit did not bound one more child record");
|
||||||
for (const auto variant : {Case::MissingParent, Case::WrongParent, Case::MissingChild,
|
for (const auto variant : {Case::MissingParent, Case::WrongParent, Case::MissingChild,
|
||||||
Case::InclusiveChild, Case::MismatchedPair, Case::DifferentLaneMask,
|
Case::MismatchedPair, Case::DifferentLaneMask,
|
||||||
Case::ExtraUse, Case::UnboundedChoice, Case::MissingOuterWitness, Case::WrongOuterWitness,
|
Case::ExtraUse, Case::UnboundedChoice, Case::MissingOuterWitness, Case::WrongOuterWitness,
|
||||||
Case::CyclicChoice, Case::ExcessiveDomain}) {
|
Case::CyclicChoice, Case::ExcessiveDomain}) {
|
||||||
auto invalid = make(variant);
|
auto invalid = make(variant);
|
||||||
@@ -2497,11 +2505,15 @@ void TestDescendingImageRange() {
|
|||||||
enum class Variant {
|
enum class Variant {
|
||||||
Valid, WrongStep, IncrementBypass, MissingNonzero, NegativeInitial,
|
Valid, WrongStep, IncrementBypass, MissingNonzero, NegativeInitial,
|
||||||
Bitmap, BitmapAddsBit, BitmapWrongWord, BitmapUnknownInitial,
|
Bitmap, BitmapAddsBit, BitmapWrongWord, BitmapUnknownInitial,
|
||||||
BitmapByteOffset, BitmapScalarOffset, BitmapOuterClobber, BitmapWrongLoadMask
|
BitmapByteOffset, BitmapScalarOffset, BitmapOuterClobber, BitmapWrongLoadMask,
|
||||||
|
Bitmap8, Bitmap8MisplacedLoad, Bitmap8OtherBuffer, Bitmap8OuterClobber, Bitmap8WrongUpperWord,
|
||||||
|
Bitmap8NarrowMask, Bitmap8OtherGuard
|
||||||
};
|
};
|
||||||
const auto make_plan = [](Variant variant) {
|
const auto make_plan = [](Variant variant) {
|
||||||
Fixture fixture;
|
Fixture fixture;
|
||||||
const bool bitmap = variant >= Variant::Bitmap;
|
const bool bitmap = variant >= Variant::Bitmap;
|
||||||
|
// Eight-word records are two four-word loads of one buffer record.
|
||||||
|
const uint32_t bitmap_words = variant >= Variant::Bitmap8 ? 8u : 4u;
|
||||||
auto *entry = fixture.block;
|
auto *entry = fixture.block;
|
||||||
auto *header = fixture.AddBlock();
|
auto *header = fixture.AddBlock();
|
||||||
auto *guard = fixture.AddBlock();
|
auto *guard = fixture.AddBlock();
|
||||||
@@ -2524,11 +2536,11 @@ void TestDescendingImageRange() {
|
|||||||
branch(latch, header);
|
branch(latch, header);
|
||||||
auto &induction = header->AppendNewInst(ValueOpcode::Phi);
|
auto &induction = header->AppendNewInst(ValueOpcode::Phi);
|
||||||
induction.SetFlags(Type::U32);
|
induction.SetFlags(Type::U32);
|
||||||
std::array<Inst *, 4> outer_words {}, remaining_words {};
|
std::array<Inst *, 8> outer_words {}, remaining_words {};
|
||||||
std::array<Value, 4> loaded_words {};
|
std::array<Value, 8> loaded_words {};
|
||||||
if (bitmap)
|
if (bitmap)
|
||||||
for (auto& word : outer_words)
|
for (uint32_t word = 0; word < bitmap_words; ++word)
|
||||||
word = &header->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
|
outer_words[word] = &header->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
|
||||||
const auto bound = fixture.Emit(ValueOpcode::SGreaterThanEqual32,
|
const auto bound = fixture.Emit(ValueOpcode::SGreaterThanEqual32,
|
||||||
{Value(&induction), Value(0u)}, 0, header);
|
{Value(&induction), Value(0u)}, 0, header);
|
||||||
choose(header, guard, variant == Variant::IncrementBypass ? latch : exit, bound);
|
choose(header, guard, variant == Variant::IncrementBypass ? latch : exit, bound);
|
||||||
@@ -2545,14 +2557,27 @@ void TestDescendingImageRange() {
|
|||||||
read.idxen = true;
|
read.idxen = true;
|
||||||
read.offen = variant == Variant::BitmapByteOffset;
|
read.offen = variant == Variant::BitmapByteOffset;
|
||||||
read.data_dwords = 4u;
|
read.data_dwords = 4u;
|
||||||
const auto packed = fixture.Emit(ValueOpcode::LoadBufferU32x4,
|
std::array<Value, 2> packed;
|
||||||
{buffer, lane,
|
for (uint32_t part = 0; part < bitmap_words / 4u; ++part) {
|
||||||
Value(variant == Variant::BitmapByteOffset ? 4u : 0u),
|
read.offset = part * (variant == Variant::Bitmap8MisplacedLoad ? 32u : 16u);
|
||||||
Value(variant == Variant::BitmapScalarOffset ? 4u : 0u), load_active},
|
const auto source = variant == Variant::Bitmap8OtherBuffer && part != 0u
|
||||||
fixture.AddMemory(read, 0x10));
|
? fixture.Buffer({fixture.UserData(8), fixture.UserData(9), fixture.UserData(10),
|
||||||
for (uint32_t word = 0; word < 4u; ++word) {
|
fixture.UserData(11)}) : buffer;
|
||||||
|
const auto guard = variant == Variant::Bitmap8OtherGuard && part != 0u
|
||||||
|
? fixture.Emit(ValueOpcode::LogicalAnd,
|
||||||
|
{load_active, fixture.Emit(ValueOpcode::ULessThan32, {lane, Value(16u)})})
|
||||||
|
: load_active;
|
||||||
|
packed[part] = fixture.Emit(ValueOpcode::LoadBufferU32x4,
|
||||||
|
{source, lane,
|
||||||
|
Value(variant == Variant::BitmapByteOffset ? 4u : 0u),
|
||||||
|
Value(variant == Variant::BitmapScalarOffset ? 4u : 0u), guard},
|
||||||
|
fixture.AddMemory(read, 0x10 + part * 8u));
|
||||||
|
}
|
||||||
|
for (uint32_t word = 0; word < bitmap_words; ++word) {
|
||||||
|
const bool wrong_word = variant == Variant::BitmapWrongWord ||
|
||||||
|
(variant == Variant::Bitmap8WrongUpperWord && word >= 4u);
|
||||||
auto component = fixture.Emit(ValueOpcode::CompositeExtractU32x4,
|
auto component = fixture.Emit(ValueOpcode::CompositeExtractU32x4,
|
||||||
{packed, Value(variant == Variant::BitmapWrongWord ? (word + 1u) % 4u : word)}, 0, entry);
|
{packed[word / 4u], Value((wrong_word ? word + 1u : word) % 4u)}, 0, entry);
|
||||||
component = fixture.Emit(ValueOpcode::SelectU32,
|
component = fixture.Emit(ValueOpcode::SelectU32,
|
||||||
{load_active, component, fixture.Emit(ValueOpcode::UndefU32)});
|
{load_active, component, fixture.Emit(ValueOpcode::UndefU32)});
|
||||||
loaded_words[word] = component;
|
loaded_words[word] = component;
|
||||||
@@ -2565,7 +2590,7 @@ void TestDescendingImageRange() {
|
|||||||
remaining_words[word]->AddPhiOperand(header, Value(outer_words[word]));
|
remaining_words[word]->AddPhiOperand(header, Value(outer_words[word]));
|
||||||
}
|
}
|
||||||
auto selected = fixture.Emit(ValueOpcode::UndefU32, {}, 0, guard);
|
auto selected = fixture.Emit(ValueOpcode::UndefU32, {}, 0, guard);
|
||||||
for (uint32_t word = 0; word < 4u; ++word) {
|
for (uint32_t word = 0; word < bitmap_words; ++word) {
|
||||||
selected = fixture.Emit(ValueOpcode::SelectU32,
|
selected = fixture.Emit(ValueOpcode::SelectU32,
|
||||||
{fixture.Emit(ValueOpcode::IEqual32, {Value(&induction), Value(word)}, 0, guard),
|
{fixture.Emit(ValueOpcode::IEqual32, {Value(&induction), Value(word)}, 0, guard),
|
||||||
Value(remaining_words[word]), selected}, 0, guard);
|
Value(remaining_words[word]), selected}, 0, guard);
|
||||||
@@ -2628,32 +2653,39 @@ void TestDescendingImageRange() {
|
|||||||
{fixture.Emit(ValueOpcode::ShiftLeftLogical32, {Value(1u), bit})})});
|
{fixture.Emit(ValueOpcode::ShiftLeftLogical32, {Value(1u), bit})})});
|
||||||
if (variant == Variant::BitmapAddsBit)
|
if (variant == Variant::BitmapAddsBit)
|
||||||
next = fixture.Emit(ValueOpcode::BitwiseOr32, {next, Value(1u << 21u)});
|
next = fixture.Emit(ValueOpcode::BitwiseOr32, {next, Value(1u << 21u)});
|
||||||
for (uint32_t word = 0; word < 4u; ++word) {
|
for (uint32_t word = 0; word < bitmap_words; ++word) {
|
||||||
|
// Relative register writes index with M0 & 0xff; a narrower mask aliases upper words.
|
||||||
|
const auto index = bitmap_words == 4u ? Value(&induction)
|
||||||
|
: fixture.Emit(ValueOpcode::BitwiseAnd32,
|
||||||
|
{Value(&induction), Value(variant == Variant::Bitmap8NarrowMask ? 3u : 255u)});
|
||||||
const auto selected_word = fixture.Emit(ValueOpcode::LogicalAnd,
|
const auto selected_word = fixture.Emit(ValueOpcode::LogicalAnd,
|
||||||
{active, fixture.Emit(ValueOpcode::IEqual32, {Value(&induction), Value(word)})});
|
{active, fixture.Emit(ValueOpcode::IEqual32, {index, Value(word)})});
|
||||||
remaining_words[word]->AddPhiOperand(body, fixture.Emit(ValueOpcode::SelectU32,
|
remaining_words[word]->AddPhiOperand(body, fixture.Emit(ValueOpcode::SelectU32,
|
||||||
{selected_word, next, Value(remaining_words[word])}));
|
{selected_word, next, Value(remaining_words[word])}));
|
||||||
auto carried = Value(remaining_words[word]);
|
auto carried = Value(remaining_words[word]);
|
||||||
if (variant == Variant::BitmapOuterClobber)
|
if (variant == Variant::BitmapOuterClobber || variant == Variant::Bitmap8OuterClobber)
|
||||||
carried = fixture.Emit(ValueOpcode::SelectU32,
|
carried = fixture.Emit(ValueOpcode::SelectU32,
|
||||||
{fixture.Emit(ValueOpcode::IEqual32,
|
{fixture.Emit(ValueOpcode::IEqual32,
|
||||||
{Value(&induction), Value(word + 1u)}, 0, latch),
|
{Value(&induction), Value(word + 1u)}, 0, latch),
|
||||||
loaded_words[(word + 1u) % 4u], carried}, 0, latch);
|
loaded_words[(word + 1u) % bitmap_words], carried}, 0, latch);
|
||||||
outer_words[word]->AddPhiOperand(latch, carried);
|
outer_words[word]->AddPhiOperand(latch, carried);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const auto step = fixture.Emit(ValueOpcode::IAdd32,
|
const auto step = fixture.Emit(ValueOpcode::IAdd32,
|
||||||
{Value(&induction), Value(variant == Variant::WrongStep ? UINT32_MAX - 1u : UINT32_MAX)}, 0, latch);
|
{Value(&induction), Value(variant == Variant::WrongStep ? UINT32_MAX - 1u : UINT32_MAX)}, 0, latch);
|
||||||
induction.AddPhiOperand(entry, Value(variant == Variant::NegativeInitial ? UINT32_MAX : 3u));
|
induction.AddPhiOperand(entry,
|
||||||
|
Value(variant == Variant::NegativeInitial ? UINT32_MAX : bitmap_words - 1u));
|
||||||
induction.AddPhiOperand(latch, step);
|
induction.AddPhiOperand(latch, step);
|
||||||
fixture.PlanAndTrack();
|
fixture.PlanAndTrack();
|
||||||
Check(ordinal.Instruction()->Arg(1).Resolve() == bit.Resolve(),
|
Check(ordinal.Instruction()->Arg(1).Resolve() == bit.Resolve(),
|
||||||
"descending range retained impossible relative-register stores");
|
"descending range retained impossible relative-register stores");
|
||||||
const auto &indirect = fixture.program.descriptor_sources[
|
const auto &indirect = fixture.program.descriptor_sources[
|
||||||
fixture.program.info.images[0].source].indirect_descriptor;
|
fixture.program.info.images[0].source].indirect_descriptor;
|
||||||
Check(indirect && indirect->selector.has_value() == (variant == Variant::Bitmap) &&
|
const bool selects = variant == Variant::Bitmap || variant == Variant::Bitmap8;
|
||||||
(!indirect->selector || indirect->selector->bitset) &&
|
Check(indirect && indirect->selector.has_value() == selects &&
|
||||||
indirect->key_count == Value(128u) &&
|
(!indirect->selector || (indirect->selector->bitset &&
|
||||||
|
indirect->selector->stride == bitmap_words * 4u)) &&
|
||||||
|
indirect->key_count == Value(bitmap_words * 32u) &&
|
||||||
!indirect->selector_first.IsEmpty(),
|
!indirect->selector_first.IsEmpty(),
|
||||||
"descending bitscan range lost its bound or accepted an unproved bitmap subset");
|
"descending bitscan range lost its bound or accepted an unproved bitmap subset");
|
||||||
return ExtractResourcePlan(fixture.program);
|
return ExtractResourcePlan(fixture.program);
|
||||||
@@ -2693,10 +2725,14 @@ void TestDescendingImageRange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto bitmap_plan = make_plan(Variant::Bitmap);
|
auto bitmap_plan = make_plan(Variant::Bitmap);
|
||||||
|
auto wide_plan = make_plan(Variant::Bitmap8);
|
||||||
for (const auto variant : {Variant::BitmapAddsBit, Variant::BitmapWrongWord,
|
for (const auto variant : {Variant::BitmapAddsBit, Variant::BitmapWrongWord,
|
||||||
Variant::BitmapUnknownInitial, Variant::BitmapByteOffset,
|
Variant::BitmapUnknownInitial, Variant::BitmapByteOffset,
|
||||||
Variant::BitmapScalarOffset, Variant::BitmapOuterClobber,
|
Variant::BitmapScalarOffset, Variant::BitmapOuterClobber,
|
||||||
Variant::BitmapWrongLoadMask})
|
Variant::BitmapWrongLoadMask, Variant::Bitmap8MisplacedLoad,
|
||||||
|
Variant::Bitmap8OtherBuffer, Variant::Bitmap8OuterClobber,
|
||||||
|
Variant::Bitmap8WrongUpperWord, Variant::Bitmap8NarrowMask,
|
||||||
|
Variant::Bitmap8OtherGuard})
|
||||||
make_plan(variant);
|
make_plan(variant);
|
||||||
// Every descriptor is distinct, so retained keys are visible in the snapshot as well as
|
// Every descriptor is distinct, so retained keys are visible in the snapshot as well as
|
||||||
// its lookup table. An unrelated table slot must never be read, even when it is malformed.
|
// its lookup table. An unrelated table slot must never be read, even when it is malformed.
|
||||||
@@ -2711,23 +2747,25 @@ void TestDescendingImageRange() {
|
|||||||
const SrtRuntime bitmap_runtime {.user_data = bitmap_data, .userdata = &memory,
|
const SrtRuntime bitmap_runtime {.user_data = bitmap_data, .userdata = &memory,
|
||||||
.read_specialization_memory = ReadLinearTestMemory};
|
.read_specialization_memory = ReadLinearTestMemory};
|
||||||
memory.watched_address = bitmap_address;
|
memory.watched_address = bitmap_address;
|
||||||
|
auto *checked_plan = &bitmap_plan;
|
||||||
const auto check_bitmap = [&](std::initializer_list<uint32_t> ranks, uint32_t count) {
|
const auto check_bitmap = [&](std::initializer_list<uint32_t> ranks, uint32_t count) {
|
||||||
bitmap_data[2] = count;
|
bitmap_data[2] = count;
|
||||||
memory.watched_reads = memory.descriptor_reads = 0u;
|
memory.watched_reads = memory.descriptor_reads = 0u;
|
||||||
std::vector<uint32_t> keys;
|
std::vector<uint32_t> keys;
|
||||||
for (const auto rank : ranks) keys.push_back(count - rank);
|
for (const auto rank : ranks) keys.push_back(count - rank);
|
||||||
std::ranges::sort(keys);
|
std::ranges::sort(keys);
|
||||||
Check(MaterializeResources(bitmap_plan, bitmap_runtime, snapshot, specialization) &&
|
const auto dwords = bitmap_data[6] * (bitmap_data[5] >> 18u);
|
||||||
|
Check(MaterializeResources(*checked_plan, bitmap_runtime, snapshot, specialization) &&
|
||||||
memory.watched_reads == uint32_t(bitmap_data[6] != 0u) &&
|
memory.watched_reads == uint32_t(bitmap_data[6] != 0u) &&
|
||||||
(bitmap_data[6] == 0u || memory.watched_dwords ==
|
(bitmap_data[6] == 0u || memory.watched_dwords ==
|
||||||
std::min(bitmap_data[6] * 4u, MaxIndirectDescriptorProbes)) &&
|
std::min(dwords, MaxIndirectDescriptorProbes)) &&
|
||||||
memory.descriptor_reads == keys.size() &&
|
memory.descriptor_reads == keys.size() &&
|
||||||
snapshot.images.size() == std::max<size_t>(keys.size(), 1u),
|
snapshot.images.size() == std::max<size_t>(keys.size(), 1u),
|
||||||
"bitmap selector lost bounded reads or retained unreachable descriptors");
|
"bitmap selector lost bounded reads or retained unreachable descriptors");
|
||||||
for (uint32_t first = 0; first < bitmap_data[6] * 4u; first += MaxIndirectDescriptorProbes)
|
for (uint32_t first = 0; first < dwords; first += MaxIndirectDescriptorProbes)
|
||||||
Check(std::ranges::find(snapshot.specialization_reads,
|
Check(std::ranges::find(snapshot.specialization_reads,
|
||||||
std::pair{uint64_t{bitmap_address} + first * 4u,
|
std::pair{uint64_t{bitmap_address} + first * 4u,
|
||||||
uint64_t{std::min(bitmap_data[6] * 4u - first, MaxIndirectDescriptorProbes)} * 4u}) !=
|
uint64_t{std::min(dwords - first, MaxIndirectDescriptorProbes)} * 4u}) !=
|
||||||
snapshot.specialization_reads.end(),
|
snapshot.specialization_reads.end(),
|
||||||
"bitmap chunk was omitted from same-dispatch write-alias protection");
|
"bitmap chunk was omitted from same-dispatch write-alias protection");
|
||||||
if (keys.empty()) {
|
if (keys.empty()) {
|
||||||
@@ -2779,8 +2817,8 @@ void TestDescendingImageRange() {
|
|||||||
memory.fail_address = UINT64_MAX;
|
memory.fail_address = UINT64_MAX;
|
||||||
|
|
||||||
const auto valid_data = bitmap_data;
|
const auto valid_data = bitmap_data;
|
||||||
for (const auto [word, value] : std::array<std::pair<uint32_t, uint32_t>, 6>{{
|
for (const auto [word, value] : std::array<std::pair<uint32_t, uint32_t>, 7>{{
|
||||||
{4u, bitmap_address + 1u}, {5u, 8u << 16u}, {5u, (16u << 16u) | (1u << 31u)},
|
{4u, bitmap_address + 1u}, {5u, 8u << 16u}, {5u, 32u << 16u}, {5u, (16u << 16u) | (1u << 31u)},
|
||||||
{7u, 0x5204u | (1u << 23u)}, {7u, 0x5204u | (1u << 28u)},
|
{7u, 0x5204u | (1u << 23u)}, {7u, 0x5204u | (1u << 28u)},
|
||||||
{7u, 0x5204u | (1u << 30u)}}}) {
|
{7u, 0x5204u | (1u << 30u)}}}) {
|
||||||
bitmap_data[word] = value;
|
bitmap_data[word] = value;
|
||||||
@@ -2820,6 +2858,23 @@ void TestDescendingImageRange() {
|
|||||||
return read.first >= memory.fail_address;
|
return read.first >= memory.fail_address;
|
||||||
}),
|
}),
|
||||||
"a complete bitmap domain read redundant chunks or lost candidate keys");
|
"a complete bitmap domain read redundant chunks or lost candidate keys");
|
||||||
|
|
||||||
|
// Eight-word records keep their upper words' ranks, and a key below the table is never read.
|
||||||
|
checked_plan = &wide_plan;
|
||||||
|
bitmap_data = valid_data;
|
||||||
|
bitmap_data[5] = 32u << 16u;
|
||||||
|
bitmap_data[6] = 2u;
|
||||||
|
std::fill_n(memory.words.begin() + bitmap_word, 16u, 0u);
|
||||||
|
memory.words[bitmap_word] = 1u | (1u << 2u);
|
||||||
|
memory.words[bitmap_word + 12u] = 1u << 1u;
|
||||||
|
memory.fail_address = 0x2000u - 32u;
|
||||||
|
check_bitmap({0u, 2u, 129u}, 2u);
|
||||||
|
memory.fail_address = UINT64_MAX;
|
||||||
|
for (const auto stride : {16u, 64u}) {
|
||||||
|
bitmap_data[5] = stride << 16u;
|
||||||
|
Check(!MaterializeResources(wide_plan, bitmap_runtime, snapshot, specialization),
|
||||||
|
"an eight-word bitmap selector accepted a different record stride");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestUniformizedMaterialImageKeys() {
|
void TestUniformizedMaterialImageKeys() {
|
||||||
@@ -4804,7 +4859,8 @@ void TestSharedAppendSelectorBounds() {
|
|||||||
Valid, PayloadOob, MissingInit, InactiveInit, UnknownInit, DuplicateAtomic,
|
Valid, PayloadOob, MissingInit, InactiveInit, UnknownInit, DuplicateAtomic,
|
||||||
WrongAtomicGuard, UnguardedCandidate, WrongStep, WrongLocalIndex, Multiwave,
|
WrongAtomicGuard, UnguardedCandidate, WrongStep, WrongLocalIndex, Multiwave,
|
||||||
OuterReentry, InnerAppendCycle, EarlyCounterRead, WrongConsumerBound, ExtraWriter, CounterAlias,
|
OuterReentry, InnerAppendCycle, EarlyCounterRead, WrongConsumerBound, ExtraWriter, CounterAlias,
|
||||||
PayloadWrap, CounterOob
|
PayloadWrap, CounterOob, Lane, LaneUnimpliedRead, LaneUnimpliedCounter, LaneCounterMask,
|
||||||
|
LaneOtherBypass, LaneInactiveSelect
|
||||||
};
|
};
|
||||||
const auto make_plan = [](Variant variant, uint32_t first_base = 1032u, uint32_t consumed_base = 8u) {
|
const auto make_plan = [](Variant variant, uint32_t first_base = 1032u, uint32_t consumed_base = 8u) {
|
||||||
Fixture fixture;
|
Fixture fixture;
|
||||||
@@ -4837,6 +4893,7 @@ void TestSharedAppendSelectorBounds() {
|
|||||||
auto *entry = fixture.block;
|
auto *entry = fixture.block;
|
||||||
auto *initialize = fixture.AddBlock();
|
auto *initialize = fixture.AddBlock();
|
||||||
auto *initialized = fixture.AddBlock();
|
auto *initialized = fixture.AddBlock();
|
||||||
|
const bool lane = variant >= Variant::Lane;
|
||||||
const auto material = fixture.Buffer({fixture.UserData(0), fixture.UserData(1),
|
const auto material = fixture.Buffer({fixture.UserData(0), fixture.UserData(1),
|
||||||
fixture.UserData(2), fixture.UserData(3)});
|
fixture.UserData(2), fixture.UserData(3)});
|
||||||
const auto table = fixture.Address(fixture.UserData(4), fixture.UserData(5));
|
const auto table = fixture.Address(fixture.UserData(4), fixture.UserData(5));
|
||||||
@@ -4855,12 +4912,18 @@ void TestSharedAppendSelectorBounds() {
|
|||||||
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(1u)});
|
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(1u)});
|
||||||
const auto local = fixture.Emit(ValueOpcode::IAdd32,
|
const auto local = fixture.Emit(ValueOpcode::IAdd32,
|
||||||
{x, fixture.Emit(ValueOpcode::ShiftLeftLogical32, {y, Value(3u)})});
|
{x, fixture.Emit(ValueOpcode::ShiftLeftLogical32, {y, Value(3u)})});
|
||||||
auto zero_lane = fixture.Emit(ValueOpcode::IEqual32, {local, Value(0u)});
|
// The negated form skips the initializer only when every lane is nonzero.
|
||||||
|
const auto local_is = [&](uint32_t value) {
|
||||||
|
return fixture.Emit(ValueOpcode::LogicalNot,
|
||||||
|
{fixture.Emit(ValueOpcode::INotEqual32, {local, Value(value)})});
|
||||||
|
};
|
||||||
|
auto zero_lane = lane ? local_is(0u) : fixture.Emit(ValueOpcode::IEqual32, {local, Value(0u)});
|
||||||
if (variant == Variant::InactiveInit) zero_lane = Value(false);
|
if (variant == Variant::InactiveInit) zero_lane = Value(false);
|
||||||
if (variant == Variant::UnknownInit)
|
if (variant == Variant::UnknownInit)
|
||||||
zero_lane = fixture.Emit(ValueOpcode::LogicalAnd,
|
zero_lane = fixture.Emit(ValueOpcode::LogicalAnd,
|
||||||
{zero_lane, fixture.Emit(ValueOpcode::IEqual32, {fixture.UserData(8), Value(0u)})});
|
{zero_lane, fixture.Emit(ValueOpcode::IEqual32, {fixture.UserData(8), Value(0u)})});
|
||||||
choose(entry, initialize, initialized, zero_lane, CFG::BranchCondition::ExecNonZero);
|
choose(entry, initialize, initialized, variant == Variant::LaneOtherBypass ? local_is(1u) : zero_lane,
|
||||||
|
CFG::BranchCondition::ExecNonZero);
|
||||||
if (variant != Variant::MissingInit)
|
if (variant != Variant::MissingInit)
|
||||||
fixture.Emit(ValueOpcode::WriteSharedU32x2,
|
fixture.Emit(ValueOpcode::WriteSharedU32x2,
|
||||||
{Value(0u), Value(0u), Value(0u), zero_lane}, shared(0u), initialize);
|
{Value(0u), Value(0u), Value(0u), zero_lane}, shared(0u), initialize);
|
||||||
@@ -4929,13 +4992,25 @@ void TestSharedAppendSelectorBounds() {
|
|||||||
if (variant == Variant::ExtraWriter)
|
if (variant == Variant::ExtraWriter)
|
||||||
fixture.Emit(ValueOpcode::WriteSharedU32,
|
fixture.Emit(ValueOpcode::WriteSharedU32,
|
||||||
{Value(0u), Value(UINT32_MAX), Value(true)}, shared(12u), previous);
|
{Value(0u), Value(UINT32_MAX), Value(true)}, shared(12u), previous);
|
||||||
if (final_counter.IsEmpty())
|
// Lane variants consume under a lane mask, which their counter and record reads use.
|
||||||
final_counter = fixture.Emit(ValueOpcode::LoadSharedU32,
|
Value lanes(true), other;
|
||||||
{Value(0u), Value(true)}, shared(0u), previous);
|
if (lane) {
|
||||||
|
lanes = fixture.Emit(ValueOpcode::ULessThan32, {y, fixture.UserData(8)}, 0, previous);
|
||||||
|
other = fixture.Emit(ValueOpcode::ULessThan32, {x, fixture.UserData(8)}, 0, previous);
|
||||||
|
}
|
||||||
|
auto *guarded = lane ? fixture.AddBlock() : previous;
|
||||||
auto *header = fixture.AddBlock();
|
auto *header = fixture.AddBlock();
|
||||||
auto *body = fixture.AddBlock();
|
auto *body = fixture.AddBlock();
|
||||||
auto *latch = fixture.AddBlock();
|
auto *latch = fixture.AddBlock();
|
||||||
auto *exit = fixture.AddBlock();
|
auto *exit = fixture.AddBlock();
|
||||||
|
if (lane) choose(previous, guarded, exit, lanes, CFG::BranchCondition::ExecNonZero);
|
||||||
|
previous = guarded;
|
||||||
|
if (final_counter.IsEmpty())
|
||||||
|
final_counter = fixture.Emit(ValueOpcode::LoadSharedU32,
|
||||||
|
{Value(0u), variant == Variant::LaneUnimpliedCounter ? other : lanes}, shared(0u), previous);
|
||||||
|
if (lane)
|
||||||
|
final_counter = fixture.Emit(ValueOpcode::ReadFirstLane,
|
||||||
|
{final_counter, variant == Variant::LaneCounterMask ? other : lanes}, 0, previous);
|
||||||
branch(previous, header);
|
branch(previous, header);
|
||||||
auto &index = header->AppendNewInst(ValueOpcode::Phi);
|
auto &index = header->AppendNewInst(ValueOpcode::Phi);
|
||||||
index.SetFlags(Type::U32);
|
index.SetFlags(Type::U32);
|
||||||
@@ -4944,12 +5019,22 @@ void TestSharedAppendSelectorBounds() {
|
|||||||
choose(header, body, exit, in_list, CFG::BranchCondition::VccNonZero);
|
choose(header, body, exit, in_list, CFG::BranchCondition::VccNonZero);
|
||||||
fixture.block = body;
|
fixture.block = body;
|
||||||
const auto selected = fixture.Emit(ValueOpcode::LoadSharedU32,
|
const auto selected = fixture.Emit(ValueOpcode::LoadSharedU32,
|
||||||
{fixture.Emit(ValueOpcode::ShiftLeftLogical32, {Value(&index), Value(2u)}), Value(true)},
|
{fixture.Emit(ValueOpcode::ShiftLeftLogical32, {Value(&index), Value(2u)}),
|
||||||
shared(consumed_base));
|
variant == Variant::LaneUnimpliedRead ? other : lanes}, shared(consumed_base));
|
||||||
const auto offset32 = fixture.Emit(ValueOpcode::ShiftLeftLogical32, {selected, Value(5u)});
|
Value offset;
|
||||||
const auto offset = fixture.Emit(ValueOpcode::ReadFirstLane,
|
if (lane) {
|
||||||
{fixture.Emit(ValueOpcode::IAdd32,
|
// The record is selected from one lane and scaled on the scalar side.
|
||||||
{fixture.Emit(ValueOpcode::ShiftLeftLogical32, {offset32, Value(2u)}), offset32}), Value(true)});
|
const auto mask = variant == Variant::LaneInactiveSelect
|
||||||
|
? fixture.Emit(ValueOpcode::LogicalAnd, {lanes, other}) : lanes;
|
||||||
|
offset = fixture.Emit(ValueOpcode::IMul32,
|
||||||
|
{fixture.Emit(ValueOpcode::ReadFirstLane, {selected, mask}), Value(160u)});
|
||||||
|
} else {
|
||||||
|
const auto offset32 = fixture.Emit(ValueOpcode::ShiftLeftLogical32, {selected, Value(5u)});
|
||||||
|
offset = fixture.Emit(ValueOpcode::ReadFirstLane,
|
||||||
|
{fixture.Emit(ValueOpcode::IAdd32,
|
||||||
|
{fixture.Emit(ValueOpcode::ShiftLeftLogical32, {offset32, Value(2u)}), offset32}),
|
||||||
|
Value(true)});
|
||||||
|
}
|
||||||
MemoryInfo scalar;
|
MemoryInfo scalar;
|
||||||
scalar.kind = ResourceKind::ScalarBuffer;
|
scalar.kind = ResourceKind::ScalarBuffer;
|
||||||
scalar.offset = 144u;
|
scalar.offset = 144u;
|
||||||
@@ -4978,7 +5063,8 @@ void TestSharedAppendSelectorBounds() {
|
|||||||
fixture.PlanAndTrack({.compute = &compute});
|
fixture.PlanAndTrack({.compute = &compute});
|
||||||
const auto &indirect = fixture.program.descriptor_sources[
|
const auto &indirect = fixture.program.descriptor_sources[
|
||||||
fixture.program.info.images[0].source].indirect_descriptor;
|
fixture.program.info.images[0].source].indirect_descriptor;
|
||||||
const bool proven = variant == Variant::Valid || variant == Variant::PayloadOob;
|
const bool proven = variant == Variant::Valid || variant == Variant::PayloadOob ||
|
||||||
|
variant == Variant::Lane;
|
||||||
Check(indirect && indirect->selector && indirect->selector->offset == 144u &&
|
Check(indirect && indirect->selector && indirect->selector->offset == 144u &&
|
||||||
indirect->selector->stride == (proven ? 160u : 32u) &&
|
indirect->selector->stride == (proven ? 160u : 32u) &&
|
||||||
indirect->key_count.IsEmpty() != proven &&
|
indirect->key_count.IsEmpty() != proven &&
|
||||||
@@ -4995,6 +5081,11 @@ void TestSharedAppendSelectorBounds() {
|
|||||||
Variant::InnerAppendCycle, Variant::EarlyCounterRead, Variant::WrongConsumerBound, Variant::ExtraWriter,
|
Variant::InnerAppendCycle, Variant::EarlyCounterRead, Variant::WrongConsumerBound, Variant::ExtraWriter,
|
||||||
Variant::CounterAlias, Variant::PayloadWrap, Variant::CounterOob})
|
Variant::CounterAlias, Variant::PayloadWrap, Variant::CounterOob})
|
||||||
make_plan(variant);
|
make_plan(variant);
|
||||||
|
auto lane_plan = make_plan(Variant::Lane);
|
||||||
|
for (const auto variant : {Variant::LaneUnimpliedRead, Variant::LaneUnimpliedCounter,
|
||||||
|
Variant::LaneCounterMask, Variant::LaneOtherBypass, Variant::LaneInactiveSelect})
|
||||||
|
CheckFatal([&] { make_plan(variant); }, "not a valid runtime value",
|
||||||
|
"shared append provenance accepted an unimplied lane mask or initializer bypass");
|
||||||
|
|
||||||
LinearTestMemory memory;
|
LinearTestMemory memory;
|
||||||
for (uint32_t index = 0; index < 8u; ++index) memory.words[(144u + 160u * index) / 4u] = index + 1u;
|
for (uint32_t index = 0; index < 8u; ++index) memory.words[(144u + 160u * index) / 4u] = index + 1u;
|
||||||
@@ -5007,7 +5098,7 @@ void TestSharedAppendSelectorBounds() {
|
|||||||
memory.words[word + 3u] = Libs::Graphics::DstSel(4, 5, 6, 7) |
|
memory.words[word + 3u] = Libs::Graphics::DstSel(4, 5, 6, 7) |
|
||||||
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
|
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
|
||||||
}
|
}
|
||||||
std::array<uint32_t, 9> user_data {0x1000u, 160u << 16u, 8u, 0u, 0x2000u, 0u, 0x1800u, 0u, 0u};
|
std::array<uint32_t, 9> user_data {0x1000u, 160u << 16u, 8u, 0u, 0x2000u, 0u, 0x1800u, 0u, 8u};
|
||||||
SrtRuntime runtime {.user_data = user_data, .userdata = &memory,
|
SrtRuntime runtime {.user_data = user_data, .userdata = &memory,
|
||||||
.read_specialization_memory = ReadLinearTestMemory};
|
.read_specialization_memory = ReadLinearTestMemory};
|
||||||
FakeGuestMemory fake(ReadRuntimeMemory, &runtime);
|
FakeGuestMemory fake(ReadRuntimeMemory, &runtime);
|
||||||
@@ -5022,10 +5113,11 @@ void TestSharedAppendSelectorBounds() {
|
|||||||
memory.words[0x800u / 4u] = counts[0];
|
memory.words[0x800u / 4u] = counts[0];
|
||||||
memory.words[0x804u / 4u] = counts[1];
|
memory.words[0x804u / 4u] = counts[1];
|
||||||
const auto expected = counts[1];
|
const auto expected = counts[1];
|
||||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
for (const auto *tested : {&plan, &lane_plan})
|
||||||
snapshot.images.size() == std::max(expected, 1u) &&
|
Check(MaterializeResources(*tested, runtime, snapshot, specialization) &&
|
||||||
(expected == 0u || snapshot.images.back().dwords[0] == 0x100u + expected),
|
snapshot.images.size() == std::max(expected, 1u) &&
|
||||||
"disjoint shared append lists or refreshed counts selected unrelated material keys");
|
(expected == 0u || snapshot.images.back().dwords[0] == 0x100u + expected),
|
||||||
|
"disjoint shared append lists or refreshed counts selected unrelated material keys");
|
||||||
}
|
}
|
||||||
memory.words[0x800u / 4u] = 24u;
|
memory.words[0x800u / 4u] = 24u;
|
||||||
memory.words[0x804u / 4u] = 2u;
|
memory.words[0x804u / 4u] = 2u;
|
||||||
@@ -5047,9 +5139,14 @@ void TestSharedAppendSelectorBounds() {
|
|||||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||||
snapshot.images.size() == 2u && snapshot.images.back().dwords[0] == 0x104u,
|
snapshot.images.size() == 2u && snapshot.images.back().dwords[0] == 0x104u,
|
||||||
"shared append materialization reused stale selector contents");
|
"shared append materialization reused stale selector contents");
|
||||||
|
// Record 1 lies past a one-record V#, so its scalar key reads zero instead of key 4.
|
||||||
user_data[2] = 1u;
|
user_data[2] = 1u;
|
||||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
memory.fail_address = 0x1000u + 144u + 160u;
|
||||||
"shared append materialization accepted a range outside its V# bounds");
|
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||||
|
snapshot.images.size() == 2u && snapshot.images[0].dwords[0] == 0x100u &&
|
||||||
|
snapshot.images[1].dwords[0] == 0x101u,
|
||||||
|
"shared append materialization did not read records past its V# as zero");
|
||||||
|
memory.fail_address = UINT64_MAX;
|
||||||
user_data[2] = 8u;
|
user_data[2] = 8u;
|
||||||
for (const auto too_large : {65537u, UINT32_MAX}) {
|
for (const auto too_large : {65537u, UINT32_MAX}) {
|
||||||
for (uint32_t list = 0; list < 2u; ++list) {
|
for (uint32_t list = 0; list < 2u; ++list) {
|
||||||
|
|||||||
@@ -660,6 +660,47 @@ void TestClassicSaveParams() {
|
|||||||
CHECK(params[0].mtime == nested_mtime);
|
CHECK(params[0].mtime == nested_mtime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestSearchSortKeys() {
|
||||||
|
Reset("SORTED");
|
||||||
|
const std::array names {DirName("alpha"), DirName("bravo"), DirName("charlie")};
|
||||||
|
constexpr std::array<uint64_t, 3> blocks {96, 48, 48};
|
||||||
|
constexpr std::array<uint32_t, 3> user_params {2, 3, 1};
|
||||||
|
constexpr std::array<int, 3> hours_ahead {1, 3, 2};
|
||||||
|
for (size_t i = 0; i < names.size(); i++) {
|
||||||
|
struct SaveDataMount3 mount {};
|
||||||
|
mount.user_id = 1;
|
||||||
|
mount.dir_name = &names[i];
|
||||||
|
mount.mount_mode = 4;
|
||||||
|
mount.blocks = blocks[i];
|
||||||
|
SaveDataMountResult result {};
|
||||||
|
CHECK(SaveDataMount3(&mount, &result) == OK);
|
||||||
|
CHECK(SaveDataSetParam(&result.mount_point, 4, &user_params[i], sizeof(user_params[i])) ==
|
||||||
|
OK);
|
||||||
|
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
|
||||||
|
fs::last_write_time(fs::path("_SaveData/SORTED") / names[i].data / "sce_sys/param.bin",
|
||||||
|
fs::file_time_type::clock::now() + std::chrono::hours(hours_ahead[i]));
|
||||||
|
}
|
||||||
|
const auto search = [](SaveDataSortKey key, SaveDataSortOrder order) {
|
||||||
|
std::array<SceSaveDataDirName, 3> found {};
|
||||||
|
SaveDataDirNameSearchCond cond {};
|
||||||
|
cond.user_id = 1;
|
||||||
|
cond.key = key;
|
||||||
|
cond.order = order;
|
||||||
|
SaveDataDirNameSearchResult result {};
|
||||||
|
result.dir_names = found.data();
|
||||||
|
result.dir_names_num = found.size();
|
||||||
|
CHECK(SaveDataDirNameSearch(&cond, &result) == OK && result.set_num == 3);
|
||||||
|
return std::string(found[0].data) + " " + found[1].data + " " + found[2].data;
|
||||||
|
};
|
||||||
|
CHECK(search(SaveDataSortKey::DirName, SaveDataSortOrder::Descent) == "charlie bravo alpha");
|
||||||
|
CHECK(search(SaveDataSortKey::UserParam, SaveDataSortOrder::Ascent) == "charlie alpha bravo");
|
||||||
|
CHECK(search(SaveDataSortKey::Blocks, SaveDataSortOrder::Ascent) == "bravo charlie alpha");
|
||||||
|
CHECK(search(SaveDataSortKey::FreeBlocks, SaveDataSortOrder::Descent) == "alpha charlie bravo");
|
||||||
|
CHECK(search(SaveDataSortKey::Mtime, SaveDataSortOrder::Descent) == "bravo charlie alpha");
|
||||||
|
CHECK(fs::remove("_SaveData/SORTED/alpha/sce_sys/blocks.bin"));
|
||||||
|
CHECK(search(SaveDataSortKey::Blocks, SaveDataSortOrder::Ascent) == "alpha bravo charlie");
|
||||||
|
}
|
||||||
|
|
||||||
void RunChild(const fs::path& executable, const char* mode) {
|
void RunChild(const fs::path& executable, const char* mode) {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
CHECK(_spawnl(_P_WAIT, executable.string().c_str(), executable.string().c_str(), mode,
|
CHECK(_spawnl(_P_WAIT, executable.string().c_str(), executable.string().c_str(), mode,
|
||||||
@@ -706,6 +747,7 @@ int main(int argc, char** argv) {
|
|||||||
TestGuestSaveIsolation();
|
TestGuestSaveIsolation();
|
||||||
TestSaveAllocations();
|
TestSaveAllocations();
|
||||||
TestClassicSaveParams();
|
TestClassicSaveParams();
|
||||||
|
TestSearchSortKeys();
|
||||||
CHECK(SaveDataTerminate() == OK);
|
CHECK(SaveDataTerminate() == OK);
|
||||||
fs::current_path(previous);
|
fs::current_path(previous);
|
||||||
fs::remove_all(temp);
|
fs::remove_all(temp);
|
||||||
|
|||||||
@@ -300,8 +300,12 @@ void TestOperandMutationLifecycle() {
|
|||||||
auto *source = definition.TryInstruction();
|
auto *source = definition.TryInstruction();
|
||||||
const auto inserted = fixture.Emit(ValueOpcode::BitFieldInsert,
|
const auto inserted = fixture.Emit(ValueOpcode::BitFieldInsert,
|
||||||
{definition, definition, Value(4u), Value(4u)});
|
{definition, definition, Value(4u), Value(4u)});
|
||||||
Check(source->UseCount() == 2, "inline operands lost distinct reverse-use indices");
|
auto *consumer = inserted.TryInstruction();
|
||||||
inserted.TryInstruction()->SetArg(1, Value(3u));
|
Check(source->Uses() == std::vector<Use>({{consumer, 0}, {consumer, 1}}),
|
||||||
|
"inline operands lost distinct reverse-use indices");
|
||||||
|
consumer->SetArg(0, definition);
|
||||||
|
Check(source->UseCount() == 2, "re-setting an operand to its value duplicated its use");
|
||||||
|
consumer->SetArg(1, Value(3u));
|
||||||
Check(source->UseCount() == 1, "inline operand mutation did not detach its old use");
|
Check(source->UseCount() == 1, "inline operand mutation did not detach its old use");
|
||||||
source->ReplaceUsesWith(Value(0xaau));
|
source->ReplaceUsesWith(Value(0xaau));
|
||||||
uint32_t result = 0;
|
uint32_t result = 0;
|
||||||
@@ -349,6 +353,35 @@ void TestOperandMutationLifecycle() {
|
|||||||
"Phi replacement retained incoming uses or changed the consumer value");
|
"Phi replacement retained incoming uses or changed the consumer value");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestReplacementKeepsUseOrder() {
|
||||||
|
Fixture fixture(2);
|
||||||
|
const auto base = fixture.Emit(ValueOpcode::IAdd32, {Value(1u), Value(2u)});
|
||||||
|
auto *before = fixture.Emit(ValueOpcode::IAdd32, {base, Value(3u)}).TryInstruction();
|
||||||
|
auto *identity = fixture.Emit(ValueOpcode::Identity, {base}).TryInstruction();
|
||||||
|
auto *pair = fixture.Emit(ValueOpcode::IAdd32, {Value(identity), Value(identity)}).TryInstruction();
|
||||||
|
auto *after = fixture.Emit(ValueOpcode::IAdd32, {Value(4u), base}).TryInstruction();
|
||||||
|
auto &large = fixture.program.value_storage.emplace_back(ValueOpcode::GetImageResource);
|
||||||
|
const auto last = large.NumArgs() - 1;
|
||||||
|
large.SetArg(last, Value(identity));
|
||||||
|
identity->ReplaceUsesWith(base, false);
|
||||||
|
Check(!identity->HasUses() && identity->NumArgs() == 0 && pair->Arg(0) == base &&
|
||||||
|
pair->Arg(1) == base && large.Arg(last) == base &&
|
||||||
|
base.TryInstruction()->Uses() ==
|
||||||
|
std::vector<Use>({{before, 0}, {after, 1}, {pair, 0}, {pair, 1}, {&large, last}}),
|
||||||
|
"identity removal lost or reordered the uses of its argument");
|
||||||
|
|
||||||
|
auto &phi = fixture.BlockAt(1).AppendNewInst(ValueOpcode::Phi);
|
||||||
|
phi.SetFlags(Type::U32);
|
||||||
|
phi.AddPhiOperand(&fixture.BlockAt(0), Value(after));
|
||||||
|
phi.AddPhiOperand(&fixture.BlockAt(1), Value(&phi));
|
||||||
|
auto *user = fixture.Emit(ValueOpcode::IAdd32, {Value(&phi), Value(&phi)}, 0, 1).TryInstruction();
|
||||||
|
phi.ReplaceUsesWith(Value(after));
|
||||||
|
Check(phi.GetOpcode() == ValueOpcode::Identity && phi.Arg(0) == Value(after) && !phi.HasUses() &&
|
||||||
|
user->Arg(0) == Value(after) && user->Arg(1) == Value(after) &&
|
||||||
|
after->Uses() == std::vector<Use>({{user, 0}, {user, 1}, {&phi, 0}}),
|
||||||
|
"self-referencing Phi replacement kept a use of itself or reordered the moved uses");
|
||||||
|
}
|
||||||
|
|
||||||
void TestControlDependentStandaloneLoadStaysTyped() {
|
void TestControlDependentStandaloneLoadStaysTyped() {
|
||||||
Fixture fixture(3);
|
Fixture fixture(3);
|
||||||
const auto memory = fixture.AddMemory(ResourceKind::ScalarAddress);
|
const auto memory = fixture.AddMemory(ResourceKind::ScalarAddress);
|
||||||
@@ -515,6 +548,34 @@ void TestFloatComparisonDescriptorInputs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestSignedConversionDescriptorInputs() {
|
||||||
|
// 3.75 and -3.75 truncate, the largest float below 2^31 converts exactly, 2^31 and +inf
|
||||||
|
// saturate up, -2^31 and -1e10 saturate down, and -0.0, a denormal and both NaNs read zero.
|
||||||
|
constexpr std::array<std::pair<uint32_t, uint32_t>, 11> inputs = {{
|
||||||
|
{0x40700000u, 3u}, {0xc0700000u, 0xfffffffdu}, {0x4effffffu, 0x7fffff80u},
|
||||||
|
{0x4f000000u, 0x7fffffffu}, {0x7f800000u, 0x7fffffffu}, {0xcf000000u, 0x80000000u},
|
||||||
|
{0xd01502f9u, 0x80000000u}, {0x80000000u, 0u}, {0x00000001u, 0u},
|
||||||
|
{0x7fc00000u, 0u}, {0xffc00000u, 0u}}};
|
||||||
|
Fixture fixture;
|
||||||
|
const auto user = fixture.Emit(
|
||||||
|
ValueOpcode::GetUserData, {Value(static_cast<ScalarReg>(2))});
|
||||||
|
const auto converted = fixture.Emit(
|
||||||
|
ValueOpcode::ConvertS32F32, {fixture.Emit(ValueOpcode::BitCastF32U32, {user})});
|
||||||
|
Check(ValidateRuntimeValue(fixture.program, converted),
|
||||||
|
"signed float conversion was rejected as a runtime value");
|
||||||
|
fixture.program.descriptor_sources.push_back(
|
||||||
|
{.dwords = {converted}, .dword_count = 1});
|
||||||
|
|
||||||
|
auto plan = ExtractResourcePlan(fixture.program);
|
||||||
|
for (const auto [bits, expected] : inputs) {
|
||||||
|
const std::array user_data{bits};
|
||||||
|
DescriptorValue result;
|
||||||
|
Check(EvaluateSource(plan, {.user_data = user_data}, 0, result) &&
|
||||||
|
result.dwords[0] == expected,
|
||||||
|
"descriptor signed conversion disagrees with the native conversion");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TestSharedIntegerRuntimeDependencies() {
|
void TestSharedIntegerRuntimeDependencies() {
|
||||||
Fixture fixture;
|
Fixture fixture;
|
||||||
const auto lane = fixture.Emit(ValueOpcode::LaneId);
|
const auto lane = fixture.Emit(ValueOpcode::LaneId);
|
||||||
@@ -884,10 +945,12 @@ int main() {
|
|||||||
TestCarryAndBitFields();
|
TestCarryAndBitFields();
|
||||||
TestInvariantAndDivergentPhi();
|
TestInvariantAndDivergentPhi();
|
||||||
TestOperandMutationLifecycle();
|
TestOperandMutationLifecycle();
|
||||||
|
TestReplacementKeepsUseOrder();
|
||||||
TestControlDependentStandaloneLoadStaysTyped();
|
TestControlDependentStandaloneLoadStaysTyped();
|
||||||
TestRuntime64BitDescriptorOps();
|
TestRuntime64BitDescriptorOps();
|
||||||
TestUniformFirstLaneSamplerLod();
|
TestUniformFirstLaneSamplerLod();
|
||||||
TestFloatComparisonDescriptorInputs();
|
TestFloatComparisonDescriptorInputs();
|
||||||
|
TestSignedConversionDescriptorInputs();
|
||||||
TestSharedIntegerRuntimeDependencies();
|
TestSharedIntegerRuntimeDependencies();
|
||||||
TestConstantBufferBounds();
|
TestConstantBufferBounds();
|
||||||
TestReadLaneElimination();
|
TestReadLaneElimination();
|
||||||
|
|||||||
+1484
-188
File diff suppressed because it is too large.
Load diff
+31
-11
@@ -6,12 +6,16 @@
|
|||||||
#include <xbyak/xbyak.h>
|
#include <xbyak/xbyak.h>
|
||||||
|
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
|
#include <cstddef>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <iterator>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#if defined(__linux__)
|
#if defined(__linux__)
|
||||||
#include <ucontext.h>
|
#include <ucontext.h>
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
#include <sys/ucontext.h>
|
||||||
#elif defined(_WIN32)
|
#elif defined(_WIN32)
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -23,18 +27,20 @@ public:
|
|||||||
explicit FakeGuestMemory(Libs::Graphics::ShaderRecompiler::IR::SrtMemoryReader reader = nullptr,
|
explicit FakeGuestMemory(Libs::Graphics::ShaderRecompiler::IR::SrtMemoryReader reader = nullptr,
|
||||||
void *userdata = nullptr) {
|
void *userdata = nullptr) {
|
||||||
Current() = {reader, userdata};
|
Current() = {reader, userdata};
|
||||||
#if defined(__linux__)
|
#if defined(__linux__) || defined(__APPLE__)
|
||||||
struct sigaction action {};
|
struct sigaction action {};
|
||||||
action.sa_sigaction = Handle;
|
action.sa_sigaction = Handle;
|
||||||
action.sa_flags = SA_SIGINFO;
|
action.sa_flags = SA_SIGINFO;
|
||||||
sigaction(SIGSEGV, &action, &m_previous);
|
for (size_t index = 0; index < std::size(kSignals); ++index)
|
||||||
|
sigaction(kSignals[index], &action, &m_previous[index]);
|
||||||
#elif defined(_WIN32)
|
#elif defined(_WIN32)
|
||||||
m_handler = AddVectoredExceptionHandler(1, Handle);
|
m_handler = AddVectoredExceptionHandler(1, Handle);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
~FakeGuestMemory() {
|
~FakeGuestMemory() {
|
||||||
#if defined(__linux__)
|
#if defined(__linux__) || defined(__APPLE__)
|
||||||
sigaction(SIGSEGV, &m_previous, nullptr);
|
for (size_t index = 0; index < std::size(kSignals); ++index)
|
||||||
|
sigaction(kSignals[index], &m_previous[index], nullptr);
|
||||||
#elif defined(_WIN32)
|
#elif defined(_WIN32)
|
||||||
RemoveVectoredExceptionHandler(m_handler);
|
RemoveVectoredExceptionHandler(m_handler);
|
||||||
#endif
|
#endif
|
||||||
@@ -54,16 +60,30 @@ private:
|
|||||||
const auto [reader, userdata] = Current();
|
const auto [reader, userdata] = Current();
|
||||||
return reader != nullptr && reader(userdata, address, {&word, 1}) ? word : 0u;
|
return reader != nullptr && reader(userdata, address, {&word, 1}) ? word : 0u;
|
||||||
}
|
}
|
||||||
#if defined(__linux__)
|
#if defined(__linux__) || defined(__APPLE__)
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
// macOS raises SIGBUS for some protection faults and SIGSEGV for others.
|
||||||
|
static constexpr int kSignals[] = {SIGSEGV, SIGBUS};
|
||||||
|
#else
|
||||||
|
static constexpr int kSignals[] = {SIGSEGV};
|
||||||
|
#endif
|
||||||
static void Handle(int, siginfo_t *, void *context) {
|
static void Handle(int, siginfo_t *, void *context) {
|
||||||
auto ®isters = static_cast<ucontext_t *>(context)->uc_mcontext.gregs;
|
auto *native = static_cast<ucontext_t *>(context);
|
||||||
const auto length =
|
#if defined(__APPLE__)
|
||||||
Libs::Graphics::ShaderRecompiler::IR::SrtWalkerLoadLength(registers[REG_RIP]);
|
auto &rip = native->uc_mcontext->__ss.__rip;
|
||||||
|
auto &rax = native->uc_mcontext->__ss.__rax;
|
||||||
|
const auto r10 = native->uc_mcontext->__ss.__r10;
|
||||||
|
#else
|
||||||
|
auto &rip = native->uc_mcontext.gregs[REG_RIP];
|
||||||
|
auto &rax = native->uc_mcontext.gregs[REG_RAX];
|
||||||
|
const auto r10 = native->uc_mcontext.gregs[REG_R10];
|
||||||
|
#endif
|
||||||
|
const auto length = Libs::Graphics::ShaderRecompiler::IR::SrtWalkerLoadLength(rip);
|
||||||
if (length == 0) std::abort();
|
if (length == 0) std::abort();
|
||||||
registers[REG_RAX] = Read(registers[REG_R10]);
|
rax = Read(r10);
|
||||||
registers[REG_RIP] += length;
|
rip += length;
|
||||||
}
|
}
|
||||||
struct sigaction m_previous {};
|
struct sigaction m_previous[std::size(kSignals)] {};
|
||||||
#elif defined(_WIN32)
|
#elif defined(_WIN32)
|
||||||
static LONG CALLBACK Handle(EXCEPTION_POINTERS *info) {
|
static LONG CALLBACK Handle(EXCEPTION_POINTERS *info) {
|
||||||
auto &context = *info->ContextRecord;
|
auto &context = *info->ContextRecord;
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
+314
-13
@@ -1938,7 +1938,7 @@ void TestVertexBufferGrouping() {
|
|||||||
Check(input.fetch_attrib_reg == 2, "a draw reread the vertex metadata of a mapped shader");
|
Check(input.fetch_attrib_reg == 2, "a draw reread the vertex metadata of a mapped shader");
|
||||||
ShaderMapUserData(regs.es_regs.data_addr, mapped);
|
ShaderMapUserData(regs.es_regs.data_addr, mapped);
|
||||||
(void)PrepareProgram(regs, HW::Context{}, HW::UserConfig{}, input);
|
(void)PrepareProgram(regs, HW::Context{}, HW::UserConfig{}, input);
|
||||||
Check(!input.fetch_embedded, "remapping a shader kept its previous vertex metadata");
|
Check(!input.has_dynamic_vertex_fetch, "remapping a shader kept its previous vertex metadata");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestNggVertexEntryState() {
|
void TestNggVertexEntryState() {
|
||||||
@@ -2001,18 +2001,21 @@ void TestNggVertexEntryState() {
|
|||||||
|
|
||||||
void TestNativeDataCacheInvalidation() {
|
void TestNativeDataCacheInvalidation() {
|
||||||
using namespace ShaderRecompiler;
|
using namespace ShaderRecompiler;
|
||||||
// Captured f5edf70dbb1f6057:0x194 and 0x1a4, each followed by S_WAITCNT.
|
// Captured f5edf70dbb1f6057:0x194 and 0x1a4, each followed by S_WAITCNT. The S_DCACHE_INV
|
||||||
|
// after them emits no fence.
|
||||||
const std::array cache_ops = {0xe1c40000u, 0u, 0xbf8c0000u,
|
const std::array cache_ops = {0xe1c40000u, 0u, 0xbf8c0000u,
|
||||||
0xe1c80000u, 0u, 0xbf8c0000u, EncodeSopp(0x01)};
|
0xe1c80000u, 0u, 0xbf8c0000u,
|
||||||
|
0xf4800000u, 0xfa000000u, EncodeSopp(0x01)};
|
||||||
Decoder::Program decoded;
|
Decoder::Program decoded;
|
||||||
Decoder::DecodeProgram(cache_ops, decoded);
|
Decoder::DecodeProgram(cache_ops, decoded);
|
||||||
for (const uint32_t index : {0u, 2u}) {
|
for (const auto [index, opcode] : {std::pair{0u, Decoder::Opcode::BUFFER_GL0_INV},
|
||||||
|
std::pair{2u, Decoder::Opcode::BUFFER_GL1_INV},
|
||||||
|
std::pair{4u, Decoder::Opcode::S_DCACHE_INV}}) {
|
||||||
const auto &inst = decoded.instructions[index];
|
const auto &inst = decoded.instructions[index];
|
||||||
Check(inst.opcode == (index == 0 ? Decoder::Opcode::BUFFER_GL0_INV
|
Check(inst.opcode == opcode &&
|
||||||
: Decoder::Opcode::BUFFER_GL1_INV) &&
|
|
||||||
inst.word_count == 2 && inst.src_count == 0 && inst.data_dwords == 0 &&
|
inst.word_count == 2 && inst.src_count == 0 && inst.data_dwords == 0 &&
|
||||||
inst.dst.kind == Decoder::OperandKind::Unknown,
|
inst.dst.kind == Decoder::OperandKind::Unknown,
|
||||||
"RDNA2 cache invalidation acquired a buffer operand or lost its encoding");
|
"RDNA2 cache invalidation acquired an operand or lost its encoding");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto options = MakeCompileOptions(ShaderType::Compute);
|
auto options = MakeCompileOptions(ShaderType::Compute);
|
||||||
@@ -4715,6 +4718,73 @@ void TestImageAtomicWidthDecoder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestImageGatherDecoder() {
|
||||||
|
using namespace ShaderRecompiler::Decoder;
|
||||||
|
struct Gather {
|
||||||
|
uint32_t opcode;
|
||||||
|
const char *name;
|
||||||
|
uint32_t address_vgprs;
|
||||||
|
};
|
||||||
|
// Every PS5 image_gather4 encoding, with the vaddr VGPR count of its 2D full-float form.
|
||||||
|
constexpr Gather gathers[] = {
|
||||||
|
{0x40, "image_gather4", 2}, {0x41, "image_gather4_cl", 3},
|
||||||
|
{0x44, "image_gather4_l", 3}, {0x45, "image_gather4_b", 3},
|
||||||
|
{0x46, "image_gather4_b_cl", 4}, {0x47, "image_gather4_lz", 2},
|
||||||
|
{0x48, "image_gather4_c", 3}, {0x49, "image_gather4_c_cl", 4},
|
||||||
|
{0x4c, "image_gather4_c_l", 4}, {0x4d, "image_gather4_c_b", 4},
|
||||||
|
{0x4e, "image_gather4_c_b_cl", 5}, {0x4f, "image_gather4_c_lz", 3},
|
||||||
|
{0x50, "image_gather4_o", 3}, {0x51, "image_gather4_cl_o", 4},
|
||||||
|
{0x54, "image_gather4_l_o", 4}, {0x55, "image_gather4_b_o", 4},
|
||||||
|
{0x56, "image_gather4_b_cl_o", 5}, {0x57, "image_gather4_lz_o", 3},
|
||||||
|
{0x58, "image_gather4_c_o", 4}, {0x59, "image_gather4_c_cl_o", 5},
|
||||||
|
{0x5c, "image_gather4_c_l_o", 5}, {0x5d, "image_gather4_c_b_o", 5},
|
||||||
|
{0x5e, "image_gather4_c_b_cl_o", 6}, {0x5f, "image_gather4_c_lz_o", 4},
|
||||||
|
{0x61, "image_gather4h", 2}, {0xc0, "image_gather4_a", 2},
|
||||||
|
{0xc1, "image_gather4_cl_a", 3}, {0xc5, "image_gather4_b_a", 3},
|
||||||
|
{0xc6, "image_gather4_b_cl_a", 4}, {0xc8, "image_gather4_c_a", 3},
|
||||||
|
{0xc9, "image_gather4_c_cl_a", 4}, {0xcd, "image_gather4_c_b_a", 4},
|
||||||
|
{0xce, "image_gather4_c_b_cl_a", 5}, {0xd0, "image_gather4_o_a", 3},
|
||||||
|
{0xd1, "image_gather4_cl_o_a", 4}, {0xd5, "image_gather4_b_o_a", 4},
|
||||||
|
{0xd6, "image_gather4_b_cl_o_a", 5}, {0xd8, "image_gather4_c_o_a", 4},
|
||||||
|
{0xd9, "image_gather4_c_cl_o_a", 5}, {0xdd, "image_gather4_c_b_o_a", 5},
|
||||||
|
{0xde, "image_gather4_c_b_cl_o_a", 6},
|
||||||
|
};
|
||||||
|
const auto expected_flags = [](std::string_view name) {
|
||||||
|
if (name == "image_gather4h") return uint32_t{ImageSampleFlagGatherHorizontal};
|
||||||
|
uint32_t flags = 0;
|
||||||
|
for (size_t at = name.find('_', 6); at != std::string_view::npos;) {
|
||||||
|
const auto end = name.find('_', at + 1);
|
||||||
|
const auto modifier = name.substr(at + 1, end - at - 1);
|
||||||
|
if (modifier == "c") flags |= ImageSampleFlagCompare;
|
||||||
|
if (modifier == "b") flags |= ImageSampleFlagBias;
|
||||||
|
if (modifier == "cl") flags |= ImageSampleFlagLodClamp;
|
||||||
|
if (modifier == "l") flags |= ImageSampleFlagLod;
|
||||||
|
if (modifier == "lz") flags |= ImageSampleFlagLevelZero;
|
||||||
|
if (modifier == "o") flags |= ImageSampleFlagOffset;
|
||||||
|
if (modifier == "a") flags |= ImageSampleFlagAdjust;
|
||||||
|
at = end;
|
||||||
|
}
|
||||||
|
return flags;
|
||||||
|
};
|
||||||
|
for (uint32_t opcode = 0; opcode < 0x100u; ++opcode) {
|
||||||
|
const auto *gather = std::ranges::find(gathers, opcode, &Gather::opcode);
|
||||||
|
const uint32_t words[] = {EncodeMimg0(opcode, 0x1), EncodeMimg1(8, 0, 1, 0)};
|
||||||
|
Instruction decoded;
|
||||||
|
DecodeInstruction(words, 0, decoded);
|
||||||
|
if (gather == std::end(gathers)) {
|
||||||
|
Check(decoded.opcode != Opcode::IMAGE_GATHER4,
|
||||||
|
"a non-gather MIMG encoding decoded as image_gather4");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Check(decoded.opcode == Opcode::IMAGE_GATHER4 && decoded.data_components == 4u &&
|
||||||
|
decoded.image_sample_flags == expected_flags(gather->name) &&
|
||||||
|
decoded.image_address_components == gather->address_vgprs &&
|
||||||
|
InstructionToString(decoded).find(std::string(gather->name) + " ") !=
|
||||||
|
std::string::npos,
|
||||||
|
"a PS5 gather encoding lost its opcode, name, modifiers or address count");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TestNewShaderDecoderArchitecture() {
|
void TestNewShaderDecoderArchitecture() {
|
||||||
using namespace ShaderRecompiler::Decoder;
|
using namespace ShaderRecompiler::Decoder;
|
||||||
|
|
||||||
@@ -5657,7 +5727,7 @@ void TestImageAddressOperands() {
|
|||||||
const uint32_t captured[] = {0xf130010au, 0x00a30013u, 0x00161a19u};
|
const uint32_t captured[] = {0xf130010au, 0x00a30013u, 0x00161a19u};
|
||||||
Decoder::Instruction gather;
|
Decoder::Instruction gather;
|
||||||
Decoder::DecodeInstruction(captured, 0, gather);
|
Decoder::DecodeInstruction(captured, 0, gather);
|
||||||
Check(gather.opcode == Decoder::Opcode::IMAGE_GATHER4_C_L &&
|
Check(gather.opcode == Decoder::Opcode::IMAGE_GATHER4 &&
|
||||||
gather.opcode_id == 0x4cu && gather.word_count == 3u &&
|
gather.opcode_id == 0x4cu && gather.word_count == 3u &&
|
||||||
gather.image_dimension == Decoder::ImageDimension::Dim2D &&
|
gather.image_dimension == Decoder::ImageDimension::Dim2D &&
|
||||||
gather.dmask == 1u && gather.data_components == 4u &&
|
gather.dmask == 1u && gather.data_components == 4u &&
|
||||||
@@ -5671,7 +5741,7 @@ void TestImageAddressOperands() {
|
|||||||
gather.image_nsa_addr[2] == 22u && !gather.glc && !gather.slc &&
|
gather.image_nsa_addr[2] == 22u && !gather.glc && !gather.slc &&
|
||||||
!gather.image_r128,
|
!gather.image_r128,
|
||||||
"captured IMAGE_GATHER4_C_L lost compare/LOD, NSA, or result metadata");
|
"captured IMAGE_GATHER4_C_L lost compare/LOD, NSA, or result metadata");
|
||||||
Check(Decoder::InstructionToString(gather).find("IMAGE_GATHER4_C_L") != std::string::npos,
|
Check(Decoder::InstructionToString(gather).find("image_gather4_c_l") != std::string::npos,
|
||||||
"captured IMAGE_GATHER4_C_L is missing from the decoded dump");
|
"captured IMAGE_GATHER4_C_L is missing from the decoded dump");
|
||||||
struct Case {
|
struct Case {
|
||||||
uint32_t opcode;
|
uint32_t opcode;
|
||||||
@@ -5687,6 +5757,9 @@ void TestImageAddressOperands() {
|
|||||||
Case{0x20, 1, true, false, 2, 1}, // packed xy
|
Case{0x20, 1, true, false, 2, 1}, // packed xy
|
||||||
Case{0x3d, 1, true, false, 5, 4}, // offset, half bias, full dref, packed xy
|
Case{0x3d, 1, true, false, 5, 4}, // offset, half bias, full dref, packed xy
|
||||||
Case{0x20, 2, true, true, 3, 2}, // packed xy/z in nonconsecutive VGPRs
|
Case{0x20, 2, true, true, 3, 2}, // packed xy/z in nonconsecutive VGPRs
|
||||||
|
Case{0x22, 4, true, false, 4, 3}, // a VGPR per 1D gradient, then packed x/slice
|
||||||
|
Case{0x22, 1, true, false, 6, 3}, // packed 2D gradients and xy
|
||||||
|
Case{0x22, 2, true, false, 9, 6}, // packed first four 3D gradients, a VGPR per last two
|
||||||
Case{0x4c, 1, false, true, 4, 4}, // full dref, xy, and LOD in NSA VGPRs
|
Case{0x4c, 1, false, true, 4, 4}, // full dref, xy, and LOD in NSA VGPRs
|
||||||
Case{0x0e, 1, false, false, 1, 1}, // resource query mip
|
Case{0x0e, 1, false, false, 1, 1}, // resource query mip
|
||||||
};
|
};
|
||||||
@@ -10739,6 +10812,8 @@ void TestCapturedBufferAtomicsX2() {
|
|||||||
"BufferAtomicIAdd64", "OpAtomicIAdd", ValueOpcode::BufferAtomicIAdd64},
|
"BufferAtomicIAdd64", "OpAtomicIAdd", ValueOpcode::BufferAtomicIAdd64},
|
||||||
{{0xe1680018u, 0x80000000u}, 0u, 241u, "BUFFER_ATOMIC_OR_X2",
|
{{0xe1680018u, 0x80000000u}, 0u, 241u, "BUFFER_ATOMIC_OR_X2",
|
||||||
"BufferAtomicOr64", "OpAtomicOr", ValueOpcode::BufferAtomicOr64},
|
"BufferAtomicOr64", "OpAtomicOr", ValueOpcode::BufferAtomicOr64},
|
||||||
|
{{0xe1582000u, 0x8003092au}, 9u, 237u, "BUFFER_ATOMIC_UMIN_X2",
|
||||||
|
"BufferAtomicUMin64", "OpAtomicUMin", ValueOpcode::BufferAtomicUMin64},
|
||||||
{{0xe1402000u, 0x80000913u}, 9u, 229u, "BUFFER_ATOMIC_SWAP_X2",
|
{{0xe1402000u, 0x80000913u}, 9u, 229u, "BUFFER_ATOMIC_SWAP_X2",
|
||||||
"BufferAtomicSwap64", "OpAtomicExchange", ValueOpcode::BufferAtomicSwap64},
|
"BufferAtomicSwap64", "OpAtomicExchange", ValueOpcode::BufferAtomicSwap64},
|
||||||
};
|
};
|
||||||
@@ -11500,7 +11575,7 @@ void TestEmbeddedFetchPreservesSharedScalarLoad() {
|
|||||||
reinterpret_cast<uint64_t>(attributes.data())};
|
reinterpret_cast<uint64_t>(attributes.data())};
|
||||||
std::memcpy(user_data.data() + 12, tables, sizeof(tables));
|
std::memcpy(user_data.data() + 12, tables, sizeof(tables));
|
||||||
ShaderVertexInputInfo input{};
|
ShaderVertexInputInfo input{};
|
||||||
input.fetch_embedded = true;
|
input.has_dynamic_vertex_fetch = true;
|
||||||
input.fetch_buffer_reg = 12;
|
input.fetch_buffer_reg = 12;
|
||||||
input.fetch_attrib_reg = 14;
|
input.fetch_attrib_reg = 14;
|
||||||
input.resources_num = 1;
|
input.resources_num = 1;
|
||||||
@@ -12593,6 +12668,71 @@ void TestFinalSsaRejectsRegisterStatePseudos() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void TestSsaRewriteSealsFilledBlocks() {
|
||||||
|
using namespace ShaderRecompiler::IR;
|
||||||
|
constexpr ScalarReg s0{0};
|
||||||
|
|
||||||
|
const auto add_blocks = [](Program &program) {
|
||||||
|
std::array<Block *, 4> blocks{};
|
||||||
|
for (auto &block : blocks) {
|
||||||
|
block =
|
||||||
|
program.block_storage.emplace_back(std::make_unique<Block>()).get();
|
||||||
|
program.blocks.push_back(block);
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
};
|
||||||
|
const auto use = [&](Block *block) {
|
||||||
|
const auto value = IREmitter(block).GetScalarReg(s0);
|
||||||
|
return &block->AppendNewInst(ValueOpcode::ReferenceU32, {value});
|
||||||
|
};
|
||||||
|
const auto is_phi = [](Value value, const Block *first, Value first_value,
|
||||||
|
const Block *second, Value second_value) {
|
||||||
|
const auto *phi = value.TryInstruction();
|
||||||
|
return phi != nullptr && phi->GetOpcode() == ValueOpcode::Phi &&
|
||||||
|
phi->NumArgs() == 2u && phi->PhiBlock(0) == first &&
|
||||||
|
phi->Arg(0) == first_value && phi->PhiBlock(1) == second &&
|
||||||
|
phi->Arg(1) == second_value;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
// entry(s0 = 7) -> left(s0 = 9) | right -> join.
|
||||||
|
Program program;
|
||||||
|
const auto [entry, left, right, join] = add_blocks(program);
|
||||||
|
entry->AddBranch(left);
|
||||||
|
entry->AddBranch(right);
|
||||||
|
left->AddBranch(join);
|
||||||
|
right->AddBranch(join);
|
||||||
|
IREmitter(entry).SetScalarReg(s0, U32(Value(7u)));
|
||||||
|
IREmitter(left).SetScalarReg(s0, U32(Value(9u)));
|
||||||
|
const auto *right_use = use(right);
|
||||||
|
const auto *join_use = use(join);
|
||||||
|
RewriteToSsa(program.blocks);
|
||||||
|
Check(right_use->Arg(0) == Value(7u) && right->Instructions().size() == 2u,
|
||||||
|
"SSA read in a single-predecessor block went through a trivial Phi");
|
||||||
|
Check(is_phi(join_use->Arg(0), left, Value(9u), right, Value(7u)),
|
||||||
|
"SSA join Phi lost a predecessor value");
|
||||||
|
}
|
||||||
|
|
||||||
|
// entry(s0 = 1) -> header <-> latch(s0 += 1), header -> tail.
|
||||||
|
Program program;
|
||||||
|
const auto [entry, header, latch, tail] = add_blocks(program);
|
||||||
|
entry->AddBranch(header);
|
||||||
|
header->AddBranch(latch);
|
||||||
|
header->AddBranch(tail);
|
||||||
|
latch->AddBranch(header);
|
||||||
|
IREmitter(entry).SetScalarReg(s0, U32(Value(1u)));
|
||||||
|
const auto *header_use = use(header);
|
||||||
|
IREmitter latch_ir(latch);
|
||||||
|
const auto sum = latch_ir.IAdd(latch_ir.GetScalarReg(s0), U32(Value(1u)));
|
||||||
|
latch_ir.SetScalarReg(s0, sum);
|
||||||
|
RewriteToSsa(program.blocks);
|
||||||
|
const auto phi = header_use->Arg(0);
|
||||||
|
Check(is_phi(phi, entry, Value(1u), latch, sum),
|
||||||
|
"SSA loop header Phi lost its entry or back-edge value");
|
||||||
|
Check(sum.Instruction()->Arg(0) == phi && latch->Instructions().size() == 3u,
|
||||||
|
"SSA read after a loop header went through a trivial Phi");
|
||||||
|
}
|
||||||
|
|
||||||
void TestValuePhiValidation() {
|
void TestValuePhiValidation() {
|
||||||
using namespace ShaderRecompiler::IR;
|
using namespace ShaderRecompiler::IR;
|
||||||
|
|
||||||
@@ -15160,7 +15300,7 @@ void TestBoundedScalarMaterialImageKeys() {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
for (const uint32_t key : {5567u, 5582u, 5573u, 5588u}) {
|
for (const uint32_t key : {0u, 5567u, 5582u, 5573u, 5588u}) {
|
||||||
if (address != 0x100000u + key * 32u || words.size() != 8) continue;
|
if (address != 0x100000u + key * 32u || words.size() != 8) continue;
|
||||||
++m.images;
|
++m.images;
|
||||||
std::ranges::fill(words, 0u);
|
std::ranges::fill(words, 0u);
|
||||||
@@ -15222,9 +15362,17 @@ void TestBoundedScalarMaterialImageKeys() {
|
|||||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||||
"unavailable selected material key was accepted");
|
"unavailable selected material key was accepted");
|
||||||
}
|
}
|
||||||
|
// Row 1's 0x94 key straddles a 311-byte V#, so it reads zero instead of 5573.
|
||||||
memory = {.stride = 1, .records = 311};
|
memory = {.stride = 1, .records = 311};
|
||||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization) && memory.key_reads == 0,
|
Check(MaterializeResources(plan, runtime, snapshot, specialization),
|
||||||
"scalar material byte range escaped V# bounds");
|
"scalar material table past its V# was rejected");
|
||||||
|
for (uint32_t root = 0; root < plan.info.images.size(); ++root) {
|
||||||
|
const auto &selector =
|
||||||
|
*plan.descriptor_sources[plan.info.images[root].source].indirect_descriptor->selector;
|
||||||
|
const auto offset = specialization.images[root].indirect_mapping_offset;
|
||||||
|
Check(snapshot.flattened_srt[offset + 1u] == (selector.offset == 0x94u ? 0u : 5567u),
|
||||||
|
"scalar material key past its V# did not read as zero");
|
||||||
|
}
|
||||||
memory = {.mixed = true};
|
memory = {.mixed = true};
|
||||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||||
snapshot.images.size() == 4 && specialization.images.size() == 4,
|
snapshot.images.size() == 4 && specialization.images.size() == 4,
|
||||||
@@ -16130,6 +16278,153 @@ void TestCapturedBufferLoadUbyteD16Decode() {
|
|||||||
"captured BUFFER_LOAD_UBYTE_D16 lost its preserved half, address, or coherence flags");
|
"captured BUFFER_LOAD_UBYTE_D16 lost its preserved half, address, or coherence flags");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestCapturedGlobalLoadShortD16HiDecode() {
|
||||||
|
namespace Decoder = ShaderRecompiler::Decoder;
|
||||||
|
// Ghost of Yotei CS 0x79b0066f0cd1f3cd: global_load_short_d16_hi v16, v18, s[0:1] offset:196.
|
||||||
|
const uint32_t code[] = {0xdc9480c4u, 0x10000012u};
|
||||||
|
Decoder::Instruction inst;
|
||||||
|
Decoder::DecodeInstruction(code, 0, inst);
|
||||||
|
Check(inst.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI &&
|
||||||
|
inst.family == Decoder::Family::FLAT && inst.word_count == 2 &&
|
||||||
|
inst.data_dwords == 1 && inst.data_bits == 16 && !inst.data_signed &&
|
||||||
|
inst.memory_segment == 2 && inst.offset == 196 && !inst.glc && !inst.slc &&
|
||||||
|
!inst.dlc,
|
||||||
|
"captured GLOBAL_LOAD_SHORT_D16_HI lost its halfword payload or address fields");
|
||||||
|
Check(inst.dst.kind == Decoder::OperandKind::Vgpr && inst.dst.reg == 16 &&
|
||||||
|
inst.dst.sdwa_sel == 5 && inst.dst.sdwa_dst_unused == 2,
|
||||||
|
"captured GLOBAL_LOAD_SHORT_D16_HI must write only the high half of v16");
|
||||||
|
Check(inst.src0.kind == Decoder::OperandKind::Vgpr && inst.src0.reg == 18 &&
|
||||||
|
inst.src1.kind == Decoder::OperandKind::Sgpr && inst.src1.reg == 0,
|
||||||
|
"captured GLOBAL_LOAD_SHORT_D16_HI lost its VGPR offset or s[0:1] base");
|
||||||
|
Check(Decoder::InstructionToString(inst).find("FLAT_LOAD_SHORT_D16_HI") != std::string::npos,
|
||||||
|
"captured GLOBAL_LOAD_SHORT_D16_HI diagnostic text lost the opcode name");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestFlatLoadShortD16HiLlvmEncodingAnchor() {
|
||||||
|
namespace Decoder = ShaderRecompiler::Decoder;
|
||||||
|
// llvm/test/MC/AMDGPU/flat-gfx10.s: flat_load_short_d16_hi v1, v[3:4]
|
||||||
|
// encodes as [0x00,0x00,0x94,0xdc,0x03,0x00,0x7d,0x01].
|
||||||
|
const uint32_t code[] = {0xdc940000u, 0x017d0003u};
|
||||||
|
Decoder::Instruction inst;
|
||||||
|
Decoder::DecodeInstruction(code, 0, inst);
|
||||||
|
Check(inst.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI && inst.word_count == 2 &&
|
||||||
|
inst.memory_segment == 0 && inst.offset == 0 && inst.data_dwords == 1 &&
|
||||||
|
inst.data_bits == 16 && !inst.data_signed,
|
||||||
|
"LLVM flat_load_short_d16_hi encoding did not decode as a FLAT halfword load");
|
||||||
|
Check(inst.dst.kind == Decoder::OperandKind::Vgpr && inst.dst.reg == 1 &&
|
||||||
|
inst.dst.sdwa_sel == 5 && inst.dst.sdwa_dst_unused == 2 &&
|
||||||
|
inst.src0.kind == Decoder::OperandKind::Vgpr && inst.src0.reg == 3 &&
|
||||||
|
inst.src1.kind == Decoder::OperandKind::Vgpr && inst.src1.reg == 4,
|
||||||
|
"LLVM flat_load_short_d16_hi v1, v[3:4] lost its destination half or address pair");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestFlatLoadShortD16HiCompilesAndValidates() {
|
||||||
|
// The captured high-half load followed by a buffer store of the merged register so that the
|
||||||
|
// load stays live: global_load_short_d16_hi v16, v18, s[0:1] offset:196.
|
||||||
|
const uint32_t shader[] = {
|
||||||
|
0xdc9480c4u, 0x10000012u,
|
||||||
|
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(16, 12, 0),
|
||||||
|
EncodeSopp(0x01),
|
||||||
|
};
|
||||||
|
auto options = MakeCompileOptions(ShaderType::Compute);
|
||||||
|
options.dump_ir = true;
|
||||||
|
auto result = RecompileForTest(shader, options);
|
||||||
|
Check(result.decoded_dump.find("FLAT_LOAD_SHORT_D16_HI") != std::string::npos &&
|
||||||
|
result.decoded_dump.find("unsupported family=") == std::string::npos,
|
||||||
|
"GLOBAL_LOAD_SHORT_D16_HI did not reach the decoded program");
|
||||||
|
Check(result.ir_dump.find("LoadAddressU16") != std::string::npos &&
|
||||||
|
result.ir_dump.find("BitFieldInsert") != std::string::npos,
|
||||||
|
"GLOBAL_LOAD_SHORT_D16_HI did not become a 16-bit load merged into the old register");
|
||||||
|
CheckSpirvBinaryValidates(result.spirv);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestFlatLoadShortD16HiAddressFields() {
|
||||||
|
namespace Decoder = ShaderRecompiler::Decoder;
|
||||||
|
// Modifier bits of the first FLAT-family word.
|
||||||
|
constexpr uint32_t DlcBit = 1u << 12u;
|
||||||
|
constexpr uint32_t LdsBit = 1u << 13u;
|
||||||
|
constexpr uint32_t GlcBit = 1u << 16u;
|
||||||
|
constexpr uint32_t SlcBit = 1u << 17u;
|
||||||
|
constexpr uint32_t SegFlat = 0;
|
||||||
|
constexpr uint32_t SegScratch = 1;
|
||||||
|
constexpr uint32_t SegGlobal = 2;
|
||||||
|
constexpr uint32_t SegReserved = 3;
|
||||||
|
constexpr uint32_t SaddrOff = 0x7du;
|
||||||
|
const auto decode = [](uint32_t seg, uint32_t offset_field, uint32_t saddr, uint32_t flags = 0) {
|
||||||
|
const uint32_t code[] = {EncodeFlat0(0x25, seg, offset_field) | flags,
|
||||||
|
EncodeFlat1(7, saddr, 0, 9)};
|
||||||
|
Decoder::Instruction inst;
|
||||||
|
Decoder::DecodeInstruction(code, 0, inst);
|
||||||
|
return inst;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OffsetCase {
|
||||||
|
uint32_t seg;
|
||||||
|
uint32_t field;
|
||||||
|
int32_t expected;
|
||||||
|
};
|
||||||
|
const OffsetCase offsets[] = {
|
||||||
|
// FLAT ignores OFFSET[11] and uses an unsigned 11-bit byte offset.
|
||||||
|
{SegFlat, 0x000, 0},
|
||||||
|
{SegFlat, 0x7ff, 2047},
|
||||||
|
{SegFlat, 0x800, 0},
|
||||||
|
{SegFlat, 0xfff, 2047},
|
||||||
|
// GLOBAL and SCRATCH sign-extend the 12-bit field.
|
||||||
|
{SegGlobal, 0x000, 0},
|
||||||
|
{SegGlobal, 0x7ff, 2047},
|
||||||
|
{SegGlobal, 0x800, -2048},
|
||||||
|
{SegGlobal, 0xfff, -1},
|
||||||
|
{SegScratch, 0x000, 0},
|
||||||
|
{SegScratch, 0x7ff, 2047},
|
||||||
|
{SegScratch, 0x800, -2048},
|
||||||
|
{SegScratch, 0xfff, -1},
|
||||||
|
};
|
||||||
|
for (const auto &test : offsets) {
|
||||||
|
const auto inst = decode(test.seg, test.field, SaddrOff);
|
||||||
|
Check(inst.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI &&
|
||||||
|
inst.memory_segment == test.seg &&
|
||||||
|
inst.offset == static_cast<uint32_t>(test.expected),
|
||||||
|
"FLAT-family D16 high-half load decoded the wrong byte offset for its segment");
|
||||||
|
Check(inst.dst.kind == Decoder::OperandKind::Vgpr && inst.dst.reg == 7 &&
|
||||||
|
inst.dst.sdwa_sel == 5 && inst.dst.sdwa_dst_unused == 2 &&
|
||||||
|
inst.src0.kind == Decoder::OperandKind::Vgpr && inst.src0.reg == 9,
|
||||||
|
"FLAT-family D16 high-half load lost its destination half or address VGPR");
|
||||||
|
}
|
||||||
|
|
||||||
|
// FLAT takes a VGPR pair whatever the SADDR field says; GLOBAL takes either an SGPR base plus
|
||||||
|
// a VGPR offset or, with SADDR off, a VGPR pair.
|
||||||
|
const auto flat = decode(SegFlat, 0, 2);
|
||||||
|
Check(flat.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI &&
|
||||||
|
flat.src1.kind == Decoder::OperandKind::Vgpr && flat.src1.reg == 10,
|
||||||
|
"FLAT D16 high-half load must use the VGPR pair and ignore SADDR");
|
||||||
|
const auto global_sgpr = decode(SegGlobal, 0, 2);
|
||||||
|
Check(global_sgpr.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI &&
|
||||||
|
global_sgpr.src_count == 2 && global_sgpr.src0.kind == Decoder::OperandKind::Vgpr &&
|
||||||
|
global_sgpr.src0.reg == 9 && global_sgpr.src1.kind == Decoder::OperandKind::Sgpr &&
|
||||||
|
global_sgpr.src1.reg == 2,
|
||||||
|
"GLOBAL D16 high-half load with SADDR s[2:3] lost its scalar base");
|
||||||
|
const auto global_pair = decode(SegGlobal, 0, SaddrOff);
|
||||||
|
Check(global_pair.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI &&
|
||||||
|
global_pair.src_count == 2 && global_pair.src0.kind == Decoder::OperandKind::Vgpr &&
|
||||||
|
global_pair.src0.reg == 9 && global_pair.src1.kind == Decoder::OperandKind::Vgpr &&
|
||||||
|
global_pair.src1.reg == 10,
|
||||||
|
"GLOBAL D16 high-half load with SADDR off must use the VGPR pair");
|
||||||
|
|
||||||
|
// DLC is only a cache hint and stays visible; LDS, GLC and SLC on a load and the reserved
|
||||||
|
// segment are not implemented and must keep failing loudly.
|
||||||
|
const auto dlc = decode(SegGlobal, 4, 2, DlcBit);
|
||||||
|
Check(dlc.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI && dlc.dlc && !dlc.glc && !dlc.slc,
|
||||||
|
"GLOBAL D16 high-half load lost its DLC hint");
|
||||||
|
Check(decode(SegGlobal, 0, 2, LdsBit).opcode == Decoder::Opcode::UNSUPPORTED,
|
||||||
|
"FLAT-family D16 high-half load accepted LDS=1");
|
||||||
|
Check(decode(SegGlobal, 0, 2, GlcBit).opcode == Decoder::Opcode::UNSUPPORTED,
|
||||||
|
"FLAT-family D16 high-half load accepted GLC=1");
|
||||||
|
Check(decode(SegGlobal, 0, 2, SlcBit).opcode == Decoder::Opcode::UNSUPPORTED,
|
||||||
|
"FLAT-family D16 high-half load accepted SLC=1 on a load");
|
||||||
|
Check(decode(SegReserved, 0, 2).opcode == Decoder::Opcode::UNSUPPORTED,
|
||||||
|
"FLAT-family D16 high-half load accepted the reserved segment");
|
||||||
|
}
|
||||||
|
|
||||||
void TestCapturedBufferStoreShortD16HiDecode() {
|
void TestCapturedBufferStoreShortD16HiDecode() {
|
||||||
namespace Decoder = ShaderRecompiler::Decoder;
|
namespace Decoder = ShaderRecompiler::Decoder;
|
||||||
const uint32_t code[] = {0xe06c6000u, 0x80010326u};
|
const uint32_t code[] = {0xe06c6000u, 0x80010326u};
|
||||||
@@ -16867,6 +17162,7 @@ int main() {
|
|||||||
// here.
|
// here.
|
||||||
TestScalarAshrI64Decoder();
|
TestScalarAshrI64Decoder();
|
||||||
TestImageAtomicWidthDecoder();
|
TestImageAtomicWidthDecoder();
|
||||||
|
TestImageGatherDecoder();
|
||||||
TestDsReturningAtomic64Decoder();
|
TestDsReturningAtomic64Decoder();
|
||||||
TestDsCompareStore32Decoder();
|
TestDsCompareStore32Decoder();
|
||||||
TestDsFloatMinMaxDecoder();
|
TestDsFloatMinMaxDecoder();
|
||||||
@@ -16966,6 +17262,7 @@ int main() {
|
|||||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||||
TestFinalSsaRejectsRegisterStatePseudos();
|
TestFinalSsaRejectsRegisterStatePseudos();
|
||||||
#endif
|
#endif
|
||||||
|
TestSsaRewriteSealsFilledBlocks();
|
||||||
TestValuePhiValidation();
|
TestValuePhiValidation();
|
||||||
TestLaneMaskProjection();
|
TestLaneMaskProjection();
|
||||||
TestLateConstantPropagation();
|
TestLateConstantPropagation();
|
||||||
@@ -17017,6 +17314,10 @@ int main() {
|
|||||||
TestPixelProgramCacheBindingIdentity();
|
TestPixelProgramCacheBindingIdentity();
|
||||||
TestGraphicsPushConstantPlacement();
|
TestGraphicsPushConstantPlacement();
|
||||||
TestCapturedBufferLoadUbyteD16Decode();
|
TestCapturedBufferLoadUbyteD16Decode();
|
||||||
|
TestCapturedGlobalLoadShortD16HiDecode();
|
||||||
|
TestFlatLoadShortD16HiLlvmEncodingAnchor();
|
||||||
|
TestFlatLoadShortD16HiAddressFields();
|
||||||
|
TestFlatLoadShortD16HiCompilesAndValidates();
|
||||||
TestCapturedBufferStoreShortD16HiDecode();
|
TestCapturedBufferStoreShortD16HiDecode();
|
||||||
TestCapturedBufferStoreFormatD16HiXDecode();
|
TestCapturedBufferStoreFormatD16HiXDecode();
|
||||||
TestCapturedBufferLoadD16XyDecode();
|
TestCapturedBufferLoadD16XyDecode();
|
||||||
|
|||||||
Reference in new issue
Block a user