Compare commits

...
Author SHA1 Message Date
nmzik cd19fa283f Implement IME dialog overlay 2026-08-05 05:14:16 +02:00
nmzik b6af7c73ec graphics: support Standard256B storage textures 2026-08-05 05:09:28 +02:00
nmzik ac64d202af shader: implement BUFFER_ATOMIC_FMAX 2026-08-05 02:59:28 +02:00
nmzik df44ad3fac shader: implement BUFFER_ATOMIC_FMIN, unhandled IR opcode now fails 2026-08-05 02:59:28 +02:00
nmzik 0a2f4814ac refactor(shader): centralize opcode metadata and lookup paths 2026-08-05 02:59:28 +02:00
nmzik e6b4707a5d renderer: support R8 uint render targets 2026-08-05 02:59:28 +02:00
nmzik 9a3468e3a0 shader: implement V_MIN_U32 2026-08-05 02:59:28 +02:00
nmzik 73da43a7be shader: implement V_CNDMASK_B32 2026-08-05 02:59:28 +02:00
nmzik aa88d94755 shader: implement V_SUB_NC_U32 2026-08-05 02:59:27 +02:00
nmzik 51562cd8d2 tests: use valid image descriptors in shader CFG cases 2026-08-05 02:59:27 +02:00
nmzik 822ca3c10b shader: implement S_CMP_EQ_U64 2026-08-05 02:59:27 +02:00
nmzik 2b6fd699e7 shader: implement V_CMP_EQ_I64 2026-08-05 02:59:27 +02:00
nmzikandStefanos Costa b9aa0dc3b3 fix KernelLseek lock leak
Co-authored-by: Stefanos Costa <stefanos.costa1999@gmail.com>
2026-08-05 02:25:05 +02:00
nmzikandStefanos Costa f380d69b9d fix AudioOut pacing
Co-authored-by: Stefanos Costa <stefanos.costa1999@gmail.com>
2026-08-05 02:17:25 +02:00
DeathWhispers f06d7e6084 launcher: disable Vulkan validation by default
Keep validation opt-in while validation-layer errors remain fatal.

Refs #161
2026-08-04 22:09:40 +02:00
nmzik 00a5dd32c0 Implement syncOnAddress ABIs (perf improvement) 2026-08-04 21:24:12 +02:00
nmzik a9ddd44d73 CI - enable LTO, remove OPT:NOREF 2026-08-04 19:30:49 +02:00
nmzikandDeathWhispers 2f9fcc63c3 fix (agc): implement DCB context state operations
Co-authored-by: DeathWhispers <56944463+Death-Whispers-U@users.noreply.github.com>
2026-08-04 19:14:21 +02:00
nmzik 0bbd0bdd99 fix launcher regression 2026-08-04 03:35:30 +02:00
nmzikandDistantMyth 6396dc9a85 feat(input): add host input remapping
Add compact keyboard and mouse mapping with analog-stick defaults and integrate a global remapping editor into the Qt launcher.

Based on the host input remapping work from PR #109.

Co-authored-by: DistantMyth <thetarun3@gmail.com>
2026-08-04 03:24:34 +02:00
nmzik a3a00464bc fix(launcher): focus emulator window on launch 2026-08-04 02:51:11 +02:00
nmzik 1da99a0671 Add warning for Windows-forbidden filename characters 2026-08-04 02:26:50 +02:00
nmzik 9304921132 add fullscreen mode 2026-08-04 02:26:50 +02:00
nmzik c13a1d752b renderer: remove duplicate vertex and index buffer rebind 2026-08-04 02:26:50 +02:00
nmzik 51d0e02543 perf: sync only GPU-dirty readbacks 2026-08-04 02:26:50 +02:00
Ali Almohayaandnmzik e8752aca7e feat: identify build provenance in window title (#159)
* feat: identify build provenance in window title

* fix: propagate generated version dependency

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-08-04 01:04:10 +02:00
nmzik 7dd50ea127 change default flexible memory size 2026-08-03 18:06:43 +02:00
100 changed files with 6054 additions and 1581 deletions

No files matched your search

+41 -12
View File
@@ -6,9 +6,27 @@ on:
branches: [ main, master ]
pull_request:
env:
KYTY_BUILD_ORIGIN: ${{ github.repository != 'KytyPS5/KytyPS5' && 'Fork' || github.event_name == 'push' && 'Official' || 'Source' }}
KYTY_BUILD_REPOSITORY: ${{ github.repository }}
jobs:
metadata:
name: Prepare build metadata
runs-on: ubuntu-latest
outputs:
release_tag: ${{ steps.release_tag.outputs.value }}
steps:
- name: Generate release tag
id: release_tag
shell: bash
run: |
echo "value=KytyPS5-$(date -u +'%Y-%m-%d')-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
windows:
name: Build KytyPS5 (Windows)
needs: metadata
runs-on: windows-2022
steps:
@@ -76,6 +94,10 @@ jobs:
cmake -S . -B _Build/windows ^
-G Ninja ^
-DCMAKE_BUILD_TYPE=Release ^
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON ^
-DKYTY_BUILD_ORIGIN=%KYTY_BUILD_ORIGIN% ^
-DKYTY_BUILD_REPOSITORY=%KYTY_BUILD_REPOSITORY% ^
-DKYTY_RELEASE_TAG=${{ needs.metadata.outputs.release_tag }} ^
-DCMAKE_C_COMPILER=clang-cl ^
-DCMAKE_CXX_COMPILER=clang-cl ^
-DCMAKE_PREFIX_PATH="%Qt6_DIR%"
@@ -83,12 +105,12 @@ jobs:
- name: Build
shell: cmd
run: |
cmake --build _Build/windows --target launcher audio_out2_port_tests virtual_memory_allocation_tests --parallel
cmake --build _Build/windows --target launcher audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests --parallel
- name: Test
shell: cmd
run: |
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|virtual_memory_allocation)$"
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|ime_dialog|virtual_memory_allocation)$"
- name: Install
shell: cmd
@@ -104,6 +126,7 @@ jobs:
macos:
name: Build KytyPS5 (macOS)
needs: metadata
runs-on: macos-15
steps:
@@ -150,6 +173,10 @@ jobs:
cmake -S . -B _Build/macos \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \
-DKYTY_BUILD_ORIGIN="$KYTY_BUILD_ORIGIN" \
-DKYTY_BUILD_REPOSITORY="$KYTY_BUILD_REPOSITORY" \
-DKYTY_RELEASE_TAG="${{ needs.metadata.outputs.release_tag }}" \
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
@@ -159,14 +186,14 @@ jobs:
shell: bash
run: |
cmake --build _Build/macos \
--target launcher audio_out2_port_tests virtual_memory_allocation_tests \
--target launcher audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests \
--parallel
- name: Test
shell: bash
run: |
ctest --test-dir _Build/macos --output-on-failure \
-R '^(audio_out2_port|virtual_memory_allocation)$'
-R '^(audio_out2_port|ime_dialog|virtual_memory_allocation)$'
- name: Install
shell: bash
@@ -213,6 +240,7 @@ jobs:
linux:
name: Build KytyPS5 (Linux)
needs: metadata
runs-on: ubuntu-24.04
steps:
@@ -273,6 +301,10 @@ jobs:
cmake -S . -B _Build/linux \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \
-DKYTY_BUILD_ORIGIN="$KYTY_BUILD_ORIGIN" \
-DKYTY_BUILD_REPOSITORY="$KYTY_BUILD_REPOSITORY" \
-DKYTY_RELEASE_TAG="${{ needs.metadata.outputs.release_tag }}" \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_PREFIX_PATH="$Qt6_DIR" 2>&1 | tee _Build/configure.log
@@ -297,14 +329,14 @@ jobs:
run: |
cmake --build _Build/linux \
--target launcher page_manager_tests memory_tracker_tests \
audio_out2_port_tests virtual_memory_allocation_tests \
audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests \
--parallel
- name: Test
shell: bash
run: |
ctest --test-dir _Build/linux --output-on-failure \
-R '^(audio_out2_port|page_manager|memory_tracker|virtual_memory_allocation)$'
-R '^(audio_out2_port|ime_dialog|page_manager|memory_tracker|virtual_memory_allocation)$'
- name: Install
shell: bash
@@ -342,10 +374,12 @@ jobs:
release:
name: Release KytyPS5
if: github.event_name == 'push' && github.repository == 'KytyPS5/KytyPS5'
needs: [windows, macos, linux]
needs: [metadata, windows, macos, linux]
runs-on: ubuntu-latest
permissions:
contents: write
env:
RELEASE_NAME: ${{ needs.metadata.outputs.release_tag }}
steps:
- name: Download builds
@@ -353,11 +387,6 @@ jobs:
with:
path: artifacts
- name: Set release name
shell: bash
run: |
echo "RELEASE_NAME=KytyPS5-$(date -u +'%Y-%m-%d')-${GITHUB_SHA::7}" >> "$GITHUB_ENV"
- name: Package builds
shell: bash
run: |
+3
View File
@@ -39,3 +39,6 @@
[submodule "3rdparty/nlohmann_json"]
path = 3rdparty/nlohmann_json
url = https://github.com/nlohmann/json.git
[submodule "3rdparty/imgui"]
path = 3rdparty/imgui
url = https://github.com/ocornut/imgui.git
+19
View File
@@ -11,6 +11,25 @@ target_include_directories(Vulkan-Headers INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/Vulkan-Headers/include"
)
add_library(imgui STATIC
imgui/imgui.cpp
imgui/imgui_draw.cpp
imgui/imgui_tables.cpp
imgui/imgui_widgets.cpp
imgui/backends/imgui_impl_vulkan.cpp
)
target_include_directories(imgui PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/imgui"
"${CMAKE_CURRENT_SOURCE_DIR}/imgui/backends"
)
target_compile_definitions(imgui PRIVATE IMGUI_IMPL_VULKAN_NO_PROTOTYPES)
target_link_libraries(imgui PUBLIC Vulkan::Headers)
if(MSVC)
target_compile_options(imgui PRIVATE /W0)
else()
target_compile_options(imgui PRIVATE -w)
endif()
set(SPIRV_SKIP_TESTS ON CACHE BOOL "" FORCE)
set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "" FORCE)
set(SKIP_SPIRV_TOOLS_INSTALL ON CACHE BOOL "" FORCE)
Vendored Submodule
+1
Submodule 3rdparty/imgui added at f1cc2ae15e.
+1 -1
+60 -10
View File
@@ -4,6 +4,12 @@ if(CMAKE_VERSION VERSION_GREATER_EQUAL 4.0)
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "Minimum policy version for bundled dependencies")
endif()
get_property(KYTY_GENERATOR_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(NOT KYTY_GENERATOR_IS_MULTI_CONFIG AND NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release RelWithDebInfo MinSizeRel)
endif()
project(Kyty)
set(KYTY_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
@@ -28,6 +34,29 @@ include(CTest)
option(KYTY_ENABLE_CLANG_TIDY "Run clang-tidy checks during builds" OFF)
set(KYTY_BUILD_ORIGIN "Source" CACHE STRING "Build provenance shown in diagnostics")
set_property(CACHE KYTY_BUILD_ORIGIN PROPERTY STRINGS Source Official Fork)
if(NOT KYTY_BUILD_ORIGIN STREQUAL "Source" AND NOT KYTY_BUILD_ORIGIN STREQUAL "Official" AND
NOT KYTY_BUILD_ORIGIN STREQUAL "Fork")
message(FATAL_ERROR "KYTY_BUILD_ORIGIN must be Source, Official, or Fork")
endif()
set(KYTY_RELEASE_TAG "" CACHE STRING "Release tag embedded in official builds")
set(KYTY_BUILD_REPOSITORY "" CACHE STRING "GitHub repository embedded in fork builds")
set(KYTY_OFFICIAL_BUILD OFF)
set(KYTY_FORK_BUILD OFF)
if(KYTY_BUILD_ORIGIN STREQUAL "Official")
if(KYTY_RELEASE_TAG STREQUAL "")
message(FATAL_ERROR "Official builds require KYTY_RELEASE_TAG")
endif()
set(KYTY_OFFICIAL_BUILD ON)
elseif(KYTY_BUILD_ORIGIN STREQUAL "Fork")
if(KYTY_BUILD_REPOSITORY STREQUAL "")
message(FATAL_ERROR "Fork builds require KYTY_BUILD_REPOSITORY")
endif()
set(KYTY_FORK_BUILD ON)
endif()
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
include(TestBigEndian)
@@ -117,16 +146,20 @@ configure_file(
find_package(Git)
add_custom_target( KytyGitVersion
COMMAND ${CMAKE_COMMAND}
-D INPUT_FILE=${KYTY_SOURCE_DIR}/kytyGitVersion.h.in
-D OUTPUT_FILE=${PROJECT_BINARY_DIR}/kytyGitVersion.h
-D GIT_EXECUTABLE=${GIT_EXECUTABLE}
-D GIT_WORKING_DIRECTORY=${CMAKE_CURRENT_SOURCE_DIR}
-P ${KYTY_SOURCE_DIR}/generate_version.cmake
add_custom_target(KytyGitVersion
COMMAND ${CMAKE_COMMAND}
-D INPUT_FILE=${KYTY_SOURCE_DIR}/kytyGitVersion.h.in
-D OUTPUT_FILE=${PROJECT_BINARY_DIR}/kytyGitVersion.h
-D GIT_EXECUTABLE=${GIT_EXECUTABLE}
-D GIT_WORKING_DIRECTORY=${CMAKE_CURRENT_SOURCE_DIR}
-P ${KYTY_SOURCE_DIR}/generate_version.cmake
BYPRODUCTS ${PROJECT_BINARY_DIR}/kytyGitVersion.h
COMMENT "Generate kytyGitVersion.h"
)
add_library(kyty_git_version INTERFACE)
add_dependencies(kyty_git_version KytyGitVersion)
if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0.0)
list(APPEND KYTY_IWYU
kyty_emulator
@@ -259,7 +292,7 @@ if (KYTY_CLANG_CL)
set_source_files_properties(${kyty_emulator_src} PROPERTIES COMPILE_FLAGS "-Wno-pragma-pack -Wno-deprecated-declarations -D_TIMESPEC_DEFINED")
endif()
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL2-static xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9)
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL2-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version)
# Linux system libraries required by the static FFmpeg archive.
if(LINUX)
@@ -352,6 +385,12 @@ add_executable(bit_array_tests EXCLUDE_FROM_ALL
)
target_include_directories(bit_array_tests PRIVATE ${inc_headers})
add_executable(ime_dialog_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/ImeDialogTests.cpp"
"${KYTY_SOURCE_DIR}/libs/imeDialog.cpp"
)
target_include_directories(ime_dialog_tests PRIVATE ${inc_headers})
add_executable(memory_tracker_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/MemoryTrackerTests.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/pageManager.cpp"
@@ -417,6 +456,13 @@ add_executable(event_queue_lifetime_tests EXCLUDE_FROM_ALL
target_link_libraries(event_queue_lifetime_tests common fmt::fmt)
target_include_directories(event_queue_lifetime_tests PRIVATE ${inc_headers})
add_executable(sync_on_address_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/SyncOnAddressTests.cpp"
"${KYTY_SOURCE_DIR}/kernel/syncOnAddress.cpp"
)
target_link_libraries(sync_on_address_tests common)
target_include_directories(sync_on_address_tests PRIVATE ${inc_headers})
add_executable(image_page_table_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/ImagePageTableTests.cpp"
)
@@ -459,6 +505,7 @@ if(NOT KYTY_CLANG_CL)
endif()
if(BUILD_TESTING)
add_test(NAME ime_dialog COMMAND $<TARGET_FILE:ime_dialog_tests>)
add_test(NAME shader_cfg COMMAND $<TARGET_FILE:shader_cfg_tests>)
add_test(NAME scalar_provenance COMMAND $<TARGET_FILE:scalar_provenance_tests>)
add_test(NAME image_page_table COMMAND $<TARGET_FILE:image_page_table_tests>)
@@ -470,6 +517,7 @@ if(BUILD_TESTING)
add_test(NAME resource_tracking COMMAND $<TARGET_FILE:resource_tracking_tests>)
add_test(NAME resource_mutex COMMAND $<TARGET_FILE:resource_mutex_tests>)
add_test(NAME event_queue_lifetime COMMAND $<TARGET_FILE:event_queue_lifetime_tests>)
add_test(NAME sync_on_address COMMAND $<TARGET_FILE:sync_on_address_tests>)
add_test(NAME audio_out2_port COMMAND $<TARGET_FILE:audio_out2_port_tests>)
add_test(NAME shader_recompiler_compute COMMAND $<TARGET_FILE:shader_recompiler_compute_tests>)
add_test(NAME virtual_memory_allocation
@@ -480,6 +528,8 @@ if(BUILD_TESTING)
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --stream-buffer-only)
add_test(NAME gpu_command_lane
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --gpu-command-lane-only)
add_test(NAME pm4_context_state
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --context-state-only)
add_test(NAME gpu_tiler
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --gpu-tiler-only)
@@ -505,6 +555,7 @@ if(BUILD_TESTING)
endif()
add_custom_target(kyty_tests DEPENDS
ime_dialog_tests
shader_cfg_tests
scalar_provenance_tests
image_page_table_tests
@@ -516,6 +567,7 @@ if(BUILD_TESTING)
resource_tracking_tests
resource_mutex_tests
event_queue_lifetime_tests
sync_on_address_tests
shader_recompiler_compute_tests
virtual_memory_allocation_tests
)
@@ -563,8 +615,6 @@ elseif(WIN32 OR LINUX)
set_target_properties(kyty_emulator PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS} -Wl,-Map=${KYTY_EMULATOR_MAP_LINK_PATH}")
endif()
add_dependencies(kyty_emulator KytyGitVersion)
if(APPLE)
# The emulator writes x86-64 trampolines/PLT stubs into memory and executes them, and
# runs guest code under Rosetta. That requires the JIT / unsigned-executable-memory
+4
View File
@@ -1,4 +1,8 @@
#define KYTY_VERSION "@KYTY_VERSION@"
#define KYTY_BUILD_REPOSITORY "@KYTY_BUILD_REPOSITORY@"
#define KYTY_RELEASE_TAG "@KYTY_RELEASE_TAG@"
#cmakedefine KYTY_OFFICIAL_BUILD
#cmakedefine KYTY_FORK_BUILD
#define KYTY_ENDIAN @KYTY_ENDIAN@
#define KYTY_PLATFORM @KYTY_PLATFORM@
#define KYTY_COMPILER KYTY_COMPILER_@KYTY_COMPILER@
+8
View File
@@ -33,6 +33,10 @@ uint32_t GetScreenHeight() {
return g_config->screen_height;
}
bool FullscreenEnabled() {
return g_config->fullscreen_enabled;
}
uint32_t GetVblankFrequency() {
return std::clamp(g_config->vblank_frequency, 30u, 360u);
}
@@ -97,4 +101,8 @@ bool ReadbackLinearImagesEnabled() {
return g_config->readback_linear_images;
}
const Keymap& GetKeymap() {
return g_config->keymap;
}
} // namespace Config
+9
View File
@@ -5,6 +5,8 @@
#include "common/subsystems.h"
#include <filesystem>
#include <string>
#include <vector>
namespace Config {
@@ -18,12 +20,15 @@ enum class ProfilerDirection { None, Network };
enum class OutputDirection { Silent, Console, File };
using Keymap = std::vector<std::string>;
constexpr uint32_t DEFAULT_CONSOLE_LANGUAGE = 1;
constexpr uint32_t MAX_CONSOLE_LANGUAGE = 29;
struct ConfigOptions {
uint32_t screen_width = 1280;
uint32_t screen_height = 720;
bool fullscreen_enabled = false;
uint32_t vblank_frequency = 60;
uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
@@ -40,12 +45,14 @@ struct ConfigOptions {
bool spirv_debug_printf_enabled = false;
bool renderdoc_enabled = false;
bool readback_linear_images = false;
Keymap keymap;
};
void Load(const ConfigOptions& cfg);
uint32_t GetScreenWidth();
uint32_t GetScreenHeight();
bool FullscreenEnabled();
uint32_t GetVblankFrequency();
uint32_t GetConsoleLanguage();
bool VulkanValidationEnabled();
@@ -70,6 +77,8 @@ bool SpirvDebugPrintfEnabled();
bool RenderDocEnabled();
bool ReadbackLinearImagesEnabled();
const Keymap& GetKeymap();
} // namespace Config
#endif /* KYTY_COMMON_EMULATOR_CONFIG_H_ */
+24 -1
View File
@@ -1,7 +1,8 @@
set(KYTY_GIT_VERSION "unknown")
set(KYTY_GIT_HASH "unknown")
if(GIT_EXECUTABLE)
execute_process(
COMMAND "${GIT_EXECUTABLE}" describe --tags --always
COMMAND "${GIT_EXECUTABLE}" describe --tags --always --dirty
WORKING_DIRECTORY "${GIT_WORKING_DIRECTORY}"
OUTPUT_VARIABLE KYTY_GIT_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
@@ -11,5 +12,27 @@ if(GIT_EXECUTABLE)
if(NOT GIT_RESULT EQUAL 0)
set(KYTY_GIT_VERSION "unknown")
endif()
execute_process(
COMMAND "${GIT_EXECUTABLE}" rev-parse --short=7 HEAD
WORKING_DIRECTORY "${GIT_WORKING_DIRECTORY}"
OUTPUT_VARIABLE KYTY_GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE GIT_HASH_RESULT
ERROR_QUIET
)
if(NOT GIT_HASH_RESULT EQUAL 0)
set(KYTY_GIT_HASH "unknown")
else()
execute_process(
COMMAND "${GIT_EXECUTABLE}" diff-index --quiet HEAD --
WORKING_DIRECTORY "${GIT_WORKING_DIRECTORY}"
RESULT_VARIABLE GIT_DIRTY_RESULT
ERROR_QUIET
)
if(NOT GIT_DIRTY_RESULT EQUAL 0)
string(APPEND KYTY_GIT_HASH "-dirty")
endif()
endif()
endif()
configure_file("${INPUT_FILE}" "${OUTPUT_FILE}")
@@ -15,6 +15,13 @@ bool TestWaitRegMemValue(uint64_t value, uint64_t ref, uint64_t mask, uint32_t f
enum class Pm4ProcessResult { Complete, Blocked };
enum class ContextStateOperation : uint32_t {
Clear = 0,
Push = 1,
Pop = 2,
PushClear = 3,
};
class Pm4Execution {
public:
[[nodiscard]] bool MadeProgress() const noexcept { return m_made_progress; }
@@ -49,24 +56,12 @@ public:
KYTY_CLASS_NO_COPY(CommandProcessor);
void Reset();
void ApplyContextStateOperation(ContextStateOperation operation);
void BufferInit();
void BufferFlush();
void BufferFlushAndWait();
void BufferWait();
void BeginReadbackTransaction() {
if (m_readback_active) {
EXIT("nested command-processor readback transaction\n");
}
m_readback_active = true;
}
void EndReadbackTransaction() {
if (!m_readback_active) {
EXIT("command-processor readback transaction is not active\n");
}
m_readback_active = false;
}
void BufferInit();
void BufferFlush();
void BufferFlushAndWait();
void BufferWait();
HW::Context& GetCtx() { return m_ctx; }
HW::UserConfig& GetUcfg() { return m_ucfg; }
HW::Shader& GetShCtx() { return m_sh_ctx; }
@@ -162,6 +157,8 @@ private:
RenderContext& m_renderer;
HW::Context m_ctx;
HW::Context m_saved_ctx;
bool m_context_state_pushed = false;
HW::UserConfig m_ucfg;
HW::Shader m_sh_ctx;
HW::UserSgprType m_user_data_marker = HW::UserSgprType::Unknown;
@@ -173,10 +170,9 @@ private:
// Persistent draw state: indirect draws update it for subsequent draws.
uint32_t m_num_instances = 1;
uint32_t m_de_count = 0;
uint32_t m_ce_count = 0;
bool m_ce_complete = false;
bool m_readback_active = false;
uint32_t m_de_count = 0;
uint32_t m_ce_count = 0;
bool m_ce_complete = false;
uint32_t m_const_ram[0x3000] = {0};
@@ -254,6 +254,7 @@ constexpr auto MakeCustomOpcodeDispatchTable() {
g_cp_op_custom_func[Pm4::R_WAIT_MEM_64] = CpOpWaitRegMem64;
g_cp_op_custom_func[Pm4::R_FLIP] = CpOpFlip;
g_cp_op_custom_func[Pm4::R_RELEASE_MEM] = CpOpReleaseMem;
g_cp_op_custom_func[Pm4::R_CONTEXT_STATE] = CpOpContextState;
return g_cp_op_custom_func;
}
@@ -121,6 +121,7 @@ uint32_t HwUcSetGeIndexOffset(CommandProcessor&, uint32_t, uint32_t, const uint3
uint32_t HwUcSetGdsOaRegisters(CommandProcessor&, uint32_t, uint32_t, const uint32_t*, uint32_t);
uint32_t CpOpNop(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpContextState(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpSetBase(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpClearState(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpIndexBase(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
@@ -2107,6 +2107,19 @@ KYTY_CP_OP_PARSER(CpOpClearState) {
return 1;
}
KYTY_CP_OP_PARSER(CpOpContextState) {
KYTY_PROFILER_FUNCTION();
const auto packet_size_dw = KYTY_PM4_LEN(cmd_id);
EXIT_NOT_IMPLEMENTED(KYTY_PM4_R(cmd_id) != Pm4::R_CONTEXT_STATE);
EXIT_NOT_IMPLEMENTED(packet_size_dw != 3u && packet_size_dw != 5u);
EXIT_NOT_IMPLEMENTED(buffer[0] > static_cast<uint32_t>(ContextStateOperation::PushClear));
cp.ApplyContextStateOperation(static_cast<ContextStateOperation>(buffer[0]));
return packet_size_dw - 1u;
}
KYTY_CP_OP_PARSER(CpOpDumpConstRam) {
KYTY_PROFILER_FUNCTION();
+59 -26
View File
@@ -36,6 +36,7 @@ static thread_local CommandProcessor* g_current_processor = nullptr;
static thread_local Pm4Execution* g_current_execution = nullptr;
static thread_local bool g_gpu_mutex_owned = false;
static thread_local bool g_gpu_thread = false;
static thread_local GpuState* g_gpu_state = nullptr;
class GpuMutexLock final {
public:
@@ -98,11 +99,10 @@ public:
void SubmitFlipPreparation(uint64_t request_id);
void Done();
void Shutdown();
[[nodiscard]] bool IsStopping();
void SendCommand(Common::UniqueFunction<void>&& command);
void SendCommandSync(Common::UniqueFunction<void>&& command);
void SendCommandSyncWithProcessor(Common::UniqueFunction<void, CommandProcessor&>&& command);
int GetFrameNum();
[[nodiscard]] bool IsStopping();
void SendCommand(Common::UniqueFunction<void>&& command);
void SendCommandSync(Common::UniqueFunction<void>&& command);
int GetFrameNum();
[[nodiscard]] static bool IsGpuThread() noexcept { return g_gpu_thread; }
private:
@@ -127,6 +127,7 @@ private:
void WaitLocked();
void Enqueue(Submission submission);
void WaitForIdle();
void ProcessCommands();
bool Process(Submission& submission);
static void ThreadRun(void* data);
CommandProcessor& GetProcessor(uint32_t queue_id);
@@ -139,6 +140,7 @@ private:
Common::CondVar m_idle;
std::array<std::deque<Submission>, QueueCount> m_queues;
std::deque<Common::UniqueFunction<void>> m_commands;
std::atomic_uint32_t m_pending_commands {0};
uint32_t m_next_queue = 0;
uint32_t m_submission_count = 0;
bool m_processing = false;
@@ -153,6 +155,8 @@ private:
uint64_t m_submit_id = 0;
std::atomic_int m_done_num = 0;
std::jthread m_thread;
friend class CommandProcessor;
};
static bool GraphicsRunDebugDumpEnabled() {
@@ -195,9 +199,25 @@ void GpuState::SendCommand(Common::UniqueFunction<void>&& command) {
Common::LockGuard lock(m_queue_mutex);
EXIT_IF(!m_accepting);
m_commands.push_back(std::move(command));
m_pending_commands.fetch_add(1, std::memory_order_release);
m_work_available.Signal();
}
void GpuState::ProcessCommands() {
EXIT_IF(!IsGpuThread());
while (m_pending_commands.load(std::memory_order_acquire) != 0) {
Common::UniqueFunction<void> command;
{
Common::LockGuard lock(m_queue_mutex);
EXIT_IF(m_commands.empty());
command = std::move(m_commands.front());
m_commands.pop_front();
EXIT_IF(m_pending_commands.fetch_sub(1, std::memory_order_acq_rel) == 0);
}
command();
}
}
void GpuState::SendCommandSync(Common::UniqueFunction<void>&& command) {
EXIT_IF(!command);
if (IsGpuThread()) {
@@ -212,17 +232,6 @@ void GpuState::SendCommandSync(Common::UniqueFunction<void>&& command) {
done.acquire();
}
void GpuState::SendCommandSyncWithProcessor(
Common::UniqueFunction<void, CommandProcessor&>&& command) {
EXIT_IF(!command);
SendCommandSync([this, operation = std::move(command)]() mutable {
EXIT_IF(g_current_processor != nullptr);
g_current_processor = m_gfx_cp.get();
operation(*m_gfx_cp);
g_current_processor = nullptr;
});
}
void GpuState::Submit(uint32_t* cmd_draw_buffer, uint32_t num_draw_dw, uint32_t* cmd_const_buffer,
uint32_t num_const_dw, bool trigger_agc_interrupt_on_done) {
GpuMutexLock lock(m_submission_mutex);
@@ -303,6 +312,8 @@ void CommandProcessor::Reset() {
m_sh_ctx.Reset();
m_ucfg.Reset();
m_ctx.Reset();
m_saved_ctx.Reset();
m_context_state_pushed = false;
m_index_type_and_size = 0;
m_index_buffer_size = 0;
m_user_data_marker = HW::UserSgprType::Unknown;
@@ -312,6 +323,30 @@ void CommandProcessor::Reset() {
std::memset(m_const_ram, 0, sizeof(m_const_ram));
}
void CommandProcessor::ApplyContextStateOperation(ContextStateOperation operation) {
switch (operation) {
case ContextStateOperation::Clear: m_ctx.Reset(); break;
case ContextStateOperation::Push:
EXIT_IF(m_context_state_pushed);
m_saved_ctx = m_ctx;
m_context_state_pushed = true;
break;
case ContextStateOperation::Pop:
EXIT_IF(!m_context_state_pushed);
m_ctx = m_saved_ctx;
m_saved_ctx = {};
m_context_state_pushed = false;
break;
case ContextStateOperation::PushClear:
EXIT_IF(m_context_state_pushed);
m_saved_ctx = m_ctx;
m_context_state_pushed = true;
m_ctx.Reset();
break;
default: EXIT("unknown context state operation: %u\n", static_cast<uint32_t>(operation));
}
}
void CommandProcessor::BufferInit() {
GetScheduler().Begin(m_ctx, m_ucfg, m_sh_ctx);
}
@@ -509,6 +544,7 @@ void GpuState::ThreadRun(void* data) {
EXIT_IF(gpu == nullptr);
KYTY_PROFILER_THREAD("Thread_Gpu");
g_gpu_thread = true;
g_gpu_state = gpu;
for (;;) {
Submission submission;
@@ -529,6 +565,7 @@ void GpuState::ThreadRun(void* data) {
} else if (!gpu->m_commands.empty()) {
command = std::move(gpu->m_commands.front());
gpu->m_commands.pop_front();
EXIT_IF(gpu->m_pending_commands.fetch_sub(1, std::memory_order_acq_rel) == 0);
gpu->m_processing = true;
} else {
int selected_queue = -1;
@@ -560,6 +597,7 @@ void GpuState::ThreadRun(void* data) {
}
if (should_stop) {
gpu->m_gfx_cp->BufferWait();
g_gpu_state = nullptr;
g_gpu_thread = false;
return;
}
@@ -741,6 +779,9 @@ void CommandProcessor::SuspendPm4() {
void CommandProcessor::ProcessPm4(Pm4Execution& execution, size_t stop_depth) {
while (execution.m_buffer_stack.size() > stop_depth) {
if (g_gpu_state != nullptr) {
g_gpu_state->ProcessCommands();
}
const auto buffer_index = execution.m_buffer_stack.size() - 1;
auto& cursor = execution.m_buffer_stack[buffer_index];
if (cursor.deferred_advance_dw != 0) {
@@ -1650,10 +1691,6 @@ void Gpu::SendCommandSync(Common::UniqueFunction<void>&& command) {
m_state->SendCommandSync(std::move(command));
}
void Gpu::SendCommandSyncWithProcessor(Common::UniqueFunction<void, CommandProcessor&>&& command) {
m_state->SendCommandSyncWithProcessor(std::move(command));
}
void Gpu::Submit(uint32_t* draw_commands, uint32_t draw_size_dw, uint32_t* constant_commands,
uint32_t constant_size_dw, bool trigger_agc_interrupt_on_done) {
EXIT_IF(draw_commands == nullptr || draw_size_dw == 0);
@@ -1679,12 +1716,8 @@ int Gpu::GetFrameNum() const {
return m_state->GetFrameNum();
}
bool Gpu::IsCommandProcessorThread() noexcept {
return g_current_processor != nullptr;
}
CommandProcessor* Gpu::CurrentCommandProcessor() noexcept {
return g_current_processor;
bool Gpu::IsGpuThread() noexcept {
return GpuState::IsGpuThread();
}
} // namespace Libs::Graphics
+1 -4
View File
@@ -9,7 +9,6 @@
namespace Libs::Graphics {
class CommandProcessor;
class GpuState;
class RenderContext;
@@ -23,7 +22,6 @@ public:
[[nodiscard]] bool IsStopping();
void SendCommand(Common::UniqueFunction<void>&& command);
void SendCommandSync(Common::UniqueFunction<void>&& command);
void SendCommandSyncWithProcessor(Common::UniqueFunction<void, CommandProcessor&>&& command);
void Submit(uint32_t* draw_commands, uint32_t draw_size_dw, uint32_t* constant_commands,
uint32_t constant_size_dw, bool trigger_agc_interrupt_on_done = false);
@@ -33,8 +31,7 @@ public:
void Done();
[[nodiscard]] int GetFrameNum() const;
[[nodiscard]] static bool IsCommandProcessorThread() noexcept;
[[nodiscard]] static CommandProcessor* CurrentCommandProcessor() noexcept;
[[nodiscard]] static bool IsGpuThread() noexcept;
private:
std::unique_ptr<GpuState> m_state;
+1
View File
@@ -30,6 +30,7 @@ constexpr auto MakeRegisterNames() {
names[R_FLIP] = "R_FLIP";
names[R_RELEASE_MEM] = "R_RELEASE_MEM";
names[R_DMA_DATA] = "R_DMA_DATA";
names[R_CONTEXT_STATE] = "R_CONTEXT_STATE";
return names;
}
+1
View File
@@ -87,6 +87,7 @@ constexpr uint32_t R_WAIT_MEM_64 = 0x16;
constexpr uint32_t R_FLIP = 0x17;
constexpr uint32_t R_RELEASE_MEM = 0x18;
constexpr uint32_t R_DMA_DATA = 0x19;
constexpr uint32_t R_CONTEXT_STATE = 0x1A;
constexpr uint32_t R_NUM = 0x3F + 1;
+17 -33
View File
@@ -29,45 +29,29 @@ public:
void MarkRegionAsGpuModified(uint64_t vaddr, uint64_t size);
void UnmarkRegionAsGpuModified(uint64_t vaddr, uint64_t size);
void UntrackMemory(uint64_t vaddr, uint64_t size);
// Removes protection from a range and flushes GPU-owned data when required.
template <typename Flush>
void InvalidateRegion(uint64_t vaddr, uint64_t size, Flush&& on_flush) {
void InvalidateRegion(uint64_t vaddr, uint64_t size, Flush&& on_flush) noexcept {
static_assert(std::is_invocable_v<Flush&>);
CheckNotInUploadCallback();
ValidateRange(vaddr, size);
const auto update_cpu_state = [this, vaddr, size] {
std::lock_guard access(m_access_mutex);
std::vector<RegionManager*> managers;
Iterate<false>(vaddr, size, [&](RegionManager* manager, uint64_t, uint64_t) {
managers.push_back(manager);
});
std::vector<std::unique_lock<TrackingSpinLock>> locks;
locks.reserve(managers.size());
for (auto* manager: managers) {
locks.emplace_back(manager->lock);
Iterate<false>(vaddr, size, [&](RegionManager* manager, uint64_t offset, uint64_t bytes) {
const bool should_flush = [&] {
// Perform both the GPU modification check and CPU state change with the lock in
// case the GPU thread is racing to mark the page modified. If a flush is needed,
// on_flush performs the CPU state change.
std::scoped_lock lock(manager->lock);
if (manager->IsModified<DirtySource::Gpu>(offset, bytes)) {
return true;
}
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
return false;
}();
if (should_flush) {
on_flush();
}
const bool gpu_modified = Iterate<false>(
vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
return manager->IsModified<DirtySource::Gpu>(offset, bytes);
});
if (gpu_modified) {
return true;
}
Iterate<false>(vaddr, size,
[](RegionManager* manager, uint64_t offset, uint64_t bytes) {
manager->ChangeState<DirtySource::Cpu, true>(
manager->GetCpuAddr() + offset, bytes);
});
return false;
};
if (!update_cpu_state()) {
return;
}
std::forward<Flush>(on_flush)();
if (update_cpu_state()) {
EXIT("memory invalidation retained GPU-owned pages\n");
}
});
}
#if KYTY_BUILD == KYTY_BUILD_DEBUG
void ValidateGpuDirtyPages(const RangeSet& dirty, uint64_t vaddr, uint64_t size,
+37 -5
View File
@@ -3,15 +3,18 @@
#include "common/assert.h"
#include "common/logging/log.h"
#include "common/profiler.h"
#include "graphics/guest_gpu/graphicsRun.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/cache/resourceMutex.h"
#include "graphics/host_gpu/renderer/cache/textureCache.h"
#include "graphics/host_gpu/renderer/commandScheduler.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "kernel/memory.h"
#include <algorithm>
#include <array>
#include <cinttypes>
#include <cstring>
#include <utility>
#include <vector>
@@ -288,10 +291,29 @@ void BufferCache::InvalidateMemory(uint64_t vaddr, uint64_t size) {
return;
}
m_memory_tracker.InvalidateRegion(vaddr, size,
[this, vaddr, size] { ReadMemory(vaddr, size); });
[this, vaddr, size] { ReadMemory(vaddr, size, true); });
}
void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size) {
void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size, bool is_write) {
if (Gpu::IsGpuThread()) {
ReadMemoryOnGpu(vaddr, size, is_write);
return;
}
if (CommandScheduler::InDeferredOperation()) {
EXIT("unsupported buffer readback from an asynchronous GPU completion, "
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
vaddr, size);
}
if (m_resource_mutex.IsOwnedByCurrentThread()) {
EXIT("unsupported buffer readback from a pre-owned resource transaction, "
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
vaddr, size);
}
m_scheduler.Context().GetGpu().SendCommandSync(
[this, vaddr, size, is_write] { ReadMemoryOnGpu(vaddr, size, is_write); });
}
void BufferCache::ReadMemoryOnGpu(uint64_t vaddr, uint64_t size, bool is_write) {
std::vector<DownloadCopy> copies;
{
FaultSafeCacheLock lock(this, m_mutex);
@@ -322,9 +344,16 @@ void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size) {
}
}
});
}
if (copies.empty()) {
return;
if (copies.empty()) {
if (!is_write) {
return;
}
// A preceding read fault can consume the last GPU-owned copy after this write
// invalidation has already chosen to flush. Complete the CPU ownership handoff even
// though this callback no longer has bytes to download.
m_memory_tracker.MarkRegionAsCpuModified(vaddr, size);
return;
}
}
auto downloads = RecordDownloads(copies);
m_scheduler.FinishCurrent();
@@ -336,6 +365,9 @@ void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size) {
}
// The enumeration above covered whole dirty pages and every exact interval on them.
m_memory_tracker.UnmarkRegionAsGpuModified(vaddr, size);
if (is_write) {
m_memory_tracker.MarkRegionAsCpuModified(vaddr, size);
}
}
}
+2 -1
View File
@@ -46,7 +46,7 @@ public:
KYTY_CLASS_NO_COPY(BufferCache);
void InvalidateMemory(uint64_t vaddr, uint64_t size);
void ReadMemory(uint64_t vaddr, uint64_t size);
void ReadMemory(uint64_t vaddr, uint64_t size, bool is_write = false);
void UnmapMemory(uint64_t vaddr, uint64_t size);
[[nodiscard]] BufferBinding ObtainBuffer(CommandBuffer& command, uint64_t vaddr, uint64_t size,
bool is_written = false, bool is_read = true,
@@ -97,6 +97,7 @@ private:
void PublishDownloads(std::span<const DownloadRange> downloads);
void QueueGarbageDownload(std::span<const DownloadCopy> copies, RetiredBuffer retire);
void WriteHostMemory(uint64_t vaddr, std::span<const uint8_t> data);
void ReadMemoryOnGpu(uint64_t vaddr, uint64_t size, bool is_write);
GraphicContext& m_graphics;
CommandScheduler& m_scheduler;
+8 -57
View File
@@ -1,7 +1,6 @@
#include "graphics/host_gpu/renderer/cache/gpuResourceManager.h"
#include "common/assert.h"
#include "graphics/guest_gpu/command_processor/commandProcessor.h"
#include "graphics/guest_gpu/graphicsRun.h"
#include "graphics/host_gpu/renderer/commandScheduler.h"
namespace Libs::Graphics {
@@ -18,69 +17,21 @@ bool GpuResourceManager::HandleFault(PageFaultAccess access, uint64_t fault_vadd
if (!IsMapped(fault_vaddr, fault_size)) {
return false;
}
if (CommandScheduler::InDeferredOperation()) {
EXIT("unsupported guest-memory fault from an asynchronous GPU completion, "
"addr=0x%016" PRIx64 " access=%u\n",
fault_vaddr, static_cast<uint32_t>(access));
if (access == PageFaultAccess::Write) {
m_buffer_cache.InvalidateMemory(fault_vaddr, fault_size);
m_texture_cache.InvalidateMemory(fault_vaddr, fault_size);
} else {
m_buffer_cache.ReadMemory(fault_vaddr, fault_size);
}
bool handled = false;
const auto resolve = [this, access, fault_vaddr, &handled](CommandProcessor& cp) {
cp.BeginReadbackTransaction();
{
ResourceMutex::FaultScope fault(m_resource_mutex);
if (access == PageFaultAccess::Write) {
m_buffer_cache.InvalidateMemory(fault_vaddr, fault_size);
m_texture_cache.InvalidateMemory(fault_vaddr, fault_size);
} else {
m_buffer_cache.ReadMemory(fault_vaddr, fault_size);
}
handled = true;
}
cp.EndReadbackTransaction();
};
if (auto* cp = Gpu::CurrentCommandProcessor(); cp != nullptr) {
resolve(*cp);
return handled;
}
if (m_resource_mutex.IsOwnedByCurrentThread()) {
EXIT("unsupported page fault from a pre-owned resource transaction, addr=0x%016" PRIx64
" access=%u\n",
fault_vaddr, static_cast<uint32_t>(access));
}
EXIT_IF(m_gpu == nullptr);
m_gpu->SendCommandSyncWithProcessor(resolve);
return handled;
return true;
}
bool GpuResourceManager::InvalidateMemory(uint64_t vaddr, uint64_t size) {
if (!IsMapped(vaddr, size)) {
return false;
}
if (CommandScheduler::InDeferredOperation()) {
EXIT("unsupported memory invalidation from an asynchronous GPU completion, "
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
vaddr, size);
}
const auto resolve = [this, vaddr, size](CommandProcessor& cp) {
cp.BeginReadbackTransaction();
{
ResourceMutex::FaultScope fault(m_resource_mutex);
m_buffer_cache.InvalidateMemory(vaddr, size);
m_texture_cache.InvalidateMemory(vaddr, size);
}
cp.EndReadbackTransaction();
};
if (auto* cp = Gpu::CurrentCommandProcessor(); cp != nullptr) {
resolve(*cp);
return true;
}
if (m_resource_mutex.IsOwnedByCurrentThread()) {
EXIT("unsupported memory invalidation from a pre-owned resource transaction, "
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
vaddr, size);
}
EXIT_IF(m_gpu == nullptr);
m_gpu->SendCommandSyncWithProcessor(resolve);
m_buffer_cache.InvalidateMemory(vaddr, size);
m_texture_cache.InvalidateMemory(vaddr, size);
return true;
}
+2 -48
View File
@@ -4,18 +4,10 @@
namespace Libs::Graphics {
ResourceMutex::FaultScope::FaultScope(ResourceMutex& mutex): m_mutex(mutex) {
m_resource_preowned = m_mutex.BeginFault();
}
ResourceMutex::FaultScope::~FaultScope() {
m_mutex.EndFault(m_resource_preowned);
}
ResourceMutex::~ResourceMutex() {
std::lock_guard state(m_state);
if (m_resource_owner != std::thread::id {} || m_fault_owner != std::thread::id {}) {
EXIT("ResourceMutex destroyed with an active owner or fault transaction\n");
if (m_resource_owner != std::thread::id {}) {
EXIT("ResourceMutex destroyed with an active owner\n");
}
}
@@ -26,9 +18,6 @@ void ResourceMutex::lock() {
if (m_resource_owner == current) {
EXIT("recursive resource transaction\n");
}
if (m_fault_owner == current) {
EXIT("resource transaction re-entered from a page-fault callback\n");
}
}
m_resource.lock();
std::lock_guard state(m_state);
@@ -55,39 +44,4 @@ bool ResourceMutex::IsOwnedByCurrentThread() {
return m_resource_owner == std::this_thread::get_id();
}
bool ResourceMutex::BeginFault() {
const auto current = std::this_thread::get_id();
{
std::lock_guard state(m_state);
if (m_fault_owner == current) {
EXIT("nested resource page-fault transaction\n");
}
if (m_resource_owner == current) {
m_fault_owner = current;
return true;
}
}
lock();
std::lock_guard state(m_state);
if (m_resource_owner != current || m_fault_owner != std::thread::id {}) {
EXIT("resource fault transaction acquired inconsistent ownership\n");
}
m_fault_owner = current;
return false;
}
void ResourceMutex::EndFault(bool resource_preowned) {
const auto current = std::this_thread::get_id();
{
std::lock_guard state(m_state);
if (m_fault_owner != current || m_resource_owner != current) {
EXIT("resource page-fault transaction ended with inconsistent ownership\n");
}
m_fault_owner = {};
}
if (!resource_preowned) {
unlock();
}
}
} // namespace Libs::Graphics
+1 -19
View File
@@ -8,21 +8,9 @@
namespace Libs::Graphics {
// Owner-tracked shared buffer/image transaction. External faults pause GPU submissions first;
// command-processor faults drain pending guest processors before entering this transaction.
// Owner-tracked shared buffer/image transaction.
class ResourceMutex final {
public:
class FaultScope final {
public:
explicit FaultScope(ResourceMutex& mutex);
~FaultScope();
KYTY_CLASS_NO_COPY(FaultScope);
private:
ResourceMutex& m_mutex;
bool m_resource_preowned = false;
};
ResourceMutex() = default;
~ResourceMutex();
KYTY_CLASS_NO_COPY(ResourceMutex);
@@ -32,15 +20,9 @@ public:
[[nodiscard]] bool IsOwnedByCurrentThread();
private:
friend class FaultScope;
[[nodiscard]] bool BeginFault();
void EndFault(bool resource_preowned);
std::mutex m_resource;
std::mutex m_state;
std::thread::id m_resource_owner;
std::thread::id m_fault_owner;
};
} // namespace Libs::Graphics
@@ -22,6 +22,10 @@ struct RenderTargetFormatMapping {
};
constexpr RenderTargetFormatMapping kRenderTargetFormats[] = {
{Prospero::ChannelLayout::k8,
Prospero::ChannelType::kUInt,
Prospero::ChannelOrder::kStandard,
{vk::Format::eR8Uint, 1}},
{Prospero::ChannelLayout::k8_8,
Prospero::ChannelType::kUNorm,
Prospero::ChannelOrder::kStandard,
@@ -379,6 +379,10 @@ static bool IsSupportedStorageTextureDescriptor(const ShaderRecompiler::IR::Imag
tile == Prospero::GpuEnumValue(Prospero::TileMode::kDepth) && !resource.read &&
!Prospero::IsFmaskTextureFormat(descriptor.Format()) && (is_2d || is_2d_array) &&
TileGetBlockLayout(TileBlockFamily::Depth64KB, depth_bpe, depth_block);
const bool supported_standard_256b =
(is_2d || is_2d_array) &&
tile == Prospero::GpuEnumValue(Prospero::TileMode::kStandard256B) &&
TileIsStandard256BTextureSupported(descriptor.Format());
const bool supported_standard_tile =
(tile == Prospero::GpuEnumValue(Prospero::TileMode::kStandard4KB) &&
TileIsStandard4KBTextureSupported(descriptor.Format())) ||
@@ -386,7 +390,8 @@ static bool IsSupportedStorageTextureDescriptor(const ShaderRecompiler::IR::Imag
TileIsStandard64KBTextureSupported(descriptor.Format()));
const bool supported_tile = tile == Prospero::GpuEnumValue(Prospero::TileMode::kLinear) ||
tile == Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget) ||
supported_depth_tile || supported_standard_tile;
supported_depth_tile || supported_standard_256b ||
supported_standard_tile;
const auto swizzle = descriptor.DstSelXYZW();
const bool supported_swizzle =
IsValidImageSwizzle(swizzle) &&
+9 -32
View File
@@ -664,12 +664,10 @@ struct DrawIndexBufferSource {
struct PreparedIndexBuffer {
std::shared_ptr<void> owner;
vk::Buffer buffer = nullptr;
uint64_t address = 0;
uint64_t size = 0;
vk::DeviceSize offset = 0;
vk::IndexType type = vk::IndexType::eUint16;
bool streamed = false;
vk::Buffer buffer = nullptr;
uint64_t size = 0;
vk::DeviceSize offset = 0;
vk::IndexType type = vk::IndexType::eUint16;
};
static uint64_t VertexBufferDescriptorSize(const ShaderVertexInputBuffer& buffer) {
@@ -896,12 +894,6 @@ static std::vector<BufferBinding> PrepareVertexBuffers(uint64_t
return AcquireVertexBuffers(buffer, vs_input_info);
}
static void RebindVertexBuffers(RenderCommandBuffer& buffer,
const ShaderVertexInputInfo& vs_input_info,
std::vector<BufferBinding>& bindings) {
bindings = AcquireVertexBuffers(buffer, vs_input_info);
}
static PreparedIndexBuffer PrepareIndexBuffer(RenderCommandBuffer& buffer,
const DrawIndexBufferSource& source) {
PreparedIndexBuffer prepared;
@@ -909,16 +901,14 @@ static PreparedIndexBuffer PrepareIndexBuffer(RenderCommandBuffer& buffe
return prepared;
}
EXIT_IF(source.size == 0);
prepared.address = source.address;
prepared.size = source.size;
prepared.type = source.type;
prepared.size = source.size;
prepared.type = source.type;
if (source.host_data != nullptr) {
auto binding =
buffer.GetContext().GetBufferCache().UploadTransient(source.host_data, source.size, 16);
prepared.owner = std::move(binding.owner);
prepared.buffer = binding.buffer;
prepared.offset = binding.offset;
prepared.streamed = true;
prepared.owner = std::move(binding.owner);
prepared.buffer = binding.buffer;
prepared.offset = binding.offset;
} else {
auto binding =
buffer.GetContext().GetBufferCache().ObtainBuffer(buffer, source.address, source.size);
@@ -929,17 +919,6 @@ static PreparedIndexBuffer PrepareIndexBuffer(RenderCommandBuffer& buffe
return prepared;
}
static void RebindIndexBuffer(RenderCommandBuffer& buffer, PreparedIndexBuffer& prepared) {
if (prepared.size == 0 || prepared.streamed) {
return;
}
auto binding =
buffer.GetContext().GetBufferCache().ObtainBuffer(buffer, prepared.address, prepared.size);
prepared.owner = std::move(binding.owner);
prepared.buffer = binding.buffer;
prepared.offset = binding.offset;
}
static void CommitVertexBuffers(RenderCommandBuffer& buffer, vk::CommandBuffer vk_buffer,
std::vector<BufferBinding>& bindings) {
for (uint32_t slot = 0; slot < bindings.size(); slot++) {
@@ -1046,8 +1025,6 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, RenderCommandBuffer
state.ps_input_info.stage, state.ps_active);
auto vertex_bindings = PrepareVertexBuffers(submit_id, buffer, draw, state.vs_input_info);
auto index_binding = PrepareIndexBuffer(buffer, index_source);
RebindVertexBuffers(buffer, state.vs_input_info, vertex_bindings);
RebindIndexBuffer(buffer, index_binding);
state.rendering =
AcquireRenderTargets(buffer, state.color_info, state.color_count, state.depth_info);
@@ -0,0 +1,876 @@
#include "graphics/presentation/imeDialogOverlay.h"
#include "SDL.h"
#include "common/assert.h"
#include "common/stringUtils.h"
#include "graphics/host_gpu/graphicContext.h"
#include "imgui.h"
#include "imgui_impl_vulkan.h"
#include "libs/controller.h"
#include "libs/imeDialog.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <deque>
#include <mutex>
#include <span>
#include <string>
#include <string_view>
#include <utility>
namespace Libs::Graphics {
namespace {
namespace Ime = Libs::Dialog::ImeDialog;
constexpr size_t INPUT_QUEUE_CAPACITY = 128;
enum class InputKind : uint8_t {
Button,
Axis,
MousePosition,
MouseButton,
MouseWheel,
ResetController
};
struct InputEvent {
InputKind kind;
uint64_t generation;
int id;
float x;
float y;
};
struct VisibilityUpdate {
uint64_t generation;
bool visible;
bool capture_controller;
bool capture_keyboard;
bool multiline;
};
std::atomic<Uint32> g_visibility_event {static_cast<Uint32>(-1)};
std::mutex g_visibility_mutex;
std::mutex g_input_mutex;
std::deque<InputEvent> g_input_events;
std::deque<VisibilityUpdate> g_visibility_updates;
size_t g_missing_visibility_wakeups = 0;
bool g_input_reset_requested = false;
uint16_t g_last_external_keycode = 0;
uint32_t g_last_external_status = 0;
uint64_t g_input_generation = 0;
bool g_input_active = false;
bool g_input_controller = false;
bool g_input_keyboard = false;
bool g_input_multiline = false;
bool g_input_lifecycle_active = false;
bool g_active = false;
bool g_controller_captured = false;
uint64_t g_generation = 0;
void ClearInputEvents() {
std::scoped_lock lock(g_input_mutex);
g_input_events.clear();
g_input_reset_requested = false;
}
void QueueInput(InputEvent event) {
std::scoped_lock lock(g_input_mutex);
const bool replaceable =
event.kind == InputKind::Axis || event.kind == InputKind::MousePosition;
if (replaceable && !g_input_events.empty()) {
auto& last = g_input_events.back();
if (last.generation == event.generation && last.kind == event.kind && last.id == event.id) {
last = event;
return;
}
}
if (g_input_events.size() == INPUT_QUEUE_CAPACITY) {
g_input_events.clear();
g_input_reset_requested = true;
}
g_input_events.push_back(event);
}
void RetryVisibilityWakeup() {
const Uint32 type = g_visibility_event.load(std::memory_order_acquire);
if (type == static_cast<Uint32>(-1)) {
return;
}
std::scoped_lock lock(g_input_mutex);
if (g_missing_visibility_wakeups == 0) {
return;
}
SDL_Event event {};
event.type = type;
if (SDL_PushEvent(&event) > 0) {
g_missing_visibility_wakeups--;
}
}
void OnVisibilityChanged(bool visible, uint64_t generation) {
std::scoped_lock visibility_lock(g_visibility_mutex);
if (!g_input_lifecycle_active) {
return;
}
if (generation < g_generation || (generation == g_generation && visible == g_active)) {
return;
}
g_generation = generation;
bool capture_controller = false;
bool capture_keyboard = false;
bool multiline = false;
if (visible) {
Ime::HostSnapshot snapshot;
if (Ime::GetHostSnapshot(&snapshot) && snapshot.generation == generation) {
capture_controller = (snapshot.disable_device & Ime::DISABLE_DEVICE_CONTROLLER) == 0;
capture_keyboard = (snapshot.disable_device & Ime::DISABLE_DEVICE_EXT_KEYBOARD) == 0;
multiline = (snapshot.option & Ime::OPTION_MULTILINE) != 0;
} else {
visible = false;
}
}
const bool was_controller = std::exchange(g_controller_captured, capture_controller);
if (capture_controller || was_controller) {
Controller::ControllerResetInputState();
}
g_active = visible;
const Uint32 type = g_visibility_event.load(std::memory_order_acquire);
if (type != static_cast<Uint32>(-1)) {
SDL_Event event {};
event.type = type;
std::scoped_lock input_lock(g_input_mutex);
g_visibility_updates.push_back(
{generation, visible, capture_controller, capture_keyboard, multiline});
if (g_missing_visibility_wakeups != 0 || SDL_PushEvent(&event) <= 0) {
g_missing_visibility_wakeups++;
}
}
}
uint32_t ExternalKeyStatus(SDL_Keymod modifiers, bool character_valid) {
uint32_t status = 0x00000001 | (character_valid ? 0x00000002 : 0);
if ((modifiers & KMOD_LCTRL) != 0) status |= 0x00000100;
if ((modifiers & KMOD_LSHIFT) != 0) status |= 0x00000200;
if ((modifiers & KMOD_LALT) != 0) status |= 0x00000400;
if ((modifiers & KMOD_LGUI) != 0) status |= 0x00000800;
if ((modifiers & KMOD_RCTRL) != 0) status |= 0x00001000;
if ((modifiers & KMOD_RSHIFT) != 0) status |= 0x00002000;
if ((modifiers & KMOD_RALT) != 0) status |= 0x00004000;
if ((modifiers & KMOD_RGUI) != 0) status |= 0x00008000;
if ((modifiers & KMOD_NUM) != 0) status |= 0x00010000;
if ((modifiers & KMOD_CAPS) != 0) status |= 0x00020000;
return status;
}
Ime::ExternalInput MakeExternalInput(Ime::ExternalAction action, uint16_t keycode,
uint32_t status) {
Ime::ExternalInput input {};
input.key.keycode = keycode;
input.key.status = status;
input.key.type = 4;
input.action = action;
return input;
}
std::u16string Utf8ToUtf16(std::string_view text) {
std::u16string result;
result.reserve(text.size());
for (size_t i = 0; i < text.size();) {
const auto first = static_cast<uint8_t>(text[i++]);
uint32_t codepoint = 0;
uint32_t remaining = 0;
if (first < 0x80) {
codepoint = first;
} else if ((first & 0xe0) == 0xc0) {
codepoint = first & 0x1f;
remaining = 1;
} else if ((first & 0xf0) == 0xe0) {
codepoint = first & 0x0f;
remaining = 2;
} else if ((first & 0xf8) == 0xf0) {
codepoint = first & 0x07;
remaining = 3;
} else {
continue;
}
if (i + remaining > text.size()) {
break;
}
bool valid = true;
for (uint32_t j = 0; j < remaining; j++) {
const auto next = static_cast<uint8_t>(text[i++]);
if ((next & 0xc0) != 0x80) {
valid = false;
break;
}
codepoint = (codepoint << 6) | (next & 0x3f);
}
if (!valid || codepoint > 0x10ffff || (codepoint >= 0xd800 && codepoint <= 0xdfff)) {
continue;
}
if (codepoint <= 0xffff) {
result.push_back(static_cast<char16_t>(codepoint));
} else {
codepoint -= 0x10000;
result.push_back(static_cast<char16_t>(0xd800 + (codepoint >> 10)));
result.push_back(static_cast<char16_t>(0xdc00 + (codepoint & 0x3ff)));
}
}
return result;
}
std::string VisibleText(const Ime::HostSnapshot& snapshot, size_t max_units) {
const size_t cursor = std::min<size_t>(snapshot.cursor, snapshot.text.size());
const size_t payload_units = max_units > 4 ? max_units - 4 : 1;
size_t begin = cursor > payload_units / 2 ? cursor - payload_units / 2 : 0;
size_t end = std::min(snapshot.text.size(), begin + payload_units);
if (end == snapshot.text.size() && end - begin < payload_units) {
begin = end > payload_units ? end - payload_units : 0;
}
if (begin > 0 && snapshot.text[begin] >= 0xdc00 && snapshot.text[begin] <= 0xdfff) {
begin--;
}
if (end < snapshot.text.size() && end > begin && snapshot.text[end - 1] >= 0xd800 &&
snapshot.text[end - 1] <= 0xdbff) {
end--;
}
std::u16string visible;
if (begin != 0) {
visible.push_back(u'\u2026');
}
const size_t caret = visible.size() + cursor - begin;
if ((snapshot.option & Ime::OPTION_PASSWORD) != 0) {
visible.append(end - begin, u'*');
} else {
visible.append(snapshot.text, begin, end - begin);
std::replace(visible.begin(), visible.end(), u'\n', u'\u21b5');
std::replace(visible.begin(), visible.end(), u'\r', u'\u21b5');
}
visible.insert(std::min(caret, visible.size()), 1, u'|');
if (end != snapshot.text.size()) {
visible.push_back(u'\u2026');
}
return Common::Utf16ToUtf8(visible.c_str());
}
const char* EnterLabel(Ime::EnterLabel label) {
switch (label) {
case Ime::EnterLabel::Send: return "Send";
case Ime::EnterLabel::Search: return "Search";
case Ime::EnterLabel::Go: return "Go";
default: return "Done";
}
}
float AlignmentPivot(Ime::Alignment alignment) {
switch (alignment) {
case Ime::Alignment::Start: return 0.0f;
case Ime::Alignment::End: return 1.0f;
default: return 0.5f;
}
}
ImGuiKey ControllerButtonToKey(int button) {
switch (button) {
case SDL_CONTROLLER_BUTTON_A: return ImGuiKey_GamepadFaceDown;
case SDL_CONTROLLER_BUTTON_B: return ImGuiKey_GamepadFaceRight;
case SDL_CONTROLLER_BUTTON_X: return ImGuiKey_GamepadFaceLeft;
case SDL_CONTROLLER_BUTTON_Y: return ImGuiKey_GamepadFaceUp;
case SDL_CONTROLLER_BUTTON_DPAD_LEFT: return ImGuiKey_GamepadDpadLeft;
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: return ImGuiKey_GamepadDpadRight;
case SDL_CONTROLLER_BUTTON_DPAD_UP: return ImGuiKey_GamepadDpadUp;
case SDL_CONTROLLER_BUTTON_DPAD_DOWN: return ImGuiKey_GamepadDpadDown;
default: return ImGuiKey_None;
}
}
PFN_vkVoidFunction LoadVulkanFunction(const char* name, void* user_data) {
auto& graphics = *static_cast<GraphicContext*>(user_data);
return graphics.instance.getProcAddr(name);
}
void CheckVulkanResult(VkResult result) {
EXIT_IF(result != VK_SUCCESS);
}
} // namespace
void InitializeImeDialogInput() {
const Uint32 type = SDL_RegisterEvents(1);
EXIT_IF(type == static_cast<Uint32>(-1));
{
std::scoped_lock lock(g_visibility_mutex);
g_input_lifecycle_active = true;
g_visibility_event.store(type, std::memory_order_release);
}
Ime::SetVisibilityCallback(OnVisibilityChanged);
Ime::HostSnapshot snapshot;
if (Ime::GetHostSnapshot(&snapshot)) {
OnVisibilityChanged(true, snapshot.generation);
}
}
void ShutdownImeDialogInput() {
Ime::SetVisibilityCallback(nullptr);
{
std::scoped_lock lock(g_visibility_mutex);
g_input_lifecycle_active = false;
g_active = false;
if (std::exchange(g_controller_captured, false)) {
Controller::ControllerResetInputState();
}
g_visibility_event.store(static_cast<Uint32>(-1), std::memory_order_release);
}
ClearInputEvents();
{
std::scoped_lock lock(g_input_mutex);
g_visibility_updates.clear();
g_missing_visibility_wakeups = 0;
}
g_input_active = false;
g_input_controller = false;
g_input_keyboard = false;
g_input_multiline = false;
if (SDL_IsTextInputActive() == SDL_TRUE) {
SDL_StopTextInput();
}
}
bool ProcessImeDialogInput(const SDL_Event& event) {
RetryVisibilityWakeup();
if (event.type == g_visibility_event.load(std::memory_order_acquire)) {
VisibilityUpdate update {};
{
std::scoped_lock lock(g_input_mutex);
if (g_visibility_updates.empty()) {
return true;
}
update = g_visibility_updates.front();
g_visibility_updates.pop_front();
}
ClearInputEvents();
g_last_external_keycode = 0;
g_last_external_status = 0;
g_input_generation = update.generation;
g_input_active = update.visible;
g_input_controller = update.capture_controller;
g_input_keyboard = update.capture_keyboard;
g_input_multiline = update.multiline;
if (g_input_keyboard) {
SDL_StartTextInput();
} else if (SDL_IsTextInputActive() == SDL_TRUE) {
SDL_StopTextInput();
}
return true;
}
if (event.type == SDL_CONTROLLERDEVICEREMOVED) {
if (g_input_active && g_input_controller) {
QueueInput({InputKind::ResetController, g_input_generation, 0, 0.0f, 0.0f});
}
return false;
}
if (!g_input_active) {
return false;
}
const uint64_t generation = g_input_generation;
const bool controller_event = event.type == SDL_CONTROLLERBUTTONDOWN ||
event.type == SDL_CONTROLLERBUTTONUP ||
event.type == SDL_CONTROLLERAXISMOTION;
if (controller_event && !g_input_controller) {
return false;
}
const bool keyboard_event = event.type == SDL_TEXTINPUT || event.type == SDL_TEXTEDITING ||
event.type == SDL_KEYDOWN || event.type == SDL_KEYUP;
if (keyboard_event && !g_input_keyboard) {
return false;
}
switch (event.type) {
case SDL_TEXTINPUT: {
const auto text = Utf8ToUtf16(event.text.text);
if (!text.empty()) {
auto input = MakeExternalInput(Ime::ExternalAction::Text, g_last_external_keycode,
g_last_external_status | 0x00000002);
input.key.character = text.front();
input.text = text;
Ime::HostQueueExternalInput(generation, std::move(input));
}
return true;
}
case SDL_TEXTEDITING: return true;
case SDL_KEYDOWN: {
g_last_external_keycode = static_cast<uint16_t>(event.key.keysym.scancode);
g_last_external_status =
ExternalKeyStatus(static_cast<SDL_Keymod>(event.key.keysym.mod), false);
auto action = Ime::ExternalAction::Text;
bool queue = true;
if (event.key.keysym.sym == SDLK_BACKSPACE) {
action = Ime::ExternalAction::Backspace;
} else if (event.key.keysym.sym == SDLK_LEFT) {
action = Ime::ExternalAction::MoveLeft;
} else if (event.key.keysym.sym == SDLK_RIGHT) {
action = Ime::ExternalAction::MoveRight;
} else if (event.key.keysym.sym == SDLK_ESCAPE) {
action = Ime::ExternalAction::Cancel;
} else if (event.key.keysym.sym == SDLK_RETURN ||
event.key.keysym.sym == SDLK_KP_ENTER) {
action =
g_input_multiline ? Ime::ExternalAction::Newline : Ime::ExternalAction::Accept;
} else if (event.key.keysym.sym == SDLK_TAB) {
action = Ime::ExternalAction::None;
} else {
queue = false;
}
if (queue) {
Ime::HostQueueExternalInput(
generation,
MakeExternalInput(action, g_last_external_keycode, g_last_external_status));
}
return true;
}
case SDL_KEYUP: return true;
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
QueueInput({InputKind::Button, generation, event.cbutton.button,
event.type == SDL_CONTROLLERBUTTONDOWN ? 1.0f : 0.0f, 0.0f});
return true;
case SDL_CONTROLLERAXISMOTION:
QueueInput({InputKind::Axis, generation, event.caxis.axis,
static_cast<float>(event.caxis.value), 0.0f});
return true;
case SDL_MOUSEMOTION:
QueueInput({InputKind::MousePosition, generation, 0, static_cast<float>(event.motion.x),
static_cast<float>(event.motion.y)});
return true;
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
QueueInput({InputKind::MouseButton, generation, event.button.button,
event.type == SDL_MOUSEBUTTONDOWN ? 1.0f : 0.0f, 0.0f});
return true;
case SDL_MOUSEWHEEL:
QueueInput({InputKind::MouseWheel, generation, 0, static_cast<float>(event.wheel.x),
static_cast<float>(event.wheel.y)});
return true;
default: return false;
}
}
struct ImeDialogOverlay::Impl {
explicit Impl(GraphicContext& context): graphics(context) {}
~Impl() {
ReleaseVulkan();
if (imgui_context != nullptr) {
ImGui::DestroyContext(imgui_context);
}
}
void EnsureContext() {
if (imgui_context != nullptr) {
ImGui::SetCurrentContext(imgui_context);
return;
}
IMGUI_CHECKVERSION();
imgui_context = ImGui::CreateContext();
ImGui::SetCurrentContext(imgui_context);
auto& io = ImGui::GetIO();
io.IniFilename = nullptr;
io.LogFilename = nullptr;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
io.BackendPlatformName = "Kyty ImeDialog input";
ImGui::StyleColorsDark();
auto& style = ImGui::GetStyle();
style.WindowRounding = 10.0f;
style.FrameRounding = 6.0f;
style.ItemSpacing = {8.0f, 8.0f};
}
void EnsureVulkan(vk::Format format, uint32_t image_count) {
EnsureContext();
if (vulkan_initialized) {
return;
}
EXIT_IF(image_count < 2);
EXIT_IF(!ImGui_ImplVulkan_LoadFunctions(VK_API_VERSION_1_3, LoadVulkanFunction, &graphics));
const VkFormat color_format = static_cast<VkFormat>(format);
ImGui_ImplVulkan_InitInfo info {};
info.ApiVersion = VK_API_VERSION_1_3;
info.Instance = static_cast<VkInstance>(graphics.instance);
info.PhysicalDevice = static_cast<VkPhysicalDevice>(graphics.physical_device);
info.Device = static_cast<VkDevice>(graphics.device);
info.QueueFamily = graphics.queue_family;
info.Queue = static_cast<VkQueue>(graphics.queue);
info.DescriptorPoolSize = IMGUI_IMPL_VULKAN_MINIMUM_SAMPLED_IMAGE_POOL_SIZE;
info.MinImageCount = image_count;
info.ImageCount = image_count;
info.UseDynamicRendering = true;
info.PipelineInfoMain.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
info.PipelineInfoMain.PipelineRenderingCreateInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
info.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount = 1;
info.PipelineInfoMain.PipelineRenderingCreateInfo.pColorAttachmentFormats = &color_format;
info.CheckVkResultFn = CheckVulkanResult;
EXIT_IF(!ImGui_ImplVulkan_Init(&info));
vulkan_initialized = true;
}
void DrainInput(uint64_t generation) {
std::deque<InputEvent> events;
bool reset = false;
{
std::scoped_lock lock(g_input_mutex);
events.swap(g_input_events);
reset = g_input_reset_requested;
g_input_reset_requested = false;
}
auto& io = ImGui::GetIO();
if (reset) {
io.ClearEventsQueue();
io.ClearInputKeys();
io.ClearInputMouse();
right_stick = {};
}
for (const auto& event: events) {
if (event.generation != generation) {
continue;
}
switch (event.kind) {
case InputKind::Button: {
const bool down = event.x != 0.0f;
if (down && event.id == SDL_CONTROLLER_BUTTON_B) {
Ime::HostCancel(generation);
} else if (down && event.id == SDL_CONTROLLER_BUTTON_Y) {
Ime::HostBackspace(generation);
}
const ImGuiKey key = ControllerButtonToKey(event.id);
if (key != ImGuiKey_None) {
io.AddKeyEvent(key, down);
}
break;
}
case InputKind::Axis: {
const float value = event.x / (event.x < 0.0f ? 32768.0f : 32767.0f);
if (event.id == SDL_CONTROLLER_AXIS_LEFTX) {
io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickLeft, value < -0.25f,
std::max(-value, 0.0f));
io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickRight, value > 0.25f,
std::max(value, 0.0f));
} else if (event.id == SDL_CONTROLLER_AXIS_LEFTY) {
io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickUp, value < -0.25f,
std::max(-value, 0.0f));
io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickDown, value > 0.25f,
std::max(value, 0.0f));
} else if (event.id == SDL_CONTROLLER_AXIS_RIGHTX) {
right_stick.x = std::abs(value) > 0.2f ? value : 0.0f;
} else if (event.id == SDL_CONTROLLER_AXIS_RIGHTY) {
right_stick.y = std::abs(value) > 0.2f ? value : 0.0f;
}
break;
}
case InputKind::MousePosition: io.AddMousePosEvent(event.x, event.y); break;
case InputKind::MouseButton: {
int button = -1;
if (event.id == SDL_BUTTON_LEFT) button = 0;
if (event.id == SDL_BUTTON_RIGHT) button = 1;
if (event.id == SDL_BUTTON_MIDDLE) button = 2;
if (button >= 0) io.AddMouseButtonEvent(button, event.x != 0.0f);
break;
}
case InputKind::MouseWheel: io.AddMouseWheelEvent(event.x, event.y); break;
case InputKind::ResetController:
io.ClearEventsQueue();
io.ClearInputKeys();
right_stick = {};
break;
}
}
}
void KeyButton(std::string_view label, char16_t value, uint64_t generation, float width,
bool default_focus) {
const bool pressed = ImGui::Button(label.data(), {width, button_height});
if (default_focus) {
ImGui::SetItemDefaultFocus();
}
if (pressed) {
Ime::HostInsertText(generation, std::u16string_view(&value, 1));
}
}
void DrawKeyRows(const Ime::HostSnapshot& snapshot, float width) {
static constexpr std::array<std::string_view, 4> lower = {"1234567890", "qwertyuiop",
"asdfghjkl", "zxcvbnm"};
static constexpr std::array<std::string_view, 4> upper = {"1234567890", "QWERTYUIOP",
"ASDFGHJKL", "ZXCVBNM"};
static constexpr std::array<std::string_view, 3> symbols = {"1234567890", "!@#$%^&*()",
"-_=+/:?."};
static constexpr std::array<std::string_view, 4> number = {"123", "456", "789", "-0."};
std::span<const std::string_view> rows;
if (snapshot.type == Ime::Type::Number) {
rows = number;
} else if (symbol_mode) {
rows = symbols;
} else {
rows = shift ? std::span<const std::string_view>(upper)
: std::span<const std::string_view>(lower);
}
bool first = focus_pending;
size_t row_index = 0;
for (const auto row: rows) {
const float key_width =
std::min(56.0f * ui_scale, (width - 8.0f * (row.size() - 1)) / row.size());
const float row_width = key_width * row.size() + 8.0f * (row.size() - 1);
ImGui::SetCursorPosX((width - row_width) * 0.5f);
ImGui::PushID(static_cast<int>(row_index++));
for (size_t i = 0; i < row.size(); i++) {
ImGui::PushID(static_cast<int>(i));
const char label[2] = {row[i], '\0'};
KeyButton(label, static_cast<char16_t>(row[i]), snapshot.generation, key_width,
first);
first = false;
ImGui::PopID();
if (i + 1 < row.size()) ImGui::SameLine();
}
ImGui::PopID();
}
focus_pending = false;
}
void DrawDialog(const Ime::HostSnapshot& snapshot, vk::Extent2D extent) {
const ImVec2 display(static_cast<float>(extent.width), static_cast<float>(extent.height));
const bool over_2k = (snapshot.option & Ime::OPTION_USE_OVER_2K) != 0;
const float reference_width = over_2k ? 3840.0f : 1920.0f;
const float reference_height = over_2k ? 2160.0f : 1080.0f;
const float scale_x = display.x / reference_width;
const float scale_y = display.y / reference_height;
const float width = static_cast<float>(snapshot.panel_width) * scale_x;
const float height = static_cast<float>(snapshot.panel_height) * scale_y;
ui_scale = std::min(scale_x, scale_y) * (over_2k ? 2.0f : 1.0f);
button_height = std::max(28.0f, 42.0f * ui_scale);
const ImVec2 pivot {AlignmentPivot(snapshot.horizontal_alignment),
AlignmentPivot(snapshot.vertical_alignment)};
if ((snapshot.option & Ime::OPTION_FIXED_POSITION) == 0) {
const float movement = 600.0f * ImGui::GetIO().DeltaTime;
panel_offset.x += right_stick.x * movement;
panel_offset.y += right_stick.y * movement;
}
const ImVec2 base_position {snapshot.posx * scale_x - pivot.x * width,
snapshot.posy * scale_y - pivot.y * height};
ImVec2 position {base_position.x + panel_offset.x, base_position.y + panel_offset.y};
if ((snapshot.option & Ime::OPTION_DISABLE_POSITION_ADJ) == 0) {
position.x = std::clamp(position.x, 0.0f, std::max(display.x - width, 0.0f));
position.y = std::clamp(position.y, 0.0f, std::max(display.y - height, 0.0f));
panel_offset = {position.x - base_position.x, position.y - base_position.y};
}
ImGui::SetNextWindowPos(position, ImGuiCond_Always);
ImGui::SetNextWindowSize({width, height}, ImGuiCond_Always);
constexpr ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings;
ImGui::Begin("##ImeDialog", nullptr, flags);
const std::u16string title = snapshot.title.empty() ? u"Enter text" : snapshot.title;
if (!snapshot.key_panel_visible) {
std::string compact = Common::Utf16ToUtf8(title.c_str()) + ": ";
const float glyph_width = std::max(ImGui::CalcTextSize("M").x, 1.0f);
const float text_width =
std::max(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(compact.c_str()).x,
glyph_width);
const size_t visible_units =
std::max<size_t>(6, static_cast<size_t>(text_width / glyph_width));
compact += snapshot.text.empty() && !snapshot.placeholder.empty()
? Common::Utf16ToUtf8(snapshot.placeholder.c_str())
: VisibleText(snapshot, visible_units);
ImGui::TextUnformatted(compact.c_str());
const float compact_height = std::min(button_height, 28.0f * ui_scale);
if (ImGui::Button("Cancel", {90.0f * ui_scale, compact_height})) {
Ime::HostCancel(snapshot.generation);
}
ImGui::SameLine();
if (ImGui::Button(EnterLabel(snapshot.enter_label),
{90.0f * ui_scale, compact_height})) {
Ime::HostAccept(snapshot.generation);
}
ImGui::End();
return;
}
ImGui::TextUnformatted(Common::Utf16ToUtf8(title.c_str()).c_str());
ImGui::Separator();
const float text_height = std::max(
32.0f, ((snapshot.option & Ime::OPTION_MULTILINE) != 0 ? 80.0f : 48.0f) * ui_scale);
ImGui::BeginChild("##ImeText", {0.0f, text_height}, true);
if (snapshot.text.empty() && !snapshot.placeholder.empty()) {
ImGui::TextDisabled("%s", Common::Utf16ToUtf8(snapshot.placeholder.c_str()).c_str());
} else {
const float glyph_width = std::max(ImGui::CalcTextSize("M").x, 1.0f);
const size_t columns = std::max<size_t>(
8, static_cast<size_t>(ImGui::GetContentRegionAvail().x / glyph_width));
const size_t lines =
std::max<size_t>(1, static_cast<size_t>(ImGui::GetContentRegionAvail().y /
ImGui::GetTextLineHeightWithSpacing()));
const auto text = VisibleText(snapshot, columns * lines);
ImGui::TextWrapped("%s", text.c_str());
}
ImGui::EndChild();
ImGui::TextDisabled("%zu / %u", snapshot.text.size(), snapshot.max_text_length);
ImGui::Separator();
const float content_width = ImGui::GetContentRegionAvail().x;
DrawKeyRows(snapshot, content_width);
if (snapshot.type != Ime::Type::Number) {
if (ImGui::Button(shift ? "Lower" : "Shift", {84.0f * ui_scale, button_height})) {
shift = !shift;
}
ImGui::SameLine();
if (ImGui::Button(symbol_mode ? "ABC" : "Symbols", {84.0f * ui_scale, button_height})) {
symbol_mode = !symbol_mode;
}
ImGui::SameLine();
if (ImGui::Button("Space", {120.0f * ui_scale, button_height})) {
Ime::HostInsertText(snapshot.generation, u" ");
}
ImGui::SameLine();
}
const float action_width = snapshot.type == Ime::Type::Number
? std::max((content_width - 16.0f) / 3.0f, 48.0f)
: 100.0f * ui_scale;
if (ImGui::Button("Backspace", {action_width, button_height})) {
Ime::HostBackspace(snapshot.generation);
}
ImGui::SameLine();
if ((snapshot.option & Ime::OPTION_MULTILINE) != 0) {
if (ImGui::Button("Newline", {action_width, button_height})) {
Ime::HostInsertText(snapshot.generation, u"\n");
}
ImGui::SameLine();
}
if (ImGui::Button("Cancel", {action_width, button_height})) {
Ime::HostCancel(snapshot.generation);
}
ImGui::SameLine();
if (ImGui::Button(EnterLabel(snapshot.enter_label), {action_width, button_height})) {
Ime::HostAccept(snapshot.generation);
}
ImGui::End();
}
bool PrepareFrame(vk::Extent2D frame_extent, vk::Format format, uint32_t image_count) {
Ime::HostSnapshot snapshot;
if (!Ime::GetHostSnapshot(&snapshot)) {
return false;
}
const uint64_t prepared_generation = snapshot.generation;
EnsureVulkan(format, image_count);
if (generation != snapshot.generation) {
generation = snapshot.generation;
focus_pending = true;
shift = (snapshot.option & Ime::OPTION_NO_AUTO_CAPITALIZE) == 0;
symbol_mode = false;
panel_offset = {};
right_stick = {};
auto& io = ImGui::GetIO();
io.ClearEventsQueue();
io.ClearInputKeys();
io.ClearInputMouse();
}
DrainInput(snapshot.generation);
auto& io = ImGui::GetIO();
io.DisplaySize = {static_cast<float>(frame_extent.width),
static_cast<float>(frame_extent.height)};
const auto now = std::chrono::steady_clock::now();
io.DeltaTime = last_frame == std::chrono::steady_clock::time_point {}
? 1.0f / 60.0f
: std::clamp(std::chrono::duration<float>(now - last_frame).count(),
1.0f / 1000.0f, 0.1f);
last_frame = now;
ImGui_ImplVulkan_NewFrame();
ImGui::NewFrame();
if (!Ime::GetHostSnapshot(&snapshot) || snapshot.generation != prepared_generation) {
ImGui::EndFrame();
return false;
}
DrawDialog(snapshot, frame_extent);
ImGui::Render();
extent = frame_extent;
return true;
}
void Record(vk::CommandBuffer command, vk::ImageView target) {
vk::RenderingAttachmentInfo color {};
color.sType = vk::StructureType::eRenderingAttachmentInfo;
color.imageView = target;
color.imageLayout = vk::ImageLayout::eColorAttachmentOptimal;
color.loadOp = vk::AttachmentLoadOp::eLoad;
color.storeOp = vk::AttachmentStoreOp::eStore;
vk::RenderingInfo rendering {};
rendering.sType = vk::StructureType::eRenderingInfo;
rendering.renderArea.extent = extent;
rendering.layerCount = 1;
rendering.colorAttachmentCount = 1;
rendering.pColorAttachments = &color;
command.beginRendering(rendering);
{
Common::LockGuard queue_lock(graphics.queue_mutex);
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(),
static_cast<VkCommandBuffer>(command));
}
command.endRendering();
}
void ReleaseVulkan() {
if (!vulkan_initialized) {
return;
}
ImGui::SetCurrentContext(imgui_context);
ImGui_ImplVulkan_Shutdown();
vulkan_initialized = false;
}
GraphicContext& graphics;
ImGuiContext* imgui_context = nullptr;
bool vulkan_initialized = false;
bool shift = false;
bool symbol_mode = false;
bool focus_pending = true;
float ui_scale = 1.0f;
float button_height = 42.0f;
ImVec2 panel_offset {};
ImVec2 right_stick {};
uint64_t generation = 0;
vk::Extent2D extent {};
std::chrono::steady_clock::time_point last_frame;
};
ImeDialogOverlay::ImeDialogOverlay(GraphicContext& graphics)
: m_impl(std::make_unique<Impl>(graphics)) {}
ImeDialogOverlay::~ImeDialogOverlay() = default;
bool ImeDialogOverlay::PrepareFrame(vk::Extent2D extent, vk::Format format, uint32_t image_count) {
return m_impl->PrepareFrame(extent, format, image_count);
}
void ImeDialogOverlay::Record(vk::CommandBuffer command, vk::ImageView target) {
m_impl->Record(command, target);
}
void ImeDialogOverlay::ReleaseVulkan() {
m_impl->ReleaseVulkan();
}
} // namespace Libs::Graphics
@@ -0,0 +1,36 @@
#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_IMEDIALOGOVERLAY_H_
#define EMULATOR_SRC_GRAPHICS_PRESENTATION_IMEDIALOGOVERLAY_H_
#include "common/common.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include <memory>
union SDL_Event;
namespace Libs::Graphics {
struct GraphicContext;
void InitializeImeDialogInput();
void ShutdownImeDialogInput();
bool ProcessImeDialogInput(const SDL_Event& event);
class ImeDialogOverlay final {
public:
explicit ImeDialogOverlay(GraphicContext& graphics);
~ImeDialogOverlay();
KYTY_CLASS_NO_COPY(ImeDialogOverlay);
[[nodiscard]] bool PrepareFrame(vk::Extent2D extent, vk::Format format, uint32_t image_count);
void Record(vk::CommandBuffer command, vk::ImageView target);
void ReleaseVulkan();
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
} // namespace Libs::Graphics
#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_IMEDIALOGOVERLAY_H_
+1
View File
@@ -25,6 +25,7 @@ public:
CommandBuffer* producer = nullptr);
[[nodiscard]] Frame* PrepareLastFrame();
[[nodiscard]] bool IsGuestPaused() const noexcept;
[[nodiscard]] bool NeedsImeRefresh() const noexcept;
[[nodiscard]] RenderContext& Renderer() const noexcept;
void Present(Frame& frame, bool reuse = false);
void Discard(Frame& frame);
+18 -1
View File
@@ -817,7 +817,24 @@ void VideoOutDriver::Impl::PresentThread(std::stop_token token) {
}
VblankBegin();
const bool presented = m_flip_queue.Flip(0);
bool presented = m_flip_queue.Flip(0);
if (!presented && m_presenter.NeedsImeRefresh()) {
if (auto* frame = m_presenter.PrepareLastFrame(); frame != nullptr) {
m_presenter.Present(*frame, true);
presented = true;
} else {
uint32_t width = 0;
uint32_t height = 0;
{
Common::LockGuard lock(m_mutex);
width = m_video_out_ctx[0].width;
height = m_video_out_ctx[0].height;
}
auto& blank = m_presenter.PrepareBlankFrame(width, height, true);
m_presenter.Present(blank);
presented = true;
}
}
if (!presented && total_wait < 0) {
bool any_open = false;
uint32_t width = 0;
@@ -0,0 +1,256 @@
#include "graphics/presentation/window/hostInput.h"
#include "SDL_keyboard.h"
#include "SDL_keycode.h"
#include "SDL_mouse.h"
#include "common/assert.h"
#include "common/emulatorConfig.h"
#include "libs/controller.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <string>
#include <string_view>
#include <utility>
namespace Libs::Graphics {
namespace {
struct Binding {
SDL_Keycode key = SDLK_UNKNOWN;
uint8_t mouse_button = 0;
uint32_t pad_button = 0;
};
static constexpr std::array PAD_BUTTONS = {
std::pair {std::string_view("L3"), Controller::PAD_BUTTON_L3},
std::pair {std::string_view("R3"), Controller::PAD_BUTTON_R3},
std::pair {std::string_view("Options"), Controller::PAD_BUTTON_OPTIONS},
std::pair {std::string_view("Up"), Controller::PAD_BUTTON_UP},
std::pair {std::string_view("Right"), Controller::PAD_BUTTON_RIGHT},
std::pair {std::string_view("Down"), Controller::PAD_BUTTON_DOWN},
std::pair {std::string_view("Left"), Controller::PAD_BUTTON_LEFT},
std::pair {std::string_view("L2"), Controller::PAD_BUTTON_L2},
std::pair {std::string_view("R2"), Controller::PAD_BUTTON_R2},
std::pair {std::string_view("L1"), Controller::PAD_BUTTON_L1},
std::pair {std::string_view("R1"), Controller::PAD_BUTTON_R1},
std::pair {std::string_view("Triangle"), Controller::PAD_BUTTON_TRIANGLE},
std::pair {std::string_view("Circle"), Controller::PAD_BUTTON_CIRCLE},
std::pair {std::string_view("Cross"), Controller::PAD_BUTTON_CROSS},
std::pair {std::string_view("Square"), Controller::PAD_BUTTON_SQUARE},
std::pair {std::string_view("TouchPad"), Controller::PAD_BUTTON_TOUCH_PAD},
};
uint32_t PadButtonFromName(std::string_view name) {
const auto button = std::find_if(PAD_BUTTONS.begin(), PAD_BUTTONS.end(),
[name](const auto& item) { return item.first == name; });
return button != PAD_BUTTONS.end() ? button->second : 0;
}
SDL_Keycode NormalizeKey(SDL_Keycode key) {
switch (key) {
case SDLK_RSHIFT: return SDLK_LSHIFT;
case SDLK_RCTRL: return SDLK_LCTRL;
case SDLK_RALT: return SDLK_LALT;
case SDLK_RGUI: return SDLK_LGUI;
default: return key;
}
}
uint8_t MouseButtonFromName(std::string_view name) {
static constexpr std::array buttons = {
std::pair {std::string_view("Mouse:Left"), uint8_t {SDL_BUTTON_LEFT}},
std::pair {std::string_view("Mouse:Middle"), uint8_t {SDL_BUTTON_MIDDLE}},
std::pair {std::string_view("Mouse:Right"), uint8_t {SDL_BUTTON_RIGHT}},
std::pair {std::string_view("Mouse:X1"), uint8_t {SDL_BUTTON_X1}},
std::pair {std::string_view("Mouse:X2"), uint8_t {SDL_BUTTON_X2}},
};
const auto button = std::find_if(buttons.begin(), buttons.end(),
[name](const auto& item) { return item.first == name; });
return button != buttons.end() ? button->second : 0;
}
bool Conflicts(const Binding& first, const Binding& second) {
return first.pad_button == second.pad_button ||
(first.key != SDLK_UNKNOWN && first.key == second.key) ||
(first.mouse_button != 0 && first.mouse_button == second.mouse_button);
}
class InputMap {
public:
InputMap(): m_custom(!Config::GetKeymap().empty()) {
for (const auto& value: Config::GetKeymap()) {
const std::string_view entry = value;
const auto split = entry.find('=');
Binding binding;
if (split != std::string_view::npos) {
binding.pad_button = PadButtonFromName(entry.substr(0, split));
const auto host_input = entry.substr(split + 1);
binding.mouse_button = MouseButtonFromName(host_input);
if (binding.mouse_button == 0) {
binding.key = NormalizeKey(SDL_GetKeyFromName(std::string(host_input).c_str()));
}
}
const bool reserved =
binding.key == SDLK_ESCAPE || binding.key == SDLK_SPACE || binding.key == SDLK_F1;
if (binding.pad_button == 0 || reserved ||
(binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) {
EXIT("Invalid input mapping: %s\n", value.c_str());
}
Add(binding);
}
}
[[nodiscard]] bool Custom() const { return m_custom; }
[[nodiscard]] uint32_t FindKey(int key_code) const {
key_code = NormalizeKey(static_cast<SDL_Keycode>(key_code));
const auto binding =
std::find_if(m_bindings.begin(), m_bindings.begin() + m_size,
[key_code](const auto& item) { return item.key == key_code; });
return binding != m_bindings.begin() + m_size ? binding->pad_button : 0;
}
[[nodiscard]] uint32_t FindMouseButton(uint8_t mouse_button) const {
const auto binding = std::find_if(
m_bindings.begin(), m_bindings.begin() + m_size,
[mouse_button](const auto& item) { return item.mouse_button == mouse_button; });
return binding != m_bindings.begin() + m_size ? binding->pad_button : 0;
}
private:
void Add(const Binding& binding) {
for (std::size_t index = 0; index < m_size;) {
if (Conflicts(m_bindings[index], binding)) {
m_bindings[index] = m_bindings[--m_size];
} else {
index++;
}
}
EXIT_IF(m_size >= m_bindings.size());
m_bindings[m_size++] = binding;
}
std::array<Binding, PAD_BUTTONS.size()> m_bindings {};
std::size_t m_size = 0;
bool m_custom;
};
const InputMap& GetInputMap() {
static const InputMap map;
return map;
}
void SetButton(uint32_t button, bool down) {
if (button == Controller::PAD_BUTTON_L2) {
Controller::ControllerAxis(Controller::HOST_INPUT_CONTROLLER_ID,
Controller::Axis::TriggerLeft, down ? 255 : 0);
} else if (button == Controller::PAD_BUTTON_R2) {
Controller::ControllerAxis(Controller::HOST_INPUT_CONTROLLER_ID,
Controller::Axis::TriggerRight, down ? 255 : 0);
} else if (button != 0) {
Controller::ControllerButton(Controller::HOST_INPUT_CONTROLLER_ID, button, down);
}
}
uint32_t DefaultKeyboardButton(int key_code) {
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
case SDLK_UP: return Controller::PAD_BUTTON_UP;
case SDLK_LEFT: return Controller::PAD_BUTTON_LEFT;
case SDLK_DOWN: return Controller::PAD_BUTTON_DOWN;
case SDLK_RIGHT: return Controller::PAD_BUTTON_RIGHT;
case SDLK_j: return Controller::PAD_BUTTON_CROSS;
case SDLK_i: return Controller::PAD_BUTTON_TRIANGLE;
case SDLK_k: return Controller::PAD_BUTTON_SQUARE;
case SDLK_l: return Controller::PAD_BUTTON_CIRCLE;
case SDLK_q: return Controller::PAD_BUTTON_L1;
case SDLK_e: return Controller::PAD_BUTTON_R1;
case SDLK_LSHIFT: return Controller::PAD_BUTTON_L3;
case SDLK_LCTRL: return Controller::PAD_BUTTON_R3;
case SDLK_RETURN:
case SDLK_RETURN2: return Controller::PAD_BUTTON_OPTIONS;
case SDLK_BACKSPACE:
case SDLK_TAB: return Controller::PAD_BUTTON_TOUCH_PAD;
default: return 0;
}
}
struct StickKeys {
bool left = false;
bool right = false;
bool up = false;
bool down = false;
};
void SetStickAxis(Controller::Axis axis, bool negative, bool positive) {
const int value = negative == positive ? 128 : negative ? 0 : 255;
Controller::ControllerAxis(Controller::HOST_INPUT_CONTROLLER_ID, axis, value);
}
void DefaultKeyboardInput(int key_code, bool down) {
static StickKeys left;
static StickKeys right;
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
case SDLK_a:
left.left = down;
SetStickAxis(Controller::Axis::LeftX, left.left, left.right);
return;
case SDLK_d:
left.right = down;
SetStickAxis(Controller::Axis::LeftX, left.left, left.right);
return;
case SDLK_w:
left.up = down;
SetStickAxis(Controller::Axis::LeftY, left.up, left.down);
return;
case SDLK_s:
left.down = down;
SetStickAxis(Controller::Axis::LeftY, left.up, left.down);
return;
case SDLK_f:
right.left = down;
SetStickAxis(Controller::Axis::RightX, right.left, right.right);
return;
case SDLK_h:
right.right = down;
SetStickAxis(Controller::Axis::RightX, right.left, right.right);
return;
case SDLK_t:
right.up = down;
SetStickAxis(Controller::Axis::RightY, right.up, right.down);
return;
case SDLK_g:
right.down = down;
SetStickAxis(Controller::Axis::RightY, right.up, right.down);
return;
default: SetButton(DefaultKeyboardButton(key_code), down); return;
}
}
} // namespace
void HostInputInit() {
GetInputMap();
}
void HostInputKey(int key_code, bool down) {
const auto& map = GetInputMap();
if (map.Custom()) {
SetButton(map.FindKey(key_code), down);
} else {
DefaultKeyboardInput(key_code, down);
}
}
void HostInputMouseButton(uint8_t mouse_button, bool down) {
const auto& map = GetInputMap();
if (map.Custom() && mouse_button != 0) {
SetButton(map.FindMouseButton(mouse_button), down);
}
}
} // namespace Libs::Graphics
@@ -0,0 +1,14 @@
#ifndef KYTY_GRAPHICS_PRESENTATION_WINDOW_HOST_INPUT_H_
#define KYTY_GRAPHICS_PRESENTATION_WINDOW_HOST_INPUT_H_
#include <cstdint>
namespace Libs::Graphics {
void HostInputInit();
void HostInputKey(int key_code, bool down);
void HostInputMouseButton(uint8_t mouse_button, bool down);
} // namespace Libs::Graphics
#endif /* KYTY_GRAPHICS_PRESENTATION_WINDOW_HOST_INPUT_H_ */
+72 -29
View File
@@ -31,11 +31,13 @@
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/host_gpu/vma.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/presentation/imeDialogOverlay.h"
#include "graphics/presentation/presenter.h"
#include "graphics/presentation/renderDoc.h"
#include "graphics/presentation/videoOut.h"
#include "graphics/presentation/window/windowInternal.h"
#include "libs/controller.h"
#include "libs/imeDialog.h"
#include "loader/systemContent.h"
#include <algorithm>
@@ -329,8 +331,9 @@ public:
void Create();
void Recreate(bool surface_lost = false);
[[nodiscard]] Status AcquireNextImage();
void RecordPresentCommands(CommandBuffer& command, VulkanImage& source);
void Submit(CommandBuffer& command);
[[nodiscard]] bool PrepareImeOverlay();
void RecordPresentCommands(CommandBuffer& command, VulkanImage& source, bool draw_ime_overlay);
void Submit(CommandBuffer& command);
[[nodiscard]] Status Present();
[[nodiscard]] uint32_t ImageCount() const noexcept {
@@ -342,16 +345,17 @@ private:
void Destroy();
void RefreshSurfaceSize();
WindowContext& m_window;
vk::SwapchainKHR m_handle = nullptr;
vk::Format m_format = vk::Format::eUndefined;
vk::Extent2D m_extent {};
std::vector<vk::Image> m_images;
std::vector<vk::ImageView> m_image_views;
std::vector<vk::Semaphore> m_image_acquired;
std::vector<vk::Semaphore> m_render_complete;
uint32_t m_image_index = static_cast<uint32_t>(-1);
uint32_t m_frame_index = 0;
WindowContext& m_window;
vk::SwapchainKHR m_handle = nullptr;
vk::Format m_format = vk::Format::eUndefined;
vk::Extent2D m_extent {};
std::vector<vk::Image> m_images;
std::vector<vk::ImageView> m_image_views;
std::vector<vk::Semaphore> m_image_acquired;
std::vector<vk::Semaphore> m_render_complete;
std::unique_ptr<ImeDialogOverlay> m_ime_overlay;
uint32_t m_image_index = static_cast<uint32_t>(-1);
uint32_t m_frame_index = 0;
};
struct Presenter::Impl {
@@ -389,11 +393,12 @@ struct Presenter::Impl {
return image;
}
RenderContext& renderer;
WindowContext& window;
Swapchain swapchain;
CommandScheduler present_scheduler;
FramePool frames;
RenderContext& renderer;
WindowContext& window;
Swapchain swapchain;
CommandScheduler present_scheduler;
FramePool frames;
std::atomic<uint64_t> presented_ime_revision {0};
};
void Swapchain::Create() {
@@ -522,6 +527,9 @@ void Swapchain::Destroy() {
Common::LockGuard queue_lock(graphics.queue_mutex);
RequireVulkanSuccess(graphics.queue.waitIdle(), "wait for swapchain queue");
}
if (m_ime_overlay != nullptr) {
m_ime_overlay->ReleaseVulkan();
}
for (const auto semaphore: m_image_acquired) {
if (semaphore != nullptr) {
@@ -606,7 +614,15 @@ Swapchain::Status Swapchain::AcquireNextImage() {
return Status::Success;
}
void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& source) {
bool Swapchain::PrepareImeOverlay() {
if (m_ime_overlay == nullptr) {
m_ime_overlay = std::make_unique<ImeDialogOverlay>(m_window.graphic_ctx);
}
return m_ime_overlay->PrepareFrame(m_extent, m_format, ImageCount());
}
void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& source,
bool draw_ime_overlay) {
if (source.state.layout != vk::ImageLayout::eTransferSrcOptimal) {
EXIT("invalid prepared presentation image, vk_image=%p layout=%d\n",
static_cast<void*>(source.image), static_cast<int>(source.state.layout));
@@ -652,11 +668,14 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& sourc
vk::Filter::eLinear);
vk::ImageMemoryBarrier to_present {};
to_present.sType = vk::StructureType::eImageMemoryBarrier;
to_present.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
to_present.dstAccessMask = vk::AccessFlagBits::eMemoryRead;
to_present.oldLayout = vk::ImageLayout::eTransferDstOptimal;
to_present.newLayout = vk::ImageLayout::ePresentSrcKHR;
to_present.sType = vk::StructureType::eImageMemoryBarrier;
to_present.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
to_present.dstAccessMask = draw_ime_overlay ? vk::AccessFlagBits::eColorAttachmentRead |
vk::AccessFlagBits::eColorAttachmentWrite
: vk::AccessFlagBits::eMemoryRead;
to_present.oldLayout = vk::ImageLayout::eTransferDstOptimal;
to_present.newLayout = draw_ime_overlay ? vk::ImageLayout::eColorAttachmentOptimal
: vk::ImageLayout::ePresentSrcKHR;
to_present.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_present.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_present.image = m_images[m_image_index];
@@ -665,9 +684,22 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& sourc
to_present.subresourceRange.levelCount = 1;
to_present.subresourceRange.baseArrayLayer = 0;
to_present.subresourceRange.layerCount = 1;
vk_command.pipelineBarrier(
vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eAllCommands,
vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, 1, &to_present);
vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
draw_ime_overlay ? vk::PipelineStageFlagBits::eColorAttachmentOutput
: vk::PipelineStageFlagBits::eAllCommands,
vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, 1,
&to_present);
if (draw_ime_overlay) {
m_ime_overlay->Record(vk_command, m_image_views[m_image_index]);
to_present.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
to_present.dstAccessMask = vk::AccessFlagBits::eMemoryRead;
to_present.oldLayout = vk::ImageLayout::eColorAttachmentOptimal;
to_present.newLayout = vk::ImageLayout::ePresentSrcKHR;
vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput,
vk::PipelineStageFlagBits::eAllCommands,
vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, 1,
&to_present);
}
command.End();
}
@@ -772,6 +804,12 @@ bool Presenter::IsGuestPaused() const noexcept {
return m_impl->window.loop.paused.load(std::memory_order_acquire);
}
bool Presenter::NeedsImeRefresh() const noexcept {
const auto visual = Dialog::ImeDialog::GetVisualState();
return visual.active ||
visual.revision != m_impl->presented_ime_revision.load(std::memory_order_acquire);
}
RenderContext& Presenter::Renderer() const noexcept {
return m_impl->renderer;
}
@@ -789,19 +827,22 @@ void Presenter::Present(Frame& frame, bool reuse) {
[&window] {
window.UpdateIcon();
SDL_ShowWindow(window.window);
SDL_RaiseWindow(window.window);
},
true);
#else
window.UpdateIcon();
SDL_ShowWindow(window.window);
SDL_RaiseWindow(window.window);
#endif
window.window_hidden = false;
m_impl->RecoverSwapchain(Swapchain::Status::Recreate);
}
auto& swapchain = m_impl->swapchain;
const auto ime_visual = Dialog::ImeDialog::GetVisualState();
auto& swapchain = m_impl->swapchain;
for (uint32_t attempt = 0; attempt < 2; attempt++) {
auto status = swapchain.AcquireNextImage();
if (status != Swapchain::Status::Success) {
@@ -814,8 +855,9 @@ void Presenter::Present(Frame& frame, bool reuse) {
{
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
frame.present_commands->WaitForFenceAndReset();
auto& command = *frame.present_commands;
swapchain.RecordPresentCommands(command, frame.image);
auto& command = *frame.present_commands;
const bool draw_ime_overlay = ime_visual.active && swapchain.PrepareImeOverlay();
swapchain.RecordPresentCommands(command, frame.image, draw_ime_overlay);
swapchain.Submit(command);
}
status = swapchain.Present();
@@ -825,6 +867,7 @@ void Presenter::Present(Frame& frame, bool reuse) {
}
RenderDocOnPresent();
m_impl->presented_ime_revision.store(ime_visual.revision, std::memory_order_release);
window.UpdateTitle();
m_impl->frames.Release(&frame, true);
return;
@@ -31,6 +31,7 @@
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/host_gpu/vma.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/presentation/imeDialogOverlay.h"
#include "graphics/presentation/presenter.h"
#include "graphics/presentation/renderDoc.h"
#include "graphics/presentation/videoOut.h"
@@ -1008,6 +1009,7 @@ void WindowContext::RecreateSurface() {
}
WindowContext::~WindowContext() {
ShutdownImeDialogInput();
presenter.reset();
LibKernel::Memory::InstallGpuResources(nullptr);
render_context.reset();
+53 -36
View File
@@ -31,8 +31,11 @@
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/vma.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/presentation/imeDialogOverlay.h"
#include "graphics/presentation/renderDoc.h"
#include "graphics/presentation/window/hostInput.h"
#include "graphics/presentation/window/windowInternal.h"
#include "kytyGitVersion.h"
#include "libs/controller.h"
#include "loader/systemContent.h"
@@ -58,8 +61,6 @@
namespace Libs::Graphics {
constexpr int KEYBOARD_CONTROLLER_ID = -1000;
struct EventKeyboard {
bool down;
bool up;
@@ -72,26 +73,6 @@ struct EventKeyboard {
double timestamp_seconds;
};
static uint32_t KeyboardKeyToPadButton(int key_code) {
switch (key_code) {
case SDLK_w: return Controller::PAD_BUTTON_UP;
case SDLK_a: return Controller::PAD_BUTTON_LEFT;
case SDLK_s: return Controller::PAD_BUTTON_DOWN;
case SDLK_d: return Controller::PAD_BUTTON_RIGHT;
case SDLK_j: return Controller::PAD_BUTTON_CROSS;
case SDLK_i: return Controller::PAD_BUTTON_TRIANGLE;
case SDLK_k: return Controller::PAD_BUTTON_SQUARE;
case SDLK_l: return Controller::PAD_BUTTON_CIRCLE;
case SDLK_q: return Controller::PAD_BUTTON_L1;
case SDLK_e: return Controller::PAD_BUTTON_R1;
case SDLK_RETURN:
case SDLK_RETURN2: return Controller::PAD_BUTTON_OPTIONS;
case SDLK_BACKSPACE:
case SDLK_TAB: return Controller::PAD_BUTTON_TOUCH_PAD;
default: return 0;
}
}
static uint32_t ControllerButtonToPadButton(int button) {
switch (button) {
case SDL_CONTROLLER_BUTTON_A: return Controller::PAD_BUTTON_CROSS;
@@ -260,16 +241,11 @@ static void GameEventKeyboard(WindowLoopState& game, const EventKeyboard& key) {
}
}
const auto button = KeyboardKeyToPadButton(key.key_code);
if (button != 0 && (key.down || key.up) && !key.repeat) {
static bool keyboard_connected = false;
if (!keyboard_connected) {
Controller::ControllerConnect(KEYBOARD_CONTROLLER_ID);
keyboard_connected = true;
}
Controller::ControllerButton(KEYBOARD_CONTROLLER_ID, button, key.down);
}
#endif
if ((key.down || key.up) && !key.repeat) {
HostInputKey(key.key_code, key.down);
}
}
static void GameEventMouse([[maybe_unused]] const EventMouse& mb) {
@@ -291,6 +267,23 @@ static void GameEventMouse([[maybe_unused]] const EventMouse& mb) {
mb.x, mb.y);
}
#endif
if (mb.down || mb.up) {
uint8_t mouse_button = 0;
if (mb.left) {
mouse_button = SDL_BUTTON_LEFT;
} else if (mb.middle) {
mouse_button = SDL_BUTTON_MIDDLE;
} else if (mb.right) {
mouse_button = SDL_BUTTON_RIGHT;
} else if (mb.x1) {
mouse_button = SDL_BUTTON_X1;
} else if (mb.x2) {
mouse_button = SDL_BUTTON_X2;
}
HostInputMouseButton(mouse_button, mb.down);
}
}
static void GameEventFinger([[maybe_unused]] const EventFinger& f) {
@@ -484,6 +477,9 @@ void WindowContext::ProcessEvent(double time_s) {
auto& game = loop;
auto* event = &game.event;
EXIT_IF(SDL_GetEventState(SDL_DISPLAYEVENT) != SDL_ENABLE);
if (ProcessImeDialogInput(*event)) {
return;
}
switch (event->type) {
case SDL_QUIT: GameEventQuit(game); break;
@@ -794,10 +790,15 @@ static void WindowCreate(WindowContext& context) {
if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) {
EXIT("%s\n", SDL_GetError());
}
HostInputInit();
InitializeImeDialogInput();
LOGF("WindowCreate(): width = %d, height = %d\n", width, height);
uint32_t window_flags = KYTY_SDL_WINDOW_FLAGS;
if (Config::FullscreenEnabled()) {
window_flags |= static_cast<uint32_t>(SDL_WINDOW_FULLSCREEN_DESKTOP);
}
#if defined(__APPLE__)
// macOS 26 window chrome (CoreUI asset decode, SwiftUI titlebar) has been observed
// throwing NSExceptions under Rosetta during the first CATransaction commit. A
@@ -937,6 +938,22 @@ void WindowContext::UpdateTitle() {
static uint64_t fps_frames = 0;
static double current_fps = 0.0;
#if KYTY_BUILD == KYTY_BUILD_DEBUG
static constexpr auto build_type = "Debug";
#elif KYTY_BUILD == KYTY_BUILD_RELEASE
static constexpr auto build_type = "Release";
#else
static constexpr auto build_type = "Unknown";
#endif
#if defined(KYTY_OFFICIAL_BUILD)
static constexpr auto build_label = "Official build " KYTY_RELEASE_TAG;
#elif defined(KYTY_FORK_BUILD)
static constexpr auto build_label = "Fork build " KYTY_BUILD_REPOSITORY " " KYTY_GIT_HASH;
#else
static constexpr auto build_label = "Source build " KYTY_GIT_HASH;
#endif
const auto now = Common::Timer::QueryPerformanceCounter();
const auto frequency = Common::Timer::QueryPerformanceFrequency();
frame_num++;
@@ -948,11 +965,11 @@ void WindowContext::UpdateTitle() {
fps_frames = 0;
}
auto fps =
fmt::format("{}{}{}{}{}{}[{}] [{}], frame: {}, fps: {:f}", (has_title ? title : ""),
(has_title ? ", " : ""), (has_title_id ? title_id : ""),
(has_title_id ? ", " : ""), (has_app_ver ? app_ver : ""),
(has_app_ver ? " " : ""), device_name, processor_name, frame_num, current_fps);
auto fps = fmt::format("[{} | {}] {}{}{}{}{}{}[{}] [{}], frame: {}, fps: {:f}", build_label,
build_type, (has_title ? title : ""), (has_title ? ", " : ""),
(has_title_id ? title_id : ""), (has_title_id ? ", " : ""),
(has_app_ver ? app_ver : ""), (has_app_ver ? " " : ""), device_name,
processor_name, frame_num, current_fps);
#if defined(__APPLE__)
// AppKit traps on title changes off the main thread; fire-and-forget keeps present pacing.
@@ -84,6 +84,8 @@ bool InstructionMaySplitSpirvBlock(const IR::Instruction& inst) {
case IR::Opcode::AtomicAndU32:
case IR::Opcode::AtomicOrU32:
case IR::Opcode::AtomicXorU32:
case IR::Opcode::AtomicFMinF32:
case IR::Opcode::AtomicFMaxF32:
case IR::Opcode::FlatLoadUbyte:
case IR::Opcode::FlatLoadSbyte:
case IR::Opcode::FlatLoadUshort:
@@ -57,6 +57,8 @@ constexpr MemoryOpcodeInfo MUBUF_OPS[] = {
{0x39u, Opcode::BufferAtomicAnd, 1, 32},
{0x3au, Opcode::BufferAtomicOr, 1, 32},
{0x3bu, Opcode::BufferAtomicXor, 1, 32},
{0x3fu, Opcode::BufferAtomicFMin, 1, 32},
{0x40u, Opcode::BufferAtomicFMax, 1, 32},
};
constexpr MemoryOpcodeInfo MTBUF_OPS[] = {
@@ -0,0 +1,45 @@
#ifndef EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_DECOMPILER_OPCODETABLE_H_
#define EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_DECOMPILER_OPCODETABLE_H_
#include "graphics/shader/recompiler/decompiler/ShaderDecoder.h"
#include <cstddef>
namespace Libs::Graphics::ShaderRecompiler::Decoder::Detail {
struct OpcodeMap {
uint32_t encoding = 0;
Opcode decoded = Opcode::Unknown;
};
template <typename Entry, size_t N>
constexpr const Entry* FindOpcode(const Entry (&table)[N], uint32_t encoding) {
for (const auto& entry: table) {
if (entry.encoding == encoding) {
return &entry;
}
}
return nullptr;
}
template <typename Entry, size_t N>
constexpr Opcode LookupOpcode(const Entry (&table)[N], uint32_t encoding) {
const auto* entry = FindOpcode(table, encoding);
return entry != nullptr ? entry->decoded : Opcode::Unsupported;
}
template <typename Entry, size_t N>
constexpr bool HasUniqueEncodings(const Entry (&table)[N]) {
for (size_t i = 0; i < N; i++) {
for (size_t j = i + 1; j < N; j++) {
if (table[i].encoding == table[j].encoding) {
return false;
}
}
}
return true;
}
} // namespace Libs::Graphics::ShaderRecompiler::Decoder::Detail
#endif // EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_DECOMPILER_OPCODETABLE_H_
@@ -1,14 +1,11 @@
#include "graphics/shader/recompiler/decompiler/ScalarAluOps.h"
#include <iterator>
#include "graphics/shader/recompiler/decompiler/OpcodeTable.h"
namespace Libs::Graphics::ShaderRecompiler::Decoder {
namespace {
struct OpcodeMap {
uint32_t opcode = 0;
Opcode ir = Opcode::Unknown;
};
using Detail::OpcodeMap;
constexpr OpcodeMap SOP2_OPS[] = {
{0x00u, Opcode::SAddU32}, {0x01u, Opcode::SSubU32}, {0x02u, Opcode::SAddI32},
@@ -58,7 +55,8 @@ constexpr OpcodeMap SOPC_OPS[] = {
{0x03u, Opcode::SCmpGeI32}, {0x04u, Opcode::SCmpLtI32}, {0x05u, Opcode::SCmpLeI32},
{0x06u, Opcode::SCmpEqU32}, {0x07u, Opcode::SCmpLgU32}, {0x08u, Opcode::SCmpGtU32},
{0x09u, Opcode::SCmpGeU32}, {0x0au, Opcode::SCmpLtU32}, {0x0bu, Opcode::SCmpLeU32},
{0x0cu, Opcode::SBitcmp0B32}, {0x0du, Opcode::SBitcmp1B32}, {0x13u, Opcode::SCmpLgU64},
{0x0cu, Opcode::SBitcmp0B32}, {0x0du, Opcode::SBitcmp1B32}, {0x12u, Opcode::SCmpEqU64},
{0x13u, Opcode::SCmpLgU64},
};
constexpr OpcodeMap SOPK_OPS[] = {
@@ -78,15 +76,11 @@ constexpr OpcodeMap SOPP_OPS[] = {
{0x0au, Opcode::SBarrier}, {0x0cu, Opcode::SWaitcnt}, {0x0eu, Opcode::SSleep},
{0x10u, Opcode::SSendmsg}, {0x16u, Opcode::STtraceData}, {0x20u, Opcode::SInstPrefetch},
};
Opcode Lookup(const OpcodeMap* ops, uint32_t count, uint32_t opcode) {
for (uint32_t i = 0; i < count; i++) {
if (ops[i].opcode == opcode) {
return ops[i].ir;
}
}
return Opcode::Unsupported;
}
static_assert(Detail::HasUniqueEncodings(SOP1_OPS));
static_assert(Detail::HasUniqueEncodings(SOP2_OPS));
static_assert(Detail::HasUniqueEncodings(SOPK_OPS));
static_assert(Detail::HasUniqueEncodings(SOPC_OPS));
static_assert(Detail::HasUniqueEncodings(SOPP_OPS));
bool DecodeBinarySources(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
Instruction& inst, uint32_t ssrc0, uint32_t ssrc1, std::string* error) {
@@ -111,7 +105,7 @@ bool DecodeSop1(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
inst.word = word;
inst.family = Family::SOP1;
inst.opcode_id = opcode;
inst.opcode = Lookup(SOP1_OPS, static_cast<uint32_t>(std::size(SOP1_OPS)), opcode);
inst.opcode = Detail::LookupOpcode(SOP1_OPS, opcode);
SetRawWords(inst, code, word_index, 1);
if (inst.opcode == Opcode::Unsupported) {
@@ -153,7 +147,7 @@ bool DecodeSop2(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
inst.word = word;
inst.family = Family::SOP2;
inst.opcode_id = opcode;
inst.opcode = Lookup(SOP2_OPS, static_cast<uint32_t>(std::size(SOP2_OPS)), opcode);
inst.opcode = Detail::LookupOpcode(SOP2_OPS, opcode);
SetRawWords(inst, code, word_index, 1);
if (inst.opcode == Opcode::Unsupported) {
@@ -180,7 +174,7 @@ bool DecodeSopk(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
inst.word = word;
inst.family = Family::SOPK;
inst.opcode_id = opcode;
inst.opcode = Lookup(SOPK_OPS, static_cast<uint32_t>(std::size(SOPK_OPS)), opcode);
inst.opcode = Detail::LookupOpcode(SOPK_OPS, opcode);
inst.src0.kind = OperandKind::IntegerInlineConstant;
inst.src0.signed_val = imm;
inst.src0.value = static_cast<uint32_t>(imm);
@@ -237,7 +231,7 @@ bool DecodeSopc(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
inst.word = word;
inst.family = Family::SOPC;
inst.opcode_id = opcode;
inst.opcode = Lookup(SOPC_OPS, static_cast<uint32_t>(std::size(SOPC_OPS)), opcode);
inst.opcode = Detail::LookupOpcode(SOPC_OPS, opcode);
inst.dst.kind = OperandKind::Scc;
SetRawWords(inst, code, word_index, 1);
@@ -261,7 +255,7 @@ bool DecodeSopp(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
inst.word = word;
inst.family = Family::SOPP;
inst.opcode_id = opcode;
inst.opcode = Lookup(SOPP_OPS, static_cast<uint32_t>(std::size(SOPP_OPS)), opcode);
inst.opcode = Detail::LookupOpcode(SOPP_OPS, opcode);
inst.src0.kind = OperandKind::LiteralConstant;
inst.src0.value = simm;
inst.src0.signed_val = static_cast<int16_t>(simm);
@@ -77,6 +77,16 @@ std::string FormatBinary(const Instruction& inst) {
return text;
}
std::string FormatSources(const Instruction& inst) {
std::string text = fmt::format("0x{:08x}: {}", inst.pc, OpcodeToString(inst.opcode));
const Operand* sources[] = {&inst.src0, &inst.src1, &inst.src2, &inst.src3};
for (uint32_t i = 0; i < inst.src_count && i < 4u; i++) {
text += i == 0 ? " " : ", ";
text += OperandToString(*sources[i]);
}
return text;
}
std::string FormatMemory(const Instruction& inst) {
std::string text = fmt::format("0x{:08x}: {} {}", inst.pc, OpcodeToString(inst.opcode).c_str(),
OperandToString(inst.dst).c_str());
@@ -511,6 +521,7 @@ std::string OpcodeToString(Opcode opcode) {
case Opcode::SCmpGeU32: return "s_cmp_ge_u32";
case Opcode::SCmpLtU32: return "s_cmp_lt_u32";
case Opcode::SCmpLeU32: return "s_cmp_le_u32";
case Opcode::SCmpEqU64: return "s_cmp_eq_u64";
case Opcode::SCmpLgU64: return "s_cmp_lg_u64";
case Opcode::VNop: return "v_nop";
case Opcode::VMovB32: return "v_mov_b32";
@@ -766,6 +777,7 @@ std::string OpcodeToString(Opcode opcode) {
case Opcode::VCmpNeU32: return "v_cmp_ne_u32";
case Opcode::VCmpGeU32: return "v_cmp_ge_u32";
case Opcode::VCmpTU32: return "v_cmp_t_u32";
case Opcode::VCmpEqI64: return "v_cmp_eq_i64";
case Opcode::VCmpNeU64: return "v_cmp_ne_u64";
case Opcode::VCmpxLtU32: return "v_cmpx_lt_u32";
case Opcode::VCmpxEqU32: return "v_cmpx_eq_u32";
@@ -823,6 +835,8 @@ std::string OpcodeToString(Opcode opcode) {
case Opcode::BufferAtomicAnd: return "buffer_atomic_and";
case Opcode::BufferAtomicOr: return "buffer_atomic_or";
case Opcode::BufferAtomicXor: return "buffer_atomic_xor";
case Opcode::BufferAtomicFMin: return "buffer_atomic_fmin";
case Opcode::BufferAtomicFMax: return "buffer_atomic_fmax";
case Opcode::FlatLoadUbyte: return "flat_load_ubyte";
case Opcode::FlatLoadSbyte: return "flat_load_sbyte";
case Opcode::FlatLoadUshort: return "flat_load_ushort";
@@ -983,6 +997,9 @@ std::string InstructionToString(const Instruction& inst) {
inst.pc, FamilyToString(inst.family).c_str(), inst.opcode_id,
RawWordsToString(inst).c_str(), inst.unsupported_reason.c_str());
}
if (inst.family == Family::SOPC) {
return WithUnsupportedReason(inst, FormatSources(inst));
}
switch (inst.opcode) {
case Opcode::SMovB32:
@@ -1110,6 +1127,8 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::BufferAtomicAnd:
case Opcode::BufferAtomicOr:
case Opcode::BufferAtomicXor:
case Opcode::BufferAtomicFMin:
case Opcode::BufferAtomicFMax:
case Opcode::BufferLoadSbyte:
case Opcode::BufferLoadSshort:
case Opcode::FlatLoadUbyte:
@@ -1171,25 +1190,6 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::DsWriteB128:
case Opcode::DsWriteAddtidB32:
case Opcode::DsReadAddtidB32: return WithUnsupportedReason(inst, FormatMemory(inst));
case Opcode::SCmpEqU32:
case Opcode::SCmpEqI32:
case Opcode::SCmpLgU32:
case Opcode::SCmpLgI32:
case Opcode::SCmpGtU32:
case Opcode::SCmpGtI32:
case Opcode::SCmpGeU32:
case Opcode::SCmpGeI32:
case Opcode::SCmpLtU32:
case Opcode::SCmpLtI32:
case Opcode::SCmpLeU32:
case Opcode::SCmpLeI32:
case Opcode::SBitcmp0B32:
case Opcode::SBitcmp1B32:
case Opcode::SCmpLgU64:
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: {} {}, {}", inst.pc,
OpcodeToString(inst.opcode).c_str(),
OperandToString(inst.src0).c_str(),
OperandToString(inst.src1).c_str()));
default: return WithUnsupportedReason(inst, FormatBinary(inst));
}
}
@@ -118,6 +118,7 @@ enum class Opcode {
SCmpGeU32,
SCmpLtU32,
SCmpLeU32,
SCmpEqU64,
SCmpLgU64,
VNop,
@@ -374,6 +375,7 @@ enum class Opcode {
VCmpNeU32,
VCmpGeU32,
VCmpTU32,
VCmpEqI64,
VCmpNeU64,
VCmpxLtU32,
VCmpxEqU32,
@@ -432,6 +434,8 @@ enum class Opcode {
BufferAtomicAnd,
BufferAtomicOr,
BufferAtomicXor,
BufferAtomicFMin,
BufferAtomicFMax,
FlatLoadUbyte,
FlatLoadSbyte,
FlatLoadUshort,
@@ -1,43 +1,91 @@
#include "graphics/shader/recompiler/decompiler/VectorAluOps.h"
#include "graphics/shader/recompiler/decompiler/OpcodeTable.h"
#include <fmt/format.h>
#include <iterator>
namespace Libs::Graphics::ShaderRecompiler::Decoder {
namespace {
struct OpcodeMap {
uint32_t opcode = 0;
Opcode ir = Opcode::Unknown;
using Detail::OpcodeMap;
// These profiles describe the selectors that the current lowering implements. They are not
// architectural SDWA legality classes.
enum class Vop2SdwaProfile {
None,
Cndmask,
Float32,
Float16,
IntegerFullDestination,
IntegerPartialDestination,
ReverseLogicalRight,
ReverseLogicalLeft,
Bitwise,
Count,
};
constexpr OpcodeMap VOP2_OPS[] = {
{0x01u, Opcode::VCndmaskB32}, {0x02u, Opcode::VDot2cF32F16},
{0x03u, Opcode::VAddF32}, {0x04u, Opcode::VSubF32},
{0x05u, Opcode::VSubrevF32}, {0x08u, Opcode::VMulF32},
{0x09u, Opcode::VMulI32I24}, {0x0bu, Opcode::VMulU32U24},
{0x0fu, Opcode::VMinF32}, {0x10u, Opcode::VMaxF32},
{0x11u, Opcode::VMinI32}, {0x12u, Opcode::VMaxI32},
{0x13u, Opcode::VMinU32}, {0x14u, Opcode::VMaxU32},
{0x15u, Opcode::VLshrB32}, {0x16u, Opcode::VLshrrevB32},
{0x17u, Opcode::VAshrI32}, {0x18u, Opcode::VAshrrevI32},
{0x19u, Opcode::VLshlB32}, {0x1au, Opcode::VLshlrevB32},
{0x1bu, Opcode::VAndB32}, {0x1cu, Opcode::VOrB32},
{0x1du, Opcode::VXorB32}, {0x1eu, Opcode::VXnorB32},
{0x1fu, Opcode::VMacF32}, {0x20u, Opcode::VMadmkF32},
{0x21u, Opcode::VMadakF32}, {0x22u, Opcode::VBcntU32B32},
{0x23u, Opcode::VMbcntLoU32B32}, {0x24u, Opcode::VMbcntHiU32B32},
{0x25u, Opcode::VAddNcU32}, {0x28u, Opcode::VAddcU32},
{0x26u, Opcode::VSubNcU32}, {0x27u, Opcode::VSubrevNcU32},
{0x2bu, Opcode::VMacF32}, {0x2cu, Opcode::VMadmkF32},
{0x2du, Opcode::VMadakF32}, {0x2fu, Opcode::VCvtPkrtzF16F32},
{0x32u, Opcode::VAddF16}, {0x33u, Opcode::VSubF16},
{0x34u, Opcode::VSubrevF16}, {0x35u, Opcode::VMulF16},
{0x36u, Opcode::VFmacF16}, {0x37u, Opcode::VFmamkF16},
{0x38u, Opcode::VFmaakF16}, {0x39u, Opcode::VMaxF16},
{0x3au, Opcode::VMinF16}, {0x3cu, Opcode::VPkFmacF16},
struct Vop2OpcodeInfo {
uint32_t encoding = 0;
Opcode decoded = Opcode::Unknown;
Vop2SdwaProfile sdwa_profile = Vop2SdwaProfile::None;
};
constexpr Vop2OpcodeInfo VOP2_OPS[] = {
{0x01u, Opcode::VCndmaskB32, Vop2SdwaProfile::Cndmask},
{0x02u, Opcode::VDot2cF32F16},
{0x03u, Opcode::VAddF32, Vop2SdwaProfile::Float32},
{0x04u, Opcode::VSubF32, Vop2SdwaProfile::Float32},
{0x05u, Opcode::VSubrevF32},
{0x08u, Opcode::VMulF32, Vop2SdwaProfile::Float32},
{0x09u, Opcode::VMulI32I24, Vop2SdwaProfile::IntegerFullDestination},
{0x0bu, Opcode::VMulU32U24, Vop2SdwaProfile::IntegerFullDestination},
{0x0fu, Opcode::VMinF32},
{0x10u, Opcode::VMaxF32},
{0x11u, Opcode::VMinI32},
{0x12u, Opcode::VMaxI32},
{0x13u, Opcode::VMinU32, Vop2SdwaProfile::IntegerPartialDestination},
{0x14u, Opcode::VMaxU32, Vop2SdwaProfile::IntegerFullDestination},
{0x15u, Opcode::VLshrB32},
{0x16u, Opcode::VLshrrevB32, Vop2SdwaProfile::ReverseLogicalRight},
{0x17u, Opcode::VAshrI32},
{0x18u, Opcode::VAshrrevI32},
{0x19u, Opcode::VLshlB32},
{0x1au, Opcode::VLshlrevB32, Vop2SdwaProfile::ReverseLogicalLeft},
{0x1bu, Opcode::VAndB32, Vop2SdwaProfile::Bitwise},
{0x1cu, Opcode::VOrB32, Vop2SdwaProfile::Bitwise},
{0x1du, Opcode::VXorB32, Vop2SdwaProfile::Bitwise},
{0x1eu, Opcode::VXnorB32, Vop2SdwaProfile::Bitwise},
{0x1fu, Opcode::VMacF32},
{0x20u, Opcode::VMadmkF32},
{0x21u, Opcode::VMadakF32},
{0x22u, Opcode::VBcntU32B32},
{0x23u, Opcode::VMbcntLoU32B32},
{0x24u, Opcode::VMbcntHiU32B32},
{0x25u, Opcode::VAddNcU32, Vop2SdwaProfile::IntegerFullDestination},
{0x28u, Opcode::VAddcU32},
{0x26u, Opcode::VSubNcU32, Vop2SdwaProfile::IntegerPartialDestination},
{0x27u, Opcode::VSubrevNcU32, Vop2SdwaProfile::IntegerFullDestination},
{0x2bu, Opcode::VMacF32},
{0x2cu, Opcode::VMadmkF32},
{0x2du, Opcode::VMadakF32},
{0x2fu, Opcode::VCvtPkrtzF16F32},
{0x32u, Opcode::VAddF16, Vop2SdwaProfile::Float16},
{0x33u, Opcode::VSubF16, Vop2SdwaProfile::Float16},
{0x34u, Opcode::VSubrevF16, Vop2SdwaProfile::Float16},
{0x35u, Opcode::VMulF16, Vop2SdwaProfile::Float16},
{0x36u, Opcode::VFmacF16},
{0x37u, Opcode::VFmamkF16},
{0x38u, Opcode::VFmaakF16},
{0x39u, Opcode::VMaxF16, Vop2SdwaProfile::Float16},
{0x3au, Opcode::VMinF16, Vop2SdwaProfile::Float16},
{0x3cu, Opcode::VPkFmacF16},
};
static_assert(Detail::HasUniqueEncodings(VOP2_OPS));
constexpr Opcode LookupVop2Opcode(uint32_t encoding) {
return Detail::LookupOpcode(VOP2_OPS, encoding);
}
constexpr OpcodeMap VOP1_OPS[] = {
{0x00u, Opcode::VNop},
{0x01u, Opcode::VMovB32},
@@ -135,37 +183,59 @@ constexpr OpcodeMap VOP3_ENCODED_VOP1_OPS[] = {
{0x5eu, Opcode::VRndneF16},
};
constexpr OpcodeMap VOPC_OPS[] = {
{0x00u, Opcode::VCmpFF32}, {0x01u, Opcode::VCmpLtF32}, {0x02u, Opcode::VCmpEqF32},
{0x03u, Opcode::VCmpLeF32}, {0x04u, Opcode::VCmpGtF32}, {0x05u, Opcode::VCmpLgF32},
{0x06u, Opcode::VCmpGeF32}, {0x07u, Opcode::VCmpOF32}, {0x08u, Opcode::VCmpUF32},
{0x09u, Opcode::VCmpNgeF32}, {0x0au, Opcode::VCmpNlgF32}, {0x0bu, Opcode::VCmpNgtF32},
{0x0cu, Opcode::VCmpNleF32}, {0x0du, Opcode::VCmpNeqF32}, {0x0eu, Opcode::VCmpNltF32},
{0x0fu, Opcode::VCmpTruF32}, {0x11u, Opcode::VCmpxLtF32}, {0x12u, Opcode::VCmpxEqF32},
{0x13u, Opcode::VCmpxLeF32}, {0x14u, Opcode::VCmpxGtF32}, {0x15u, Opcode::VCmpxLgF32},
{0x16u, Opcode::VCmpxGeF32}, {0x19u, Opcode::VCmpxNgeF32}, {0x1au, Opcode::VCmpxNlgF32},
{0x1bu, Opcode::VCmpxNgtF32}, {0x1cu, Opcode::VCmpxNleF32}, {0x1du, Opcode::VCmpxNeqF32},
{0x1eu, Opcode::VCmpxNltF32}, {0x80u, Opcode::VCmpFI32}, {0x81u, Opcode::VCmpLtI32},
{0x82u, Opcode::VCmpEqI32}, {0x83u, Opcode::VCmpLeI32}, {0x84u, Opcode::VCmpGtI32},
{0x85u, Opcode::VCmpNeI32}, {0x86u, Opcode::VCmpGeI32}, {0x87u, Opcode::VCmpTI32},
{0x88u, Opcode::VCmpClassF32}, {0x89u, Opcode::VCmpLtI16}, {0x8au, Opcode::VCmpEqI16},
{0x8bu, Opcode::VCmpLeI16}, {0x8cu, Opcode::VCmpGtI16}, {0x8du, Opcode::VCmpNeI16},
{0x8eu, Opcode::VCmpGeI16}, {0x91u, Opcode::VCmpxLtI32}, {0x92u, Opcode::VCmpxEqI32},
{0x93u, Opcode::VCmpxLeI32}, {0x94u, Opcode::VCmpxGtI32}, {0x95u, Opcode::VCmpxNeI32},
{0x96u, Opcode::VCmpxGeI32}, {0xa9u, Opcode::VCmpLtU16}, {0xaau, Opcode::VCmpEqU16},
{0xabu, Opcode::VCmpLeU16}, {0xacu, Opcode::VCmpGtU16}, {0xadu, Opcode::VCmpNeU16},
{0xaeu, Opcode::VCmpGeU16}, {0xc0u, Opcode::VCmpFU32}, {0xc1u, Opcode::VCmpLtU32},
{0xc2u, Opcode::VCmpEqU32}, {0xc3u, Opcode::VCmpLeU32}, {0xc4u, Opcode::VCmpGtU32},
{0xc5u, Opcode::VCmpNeU32}, {0xc6u, Opcode::VCmpGeU32}, {0xc7u, Opcode::VCmpTU32},
{0xd1u, Opcode::VCmpxLtU32}, {0xd2u, Opcode::VCmpxEqU32}, {0xd3u, Opcode::VCmpxLeU32},
{0xd4u, Opcode::VCmpxGtU32}, {0xd5u, Opcode::VCmpxNeU32}, {0xd6u, Opcode::VCmpxGeU32},
{0xe5u, Opcode::VCmpNeU64}, {0xc9u, Opcode::VCmpLtF16}, {0xcau, Opcode::VCmpEqF16},
{0xcbu, Opcode::VCmpLeF16}, {0xccu, Opcode::VCmpGtF16}, {0xcdu, Opcode::VCmpLgF16},
{0xceu, Opcode::VCmpGeF16}, {0xedu, Opcode::VCmpNeqF16}, {0xd9u, Opcode::VCmpxLtF16},
{0xdau, Opcode::VCmpxEqF16}, {0xdbu, Opcode::VCmpxLeF16}, {0xdcu, Opcode::VCmpxGtF16},
{0xdeu, Opcode::VCmpxGeF16}, {0xfdu, Opcode::VCmpxNeqF16}, {0xfeu, Opcode::VCmpxNltF16},
struct VopcOpcodeInfo {
uint32_t encoding = 0;
Opcode decoded = Opcode::Unknown;
bool supports_dpp = true;
};
constexpr VopcOpcodeInfo VOPC_OPS[] = {
{0x00u, Opcode::VCmpFF32}, {0x01u, Opcode::VCmpLtF32},
{0x02u, Opcode::VCmpEqF32}, {0x03u, Opcode::VCmpLeF32},
{0x04u, Opcode::VCmpGtF32}, {0x05u, Opcode::VCmpLgF32},
{0x06u, Opcode::VCmpGeF32}, {0x07u, Opcode::VCmpOF32},
{0x08u, Opcode::VCmpUF32}, {0x09u, Opcode::VCmpNgeF32},
{0x0au, Opcode::VCmpNlgF32}, {0x0bu, Opcode::VCmpNgtF32},
{0x0cu, Opcode::VCmpNleF32}, {0x0du, Opcode::VCmpNeqF32},
{0x0eu, Opcode::VCmpNltF32}, {0x0fu, Opcode::VCmpTruF32},
{0x11u, Opcode::VCmpxLtF32}, {0x12u, Opcode::VCmpxEqF32},
{0x13u, Opcode::VCmpxLeF32}, {0x14u, Opcode::VCmpxGtF32},
{0x15u, Opcode::VCmpxLgF32}, {0x16u, Opcode::VCmpxGeF32},
{0x19u, Opcode::VCmpxNgeF32}, {0x1au, Opcode::VCmpxNlgF32},
{0x1bu, Opcode::VCmpxNgtF32}, {0x1cu, Opcode::VCmpxNleF32},
{0x1du, Opcode::VCmpxNeqF32}, {0x1eu, Opcode::VCmpxNltF32},
{0x80u, Opcode::VCmpFI32}, {0x81u, Opcode::VCmpLtI32},
{0x82u, Opcode::VCmpEqI32}, {0x83u, Opcode::VCmpLeI32},
{0x84u, Opcode::VCmpGtI32}, {0x85u, Opcode::VCmpNeI32},
{0x86u, Opcode::VCmpGeI32}, {0x87u, Opcode::VCmpTI32},
{0x88u, Opcode::VCmpClassF32}, {0x89u, Opcode::VCmpLtI16},
{0x8au, Opcode::VCmpEqI16}, {0x8bu, Opcode::VCmpLeI16},
{0x8cu, Opcode::VCmpGtI16}, {0x8du, Opcode::VCmpNeI16},
{0x8eu, Opcode::VCmpGeI16}, {0x91u, Opcode::VCmpxLtI32},
{0x92u, Opcode::VCmpxEqI32}, {0x93u, Opcode::VCmpxLeI32},
{0x94u, Opcode::VCmpxGtI32}, {0x95u, Opcode::VCmpxNeI32},
{0x96u, Opcode::VCmpxGeI32}, {0xa9u, Opcode::VCmpLtU16},
{0xaau, Opcode::VCmpEqU16}, {0xabu, Opcode::VCmpLeU16},
{0xacu, Opcode::VCmpGtU16}, {0xadu, Opcode::VCmpNeU16},
{0xaeu, Opcode::VCmpGeU16}, {0xc0u, Opcode::VCmpFU32},
{0xc1u, Opcode::VCmpLtU32}, {0xc2u, Opcode::VCmpEqU32},
{0xc3u, Opcode::VCmpLeU32}, {0xc4u, Opcode::VCmpGtU32},
{0xc5u, Opcode::VCmpNeU32}, {0xc6u, Opcode::VCmpGeU32},
{0xc7u, Opcode::VCmpTU32}, {0xa2u, Opcode::VCmpEqI64, false},
{0xd1u, Opcode::VCmpxLtU32}, {0xd2u, Opcode::VCmpxEqU32},
{0xd3u, Opcode::VCmpxLeU32}, {0xd4u, Opcode::VCmpxGtU32},
{0xd5u, Opcode::VCmpxNeU32}, {0xd6u, Opcode::VCmpxGeU32},
{0xe5u, Opcode::VCmpNeU64, false}, {0xc9u, Opcode::VCmpLtF16},
{0xcau, Opcode::VCmpEqF16}, {0xcbu, Opcode::VCmpLeF16},
{0xccu, Opcode::VCmpGtF16}, {0xcdu, Opcode::VCmpLgF16},
{0xceu, Opcode::VCmpGeF16}, {0xedu, Opcode::VCmpNeqF16},
{0xd9u, Opcode::VCmpxLtF16}, {0xdau, Opcode::VCmpxEqF16},
{0xdbu, Opcode::VCmpxLeF16}, {0xdcu, Opcode::VCmpxGtF16},
{0xdeu, Opcode::VCmpxGeF16}, {0xfdu, Opcode::VCmpxNeqF16},
{0xfeu, Opcode::VCmpxNltF16},
};
static_assert(Detail::HasUniqueEncodings(VOPC_OPS));
constexpr OpcodeMap VOP3_OPS[] = {
{0x141u, Opcode::VMadF32}, {0x142u, Opcode::VMadI32I24},
{0x143u, Opcode::VMadU32U24}, {0x176u, Opcode::VMadU64U32},
@@ -214,16 +284,6 @@ constexpr OpcodeMap VOP3P_OPS[] = {
{0x22u, Opcode::VMadMixhiF16},
};
Opcode Lookup(const OpcodeMap* ops, uint32_t count, uint32_t opcode) {
for (uint32_t i = 0; i < count; i++) {
const auto& op = ops[i];
if (op.opcode == opcode) {
return op.ir;
}
}
return Opcode::Unsupported;
}
bool IsVop2LiteralMadOpcode(uint32_t opcode) {
return opcode == 0x20u || opcode == 0x21u || opcode == 0x2cu || opcode == 0x2du;
}
@@ -234,24 +294,23 @@ bool IsUnsupportedVop3EncodedVop2Alias(uint32_t opcode) {
}
Opcode LookupVop3Opcode(uint32_t opcode) {
const auto direct = Lookup(VOP3_OPS, static_cast<uint32_t>(std::size(VOP3_OPS)), opcode);
const auto direct = Detail::LookupOpcode(VOP3_OPS, opcode);
if (direct != Opcode::Unsupported) {
return direct;
}
if (opcode <= 0xffu) {
return Lookup(VOPC_OPS, static_cast<uint32_t>(std::size(VOPC_OPS)), opcode);
return Detail::LookupOpcode(VOPC_OPS, opcode);
}
if (opcode >= 0x100u && opcode <= 0x13fu) {
if (IsUnsupportedVop3EncodedVop2Alias(opcode - 0x100u)) {
return Opcode::Unsupported;
}
return Lookup(VOP2_OPS, static_cast<uint32_t>(std::size(VOP2_OPS)), opcode - 0x100u);
return LookupVop2Opcode(opcode - 0x100u);
}
if (opcode >= 0x180u && opcode <= 0x1ffu) {
return Lookup(VOP3_ENCODED_VOP1_OPS,
static_cast<uint32_t>(std::size(VOP3_ENCODED_VOP1_OPS)), opcode - 0x180u);
return Detail::LookupOpcode(VOP3_ENCODED_VOP1_OPS, opcode - 0x180u);
}
return Lookup(VOP3_OPS, static_cast<uint32_t>(std::size(VOP3_OPS)), opcode);
return Detail::LookupOpcode(VOP3_OPS, opcode);
}
bool IsVop3EncodedVopc(uint32_t opcode) {
@@ -791,7 +850,6 @@ Vop2SdwaFields DecodeVop2SdwaFields(uint32_t modifier) {
}
struct Vop2SdwaRule {
Opcode opcode = Opcode::Unknown;
uint32_t dst_selectors = SdwaSelFull();
uint32_t src0_selectors = SdwaSelFull();
uint32_t src1_selectors = SdwaSelFull();
@@ -804,46 +862,27 @@ constexpr uint32_t SdwaSelAll() {
}
constexpr Vop2SdwaRule VOP2_SDWA_RULES[] = {
{Opcode::VCndmaskB32, SdwaSelWords(), SdwaSelWords() | SdwaSelFull(),
SdwaSelWords() | SdwaSelFull(), true, false},
{Opcode::VAddF32, SdwaSelFull(), SdwaSelFull(), SdwaSelFull(), false, true},
{Opcode::VSubF32, SdwaSelFull(), SdwaSelFull(), SdwaSelFull(), false, true},
{Opcode::VMulF32, SdwaSelFull(), SdwaSelFull(), SdwaSelFull(), false, true},
{Opcode::VAddF16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords() | SdwaSelFull(),
SdwaSelWords() | SdwaSelFull(), true, true},
{Opcode::VSubF16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords() | SdwaSelFull(),
SdwaSelWords() | SdwaSelFull(), true, true},
{Opcode::VSubrevF16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords() | SdwaSelFull(),
SdwaSelWords() | SdwaSelFull(), true, true},
{Opcode::VMulF16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords() | SdwaSelFull(),
SdwaSelWords() | SdwaSelFull(), true, true},
{Opcode::VMaxF16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords() | SdwaSelFull(),
SdwaSelWords() | SdwaSelFull(), true, true},
{Opcode::VMinF16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords() | SdwaSelFull(),
SdwaSelWords() | SdwaSelFull(), true, true},
{Opcode::VMulI32I24, SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), false, false},
{Opcode::VMulU32U24, SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), false, false},
{Opcode::VMinU32, SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), false, false},
{Opcode::VMaxU32, SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), false, false},
{Opcode::VLshrrevB32, SdwaSelFull(), SdwaSelFull(), SdwaSelWords() | SdwaSelFull(), false,
false},
{Opcode::VLshlrevB32, SdwaSelFull(), SdwaSelFull(), SdwaSelAll(), false, false},
{Opcode::VAndB32, SdwaSelWords() | SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), true, false},
{Opcode::VOrB32, SdwaSelWords() | SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), true, false},
{Opcode::VXorB32, SdwaSelWords() | SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), true, false},
{Opcode::VXnorB32, SdwaSelWords() | SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), true, false},
{Opcode::VAddNcU32, SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), false, false},
{Opcode::VSubNcU32, SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), false, false},
{Opcode::VSubrevNcU32, SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), false, false},
{},
{SdwaSelWords() | SdwaSelFull(), SdwaSelWords() | SdwaSelFull(), SdwaSelWords() | SdwaSelFull(),
true, false},
{SdwaSelFull(), SdwaSelFull(), SdwaSelFull(), false, true},
{SdwaSelWords() | SdwaSelFull(), SdwaSelWords() | SdwaSelFull(), SdwaSelWords() | SdwaSelFull(),
true, true},
{SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), false, false},
{SdwaSelAll(), SdwaSelAll(), SdwaSelAll(), true, false},
{SdwaSelFull(), SdwaSelFull(), SdwaSelWords() | SdwaSelFull(), false, false},
{SdwaSelFull(), SdwaSelFull(), SdwaSelAll(), false, false},
{SdwaSelWords() | SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), true, false},
};
static_assert(sizeof(VOP2_SDWA_RULES) / sizeof(VOP2_SDWA_RULES[0]) ==
static_cast<size_t>(Vop2SdwaProfile::Count));
const Vop2SdwaRule* FindVop2SdwaRule(Opcode opcode) {
for (const auto& rule: VOP2_SDWA_RULES) {
if (rule.opcode == opcode) {
return &rule;
}
const Vop2SdwaRule* FindVop2SdwaRule(uint32_t encoding) {
const auto* info = Detail::FindOpcode(VOP2_OPS, encoding);
if (info == nullptr || info->sdwa_profile == Vop2SdwaProfile::None) {
return nullptr;
}
return nullptr;
return &VOP2_SDWA_RULES[static_cast<size_t>(info->sdwa_profile)];
}
bool IsVop2SdwaDestinationSupported(const Vop2SdwaRule& rule, const Vop2SdwaFields& fields) {
@@ -881,7 +920,7 @@ bool ValidateVop2Sdwa(Instruction& inst, uint32_t opcode, const Vop2SdwaFields&
return true;
}
const auto* rule = FindVop2SdwaRule(inst.opcode);
const auto* rule = FindVop2SdwaRule(opcode);
if (rule == nullptr) {
SetUnsupported(inst, Family::VOP2, opcode,
"VOP2 SDWA modifier is not supported for opcode");
@@ -1135,6 +1174,11 @@ bool DecodeVopcDpp(uint32_t pc, std::span<const uint32_t> code, uint32_t word_in
SetUnsupported(inst, Family::VOPC, opcode, "VOPC opcode is not implemented");
return true;
}
const auto* info = Detail::FindOpcode(VOPC_OPS, opcode);
if (info == nullptr || !info->supports_dpp) {
SetUnsupported(inst, Family::VOPC, opcode, "VOPC DPP modifier is not supported for opcode");
return true;
}
if (!DecodeScalarSource(src0 + 256u, pc, inst.src0, error) ||
!DecodeVectorGpr(vsrc1, inst.src1, error)) {
return false;
@@ -1427,7 +1471,7 @@ bool DecodeVop2(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
inst.word = word;
inst.family = Family::VOP2;
inst.opcode_id = opcode;
inst.opcode = Lookup(VOP2_OPS, static_cast<uint32_t>(std::size(VOP2_OPS)), opcode);
inst.opcode = LookupVop2Opcode(opcode);
SetRawWords(inst, code, word_index, 1);
switch (opcode) {
@@ -1468,7 +1512,7 @@ bool DecodeVop1(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
inst.word = word;
inst.family = Family::VOP1;
inst.opcode_id = opcode;
inst.opcode = Lookup(VOP1_OPS, static_cast<uint32_t>(std::size(VOP1_OPS)), opcode);
inst.opcode = Detail::LookupOpcode(VOP1_OPS, opcode);
SetRawWords(inst, code, word_index, 1);
if (inst.opcode == Opcode::Unsupported) {
@@ -1509,7 +1553,7 @@ bool DecodeVopc(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
inst.word = word;
inst.family = Family::VOPC;
inst.opcode_id = opcode;
inst.opcode = Lookup(VOPC_OPS, static_cast<uint32_t>(std::size(VOPC_OPS)), opcode);
inst.opcode = Detail::LookupOpcode(VOPC_OPS, opcode);
inst.dst.kind = IsVopcCompareExec(inst.opcode) ? OperandKind::ExecLo : OperandKind::VccLo;
SetRawWords(inst, code, word_index, 1);
@@ -1723,7 +1767,7 @@ bool DecodeVop3p(uint32_t pc, std::span<const uint32_t> code, uint32_t word_inde
inst.word = word0;
inst.family = Family::VOP3P;
inst.opcode_id = opcode;
inst.opcode = Lookup(VOP3P_OPS, static_cast<uint32_t>(std::size(VOP3P_OPS)), opcode);
inst.opcode = Detail::LookupOpcode(VOP3P_OPS, opcode);
SetRawWords(inst, code, word_index, 2);
if (inst.opcode == Opcode::Unsupported) {
@@ -131,7 +131,9 @@ bool IsAtomic(IR::Opcode op) {
case IR::Opcode::AtomicUMaxU32:
case IR::Opcode::AtomicAndU32:
case IR::Opcode::AtomicOrU32:
case IR::Opcode::AtomicXorU32: return true;
case IR::Opcode::AtomicXorU32:
case IR::Opcode::AtomicFMinF32:
case IR::Opcode::AtomicFMaxF32: return true;
default: return false;
}
}
@@ -141,6 +143,9 @@ uint32_t BufferAddressOperandCount(const IR::Instruction& inst) {
}
bool ValidateInstructionContract(const IR::Instruction& inst, std::string* error) {
if (static_cast<size_t>(inst.op) >= static_cast<size_t>(IR::Opcode::Count)) {
return Fail(error, "IR instruction opcode is out of range");
}
if (inst.src_count > std::size(inst.src)) {
return Fail(error, "instruction source count exceeds fixed IR storage");
}
@@ -200,6 +205,12 @@ bool ValidateInstructionContract(const IR::Instruction& inst, std::string* error
inst.src_count != 2)) {
return Fail(error, "storage image instruction has an invalid resource class");
}
const auto float_atomic =
inst.op == IR::Opcode::AtomicFMinF32 || inst.op == IR::Opcode::AtomicFMaxF32;
if (float_atomic &&
(kind != IR::ResourceKind::Buffer || inst.src_count != buffer_address_count + 1u)) {
return Fail(error, "floating-point atomic instruction must target a buffer");
}
if (IsAtomic(inst.op) &&
!((kind == IR::ResourceKind::Buffer && inst.src_count == buffer_address_count + 1u) ||
((kind == IR::ResourceKind::Lds || kind == IR::ResourceKind::Gds) &&
@@ -389,7 +400,7 @@ bool ProgramRequiresExactSubgroupSize(const IR::Program& program) {
if (Emitter::InstructionHasDppSource(inst)) {
return true;
}
if (Emitter::IsCompareOpcode(inst.op) && !Emitter::IsSccOperand(inst.dst)) {
if (IR::IsCompareOpcode(inst.op) && !Emitter::IsSccOperand(inst.dst)) {
return true;
}
if ((IsExecDestination(inst.dst) || IsExecDestination(inst.dst2)) &&
@@ -488,6 +499,13 @@ bool EmitProgram(const IR::Program& program, const IR::ResourceSnapshot& resourc
AllocateBlockLabels(state, program);
AllocateDispatcherState(state, program);
EmitFunction(state, program);
if (state.unsupported_ir_instruction) {
char pc[9] {};
std::snprintf(pc, sizeof(pc), "%08x", state.unsupported_ir_pc);
return Fail(error, "SPIR-V emitter has no handler for IR opcode " +
std::string(IR::OpcodeName(state.unsupported_ir_opcode)) +
" at pc 0x" + pc);
}
ReportReserveExceeded(program, "registers", state.registers.size());
ReportReserveExceeded(program, "inputs", state.inputs.size());
@@ -1486,17 +1486,20 @@ void EmitBitCompareB32(EmitterState& state, const IR::Instruction& inst, bool bi
EmitStoreU32(state, inst.dst, ret);
}
void EmitCompareNeU64(EmitterState& state, const IR::Instruction& inst) {
const auto lhs_low = EmitSequentialValueLoad(state, inst.src[0], 0);
const auto lhs_high = EmitSequentialValueLoad(state, inst.src[0], 1);
const auto rhs_low = EmitSequentialValueLoad(state, inst.src[1], 0);
const auto rhs_high = EmitSequentialValueLoad(state, inst.src[1], 1);
const auto ne_low = state.builder.AllocateId();
const auto ne_high = state.builder.AllocateId();
const auto cond = state.builder.AllocateId();
state.builder.AddFunction({OpINotEqual, state.bool_type, ne_low, lhs_low, rhs_low});
state.builder.AddFunction({OpINotEqual, state.bool_type, ne_high, lhs_high, rhs_high});
state.builder.AddFunction({OpLogicalOr, state.bool_type, cond, ne_low, ne_high});
void EmitCompareU64(EmitterState& state, const IR::Instruction& inst) {
const bool equal = inst.op == IR::Opcode::CompareEqU64;
const auto lhs_low = EmitSequentialValueLoad(state, inst.src[0], 0);
const auto lhs_high = EmitSequentialValueLoad(state, inst.src[0], 1);
const auto rhs_low = EmitSequentialValueLoad(state, inst.src[1], 0);
const auto rhs_high = EmitSequentialValueLoad(state, inst.src[1], 1);
const auto low = state.builder.AllocateId();
const auto high = state.builder.AllocateId();
const auto cond = state.builder.AllocateId();
const auto compare_op = equal ? OpIEqual : OpINotEqual;
state.builder.AddFunction({compare_op, state.bool_type, low, lhs_low, rhs_low});
state.builder.AddFunction({compare_op, state.bool_type, high, lhs_high, rhs_high});
state.builder.AddFunction(
{equal ? OpLogicalAnd : OpLogicalOr, state.bool_type, cond, low, high});
EmitCompareResult(state, inst.dst, cond);
}
@@ -75,8 +75,6 @@ bool IsSccOperand(const IR::Operand& operand) {
return operand.kind == IR::OperandKind::Register && operand.reg.file == IR::RegisterFile::Scc;
}
bool IsCompareOpcode(IR::Opcode op);
void CollectMaskStateRegisters(std::vector<RegisterBinding>& registers) {
CollectRegister(registers, {IR::RegisterFile::Exec, 0});
CollectRegister(registers, {IR::RegisterFile::Exec, 1});
@@ -189,11 +187,11 @@ void CollectRegisters(const IR::Program& program, std::vector<RegisterBinding>&
if (inst.op == IR::Opcode::BitCountU64 || inst.op == IR::Opcode::FindMsbFromHighU64) {
CollectSequentialRegisters(registers, inst.src[0], 2);
}
if (inst.op == IR::Opcode::CompareNeU64) {
if (IR::IsCompare64Opcode(inst.op)) {
CollectSequentialRegisters(registers, inst.src[0], 2);
CollectSequentialRegisters(registers, inst.src[1], 2);
}
if (IsCompareOpcode(inst.op) && inst.dst.kind == IR::OperandKind::Register &&
if (IR::IsCompareOpcode(inst.op) && inst.dst.kind == IR::OperandKind::Register &&
inst.dst.reg.file != IR::RegisterFile::Scc) {
CollectSequentialRegisters(registers, inst.dst, 2);
}
@@ -384,93 +382,6 @@ bool ProgramNeedsSubgroupShuffle(const IR::Program& program) {
return false;
}
bool IsCompareOpcode(IR::Opcode op) {
switch (op) {
case IR::Opcode::CompareFalse:
case IR::Opcode::CompareTrue:
case IR::Opcode::CompareEqU32:
case IR::Opcode::CompareNeU32:
case IR::Opcode::CompareGtU32:
case IR::Opcode::CompareGeU32:
case IR::Opcode::CompareLtU32:
case IR::Opcode::CompareLeU32:
case IR::Opcode::CompareNeU64:
case IR::Opcode::CompareMaskEqU32:
case IR::Opcode::CompareMaskNeU32:
case IR::Opcode::CompareMaskGtU32:
case IR::Opcode::CompareMaskGeU32:
case IR::Opcode::CompareMaskLtU32:
case IR::Opcode::CompareMaskLeU32:
case IR::Opcode::CompareEqI32:
case IR::Opcode::CompareNeI32:
case IR::Opcode::CompareGtI32:
case IR::Opcode::CompareGeI32:
case IR::Opcode::CompareLtI32:
case IR::Opcode::CompareLeI32:
case IR::Opcode::CompareEqI16:
case IR::Opcode::CompareNeI16:
case IR::Opcode::CompareGtI16:
case IR::Opcode::CompareGeI16:
case IR::Opcode::CompareLtI16:
case IR::Opcode::CompareLeI16:
case IR::Opcode::CompareMaskEqI32:
case IR::Opcode::CompareMaskNeI32:
case IR::Opcode::CompareMaskGtI32:
case IR::Opcode::CompareMaskGeI32:
case IR::Opcode::CompareMaskLtI32:
case IR::Opcode::CompareMaskLeI32:
case IR::Opcode::CompareEqU16:
case IR::Opcode::CompareNeU16:
case IR::Opcode::CompareGtU16:
case IR::Opcode::CompareGeU16:
case IR::Opcode::CompareLtU16:
case IR::Opcode::CompareLeU16:
case IR::Opcode::CompareEqF32:
case IR::Opcode::CompareNeF32:
case IR::Opcode::CompareGtF32:
case IR::Opcode::CompareGeF32:
case IR::Opcode::CompareLtF32:
case IR::Opcode::CompareLeF32:
case IR::Opcode::CompareOrderedF32:
case IR::Opcode::CompareUnorderedF32:
case IR::Opcode::CompareUnordEqF32:
case IR::Opcode::CompareUnordNeF32:
case IR::Opcode::CompareUnordGtF32:
case IR::Opcode::CompareUnordGeF32:
case IR::Opcode::CompareUnordLtF32:
case IR::Opcode::CompareUnordLeF32:
case IR::Opcode::CompareClassF32:
case IR::Opcode::CompareEqF16:
case IR::Opcode::CompareNeF16:
case IR::Opcode::CompareGtF16:
case IR::Opcode::CompareGeF16:
case IR::Opcode::CompareLtF16:
case IR::Opcode::CompareLeF16:
case IR::Opcode::CompareUnordNeF16:
case IR::Opcode::CompareMaskEqF16:
case IR::Opcode::CompareMaskNeF16:
case IR::Opcode::CompareMaskGtF16:
case IR::Opcode::CompareMaskGeF16:
case IR::Opcode::CompareMaskLtF16:
case IR::Opcode::CompareMaskLeF16:
case IR::Opcode::CompareMaskUnordNeF16:
case IR::Opcode::CompareMaskUnordGeF16:
case IR::Opcode::CompareMaskEqF32:
case IR::Opcode::CompareMaskNeF32:
case IR::Opcode::CompareMaskGtF32:
case IR::Opcode::CompareMaskGeF32:
case IR::Opcode::CompareMaskLtF32:
case IR::Opcode::CompareMaskLeF32:
case IR::Opcode::CompareMaskUnordEqF32:
case IR::Opcode::CompareMaskUnordNeF32:
case IR::Opcode::CompareMaskUnordGtF32:
case IR::Opcode::CompareMaskUnordGeF32:
case IR::Opcode::CompareMaskUnordLtF32:
case IR::Opcode::CompareMaskUnordLeF32: return true;
default: return false;
}
}
bool ProgramNeedsSubgroupLocalInvocationId(const IR::Program& program) {
for (const auto& block: program.blocks) {
for (const auto& inst: block.instructions) {
@@ -481,7 +392,7 @@ bool ProgramNeedsSubgroupLocalInvocationId(const IR::Program& program) {
InstructionHasDppSource(inst)) {
return true;
}
if (IsCompareOpcode(inst.op)) {
if (IR::IsCompareOpcode(inst.op)) {
return true;
}
if (inst.dst.kind == IR::OperandKind::Register &&
@@ -516,7 +516,8 @@ void EmitInstruction(EmitterState& state, const IR::Instruction& inst) {
case IR::Opcode::CompareGeU32: EmitCompareU32(state, inst, OpUGreaterThanEqual); break;
case IR::Opcode::CompareLtU32: EmitCompareU32(state, inst, OpULessThan); break;
case IR::Opcode::CompareLeU32: EmitCompareU32(state, inst, OpULessThanEqual); break;
case IR::Opcode::CompareNeU64: EmitCompareNeU64(state, inst); break;
case IR::Opcode::CompareEqU64:
case IR::Opcode::CompareNeU64: EmitCompareU64(state, inst); break;
case IR::Opcode::CompareMaskEqU32: EmitCompareMaskU32(state, inst, OpIEqual); break;
case IR::Opcode::CompareMaskNeU32: EmitCompareMaskU32(state, inst, OpINotEqual); break;
case IR::Opcode::CompareMaskGtU32: EmitCompareMaskU32(state, inst, OpUGreaterThan); break;
@@ -774,6 +775,12 @@ void EmitInstruction(EmitterState& state, const IR::Instruction& inst) {
case IR::Opcode::AtomicXorU32:
EmitGuardedByExec(state, [&]() { EmitAtomicU32(state, inst, OpAtomicXor); });
break;
case IR::Opcode::AtomicFMinF32:
EmitGuardedByExec(state, [&]() { EmitAtomicFMinF32(state, inst); });
break;
case IR::Opcode::AtomicFMaxF32:
EmitGuardedByExec(state, [&]() { EmitAtomicFMaxF32(state, inst); });
break;
case IR::Opcode::FlatLoadUbyte: EmitFlatLoadUbyte(state, inst); break;
case IR::Opcode::FlatLoadSbyte: EmitFlatLoadSbyte(state, inst); break;
case IR::Opcode::FlatLoadUshort: EmitFlatLoadUshort(state, inst); break;
@@ -850,7 +857,13 @@ void EmitInstruction(EmitterState& state, const IR::Instruction& inst) {
}
EmitGuardedByExec(state, [&]() { EmitExport(state, inst); });
break;
default: break;
default:
if (!state.unsupported_ir_instruction) {
state.unsupported_ir_instruction = true;
state.unsupported_ir_opcode = inst.op;
state.unsupported_ir_pc = inst.pc;
}
break;
}
}
@@ -414,6 +414,9 @@ struct EmitterState {
bool needs_image_gather_extended = false;
bool needs_function_lds = false;
bool needs_pixel_valid_mask = false;
bool unsupported_ir_instruction = false;
IR::Opcode unsupported_ir_opcode = IR::Opcode::Count;
uint32_t unsupported_ir_pc = 0;
std::vector<RegisterBinding> registers;
std::vector<InputBinding> inputs;
std::vector<OutputBinding> outputs;
@@ -611,8 +614,6 @@ bool IsMaskRegisterFile(IR::RegisterFile file);
bool IsSccOperand(const IR::Operand& operand);
bool IsCompareOpcode(IR::Opcode op);
void CollectMaskStateRegisters(std::vector<RegisterBinding>& registers);
void CollectSequentialRegisters(std::vector<RegisterBinding>& registers, const IR::Operand& base,
@@ -651,8 +652,6 @@ bool ProgramNeedsSubgroupBallot(const IR::Program& program);
bool ProgramNeedsSubgroupShuffle(const IR::Program& program);
bool IsCompareOpcode(IR::Opcode op);
bool ProgramNeedsSubgroupLocalInvocationId(const IR::Program& program);
uint32_t PointerForRegister(const EmitterState& state, IR::Register reg);
@@ -1015,6 +1014,9 @@ void EmitDeviceAtomicMemoryBarrier(EmitterState& state);
void EmitAtomicU32(EmitterState& state, const IR::Instruction& inst, uint32_t opcode);
void EmitAtomicFMinF32(EmitterState& state, const IR::Instruction& inst);
void EmitAtomicFMaxF32(EmitterState& state, const IR::Instruction& inst);
void EmitSLoadDword(EmitterState& state, const IR::Instruction& inst);
void EmitLoadSrtDword(EmitterState& state, const IR::Instruction& inst);
@@ -1345,7 +1347,7 @@ void EmitCompareI16(EmitterState& state, const IR::Instruction& inst, uint32_t o
void EmitBitCompareB32(EmitterState& state, const IR::Instruction& inst, bool bit_set);
void EmitCompareNeU64(EmitterState& state, const IR::Instruction& inst);
void EmitCompareU64(EmitterState& state, const IR::Instruction& inst);
void EmitCompareConstant(EmitterState& state, const IR::Instruction& inst, bool value);
@@ -1577,19 +1579,22 @@ uint32_t EmitValueOrZeroIfCondition(EmitterState& state, uint32_t condition, Fn&
}
const auto then_label = state.builder.AllocateId();
const auto then_exit = state.builder.AllocateId();
const auto else_label = state.builder.AllocateId();
const auto merge_label = state.builder.AllocateId();
state.builder.AddFunction({OpSelectionMerge, merge_label, SelectionControlNone});
state.builder.AddFunction({OpBranchConditional, condition, then_label, else_label});
state.builder.AddFunction({OpLabel, then_label});
const auto then_value = fn();
state.builder.AddFunction({OpBranch, then_exit});
state.builder.AddFunction({OpLabel, then_exit});
state.builder.AddFunction({OpBranch, merge_label});
state.builder.AddFunction({OpLabel, else_label});
state.builder.AddFunction({OpBranch, merge_label});
state.builder.AddFunction({OpLabel, merge_label});
const auto value = state.builder.AllocateId();
state.builder.AddFunction(
{OpPhi, state.uint_type, value, then_value, then_label, ConstantU32(state, 0), else_label});
{OpPhi, state.uint_type, value, then_value, then_exit, ConstantU32(state, 0), else_label});
return value;
}
@@ -640,8 +640,8 @@ void EmitMemoryStoreU32(EmitterState& state, const IR::Instruction& inst, IR::Re
}
template <typename Fn>
void EmitAtomicUpdateU32(EmitterState& state, uint32_t pointer, IR::ResourceKind kind,
Fn&& desired_value) {
uint32_t EmitAtomicUpdateU32(EmitterState& state, uint32_t pointer, IR::ResourceKind kind,
Fn&& desired_value) {
const auto scope = kind == IR::ResourceKind::Lds ? ScopeWorkgroup : ScopeDevice;
const auto memory = kind == IR::ResourceKind::Lds ? MemorySemanticsWorkgroupMemory
: MemorySemanticsUniformMemory;
@@ -675,6 +675,7 @@ void EmitAtomicUpdateU32(EmitterState& state, uint32_t pointer, IR::ResourceKind
const auto semantics = MemorySemanticsAcquireRelease | memory;
state.builder.AddFunction(
{OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, semantics)});
return observed;
}
void EmitMemoryStoreSubDwordU32(EmitterState& state, const IR::Instruction& inst,
@@ -1181,6 +1182,79 @@ void EmitAtomicU32(EmitterState& state, const IR::Instruction& inst, uint32_t op
EmitStoreU32(state, inst.dst, old);
}
namespace {
uint32_t EmitF32BitsOrderedCompare(EmitterState& state, uint32_t lhs_bits, uint32_t rhs_bits,
uint32_t comparison_opcode) {
struct ClassifiedBits {
uint32_t nan = 0;
uint32_t zero = 0;
uint32_t key = 0;
};
const auto Classify = [&](uint32_t bits) {
ClassifiedBits cls;
const auto abs_bits = EmitAndConstant(state, bits, 0x7fffffffu);
const auto exponent_bits = EmitAndConstant(state, abs_bits, 0x7f800000u);
const auto mantissa_bits = EmitAndConstant(state, abs_bits, 0x007fffffu);
const auto exponent_max =
EmitCompareU32Constant(state, OpIEqual, exponent_bits, 0x7f800000u);
const auto mantissa_nonzero = EmitCompareU32Constant(state, OpINotEqual, mantissa_bits, 0);
const auto negative = EmitCompareU32Constant(state, OpINotEqual,
EmitAndConstant(state, bits, 0x80000000u), 0);
const auto negative_key = state.builder.AllocateId();
const auto positive_key = state.builder.AllocateId();
// Map all non-NaN IEEE-754 encodings to monotonically increasing unsigned keys.
state.builder.AddFunction({OpNot, state.uint_type, negative_key, bits});
state.builder.AddFunction(
{OpBitwiseXor, state.uint_type, positive_key, bits, ConstantU32(state, 0x80000000u)});
cls.nan = EmitLogicalAndBool(state, exponent_max, mantissa_nonzero);
cls.zero = EmitCompareU32Constant(state, OpIEqual, abs_bits, 0);
cls.key = EmitSelectValueU32(state, negative, negative_key, positive_key);
return cls;
};
const auto lhs = Classify(lhs_bits);
const auto rhs = Classify(rhs_bits);
const auto any_nan = EmitLogicalOrBool(state, lhs.nan, rhs.nan);
// The key transform orders -0 below +0, while the floating comparison treats them as equal.
const auto both_zero = EmitLogicalAndBool(state, lhs.zero, rhs.zero);
const auto ordered_nonzero =
EmitLogicalNotBool(state, EmitLogicalOrBool(state, any_nan, both_zero));
const auto comparison = state.builder.AllocateId();
state.builder.AddFunction({comparison_opcode, state.bool_type, comparison, lhs.key, rhs.key});
return EmitLogicalAndBool(state, ordered_nonzero, comparison);
}
void EmitAtomicFMinMaxF32(EmitterState& state, const IR::Instruction& inst,
uint32_t comparison_opcode) {
const auto index =
EmitMemoryDwordIndex(state, inst, inst.memory, 1, AddressSourceCount(inst, 1));
const auto in_bounds = EmitStorageBufferElementInBounds(state, inst.memory, index, inst.pc);
const auto src_u32 = EmitValueLoad(state, inst.src[0]);
const auto old = EmitValueOrZeroIfCondition(state, in_bounds, [&]() {
const auto pointer = EmitStorageBufferElementPointer(state, inst.memory, index, inst.pc);
return EmitAtomicUpdateU32(state, pointer, inst.memory.kind, [&](uint32_t old_u32) {
const auto replace_old = state.builder.AllocateId();
const auto replace =
EmitF32BitsOrderedCompare(state, src_u32, old_u32, comparison_opcode);
state.builder.AddFunction(
{OpSelect, state.uint_type, replace_old, replace, src_u32, old_u32});
return replace_old;
});
});
EmitStoreU32(state, inst.dst, old);
}
} // namespace
void EmitAtomicFMinF32(EmitterState& state, const IR::Instruction& inst) {
EmitAtomicFMinMaxF32(state, inst, OpULessThan);
}
void EmitAtomicFMaxF32(EmitterState& state, const IR::Instruction& inst) {
EmitAtomicFMinMaxF32(state, inst, OpUGreaterThan);
}
void EmitSLoadDword(EmitterState& state, const IR::Instruction& inst) {
if (state.address_memory_variable == 0) {
ExitDescriptorBindingFailure(state, IR::DescriptorBindingKind::AddressMemory,
@@ -30,7 +30,9 @@ bool IsAtomic(Opcode op) {
case Opcode::AtomicUMaxU32:
case Opcode::AtomicAndU32:
case Opcode::AtomicOrU32:
case Opcode::AtomicXorU32: return true;
case Opcode::AtomicXorU32:
case Opcode::AtomicFMinF32:
case Opcode::AtomicFMaxF32: return true;
default: return false;
}
}
+44 -12
View File
@@ -490,13 +490,26 @@ bool IsScalarMinMaxOpcode(Decoder::Opcode opcode) {
}
}
bool ResolveIrOpcode(Decoder::Opcode decoded, Opcode& lowered, std::string* error) {
const auto opcode = LookupIrOpcode(decoded);
if (!opcode.has_value()) {
if (error != nullptr) {
*error = fmt::format("decoded opcode has no IR lowering: {}",
Decoder::OpcodeToString(decoded));
}
return false;
}
lowered = *opcode;
return true;
}
bool LowerScalarMinMaxWithScc(const Decoder::Instruction& decoded, BasicBlock& block,
std::string* error) {
Instruction result;
result.pc = decoded.pc;
result.op = LookupIrOpcode(decoded.opcode);
result.src_count = 2;
if (!LowerRegisterOperand(decoded.dst, result.dst, error) ||
if (!ResolveIrOpcode(decoded.opcode, result.op, error) ||
!LowerRegisterOperand(decoded.dst, result.dst, error) ||
!LowerSourceOperand(decoded.src0, result.src[0], error) ||
!LowerSourceOperand(decoded.src1, result.src[1], error)) {
return false;
@@ -962,7 +975,12 @@ bool LowerControlInstruction(const Decoder::Instruction& decoded, BasicBlock& bl
return LowerControlMarker(decoded, block, Opcode::TtraceData, true, error);
case Decoder::Opcode::SInstPrefetch:
return LowerControlMarker(decoded, block, Opcode::InstPrefetch, true, error);
default: return false;
default:
if (error != nullptr) {
*error = fmt::format("control opcode has no specialized IR lowering: {}",
Decoder::OpcodeToString(decoded.opcode));
}
return false;
}
}
@@ -1051,6 +1069,18 @@ bool IsTerminatorOpcode(Decoder::Opcode opcode) {
bool LowerImplemented(const Decoder::Instruction& decoded, BasicBlock& block, std::string* error);
bool IsMemoryFamily(Decoder::Family family) {
switch (family) {
case Decoder::Family::SMEM:
case Decoder::Family::MUBUF:
case Decoder::Family::MTBUF:
case Decoder::Family::FLAT:
case Decoder::Family::DS:
case Decoder::Family::MIMG: return true;
default: return false;
}
}
bool LowerDecodedInstruction(const Decoder::Instruction& inst, BasicBlock& block,
std::string* error) {
switch (inst.opcode) {
@@ -1104,6 +1134,15 @@ bool LowerDecodedInstruction(const Decoder::Instruction& inst, BasicBlock& block
if (IsMemoryOpcode(inst.opcode)) {
return LowerMemoryInstruction(inst, block, error);
}
// Memory instructions require specialized lowering to populate MemoryInfo. Never let a
// newly decoded memory-family opcode fall through to generic register-only IR.
if (IsMemoryFamily(inst.family)) {
if (error != nullptr) {
*error = fmt::format("decoded memory-family opcode has no specialized IR lowering: {}",
Decoder::OpcodeToString(inst.opcode));
}
return false;
}
if (ScalarShiftLeftAddAmount(inst.opcode) != 0) {
return LowerScalarShiftLeftAdd(inst, block, error);
}
@@ -1116,23 +1155,16 @@ bool LowerDecodedInstruction(const Decoder::Instruction& inst, BasicBlock& block
if (VectorByteConvertIndex(inst.opcode) <= 3u) {
return LowerVectorByteConvert(inst, block, error);
}
if (!IsImplemented(inst.opcode)) {
if (error != nullptr) {
*error = fmt::format("decoded opcode has no IR lowering yet: {}",
Decoder::InstructionToString(inst).c_str());
}
return false;
}
return LowerImplemented(inst, block, error);
}
bool LowerImplemented(const Decoder::Instruction& decoded, BasicBlock& block, std::string* error) {
Instruction inst;
inst.pc = decoded.pc;
inst.op = LookupIrOpcode(decoded.opcode);
inst.src_count = decoded.src_count;
if (!LowerRegisterOperand(decoded.dst, inst.dst, error)) {
if (!ResolveIrOpcode(decoded.opcode, inst.op, error) ||
!LowerRegisterOperand(decoded.dst, inst.dst, error)) {
return false;
}
ApplyDppDestinationMask(decoded, inst.dst);
+35 -337
View File
@@ -8,350 +8,48 @@
#include "graphics/shader/shader.h"
#include <array>
#include <string_view>
#include <vector>
namespace Libs::Graphics::ShaderRecompiler::IR {
enum class Opcode {
ControlNop,
Waitcnt,
Barrier,
Sendmsg,
TtraceData,
InstPrefetch,
MoveU32,
MoveF32Bits,
MoveRelDestU32,
MoveRelSourceU32,
MoveU64,
WqmB64,
SaveexecB32,
SaveexecB64,
ReadFirstLaneU32,
ReadLaneU32,
WriteLaneU32,
Permlane16B32,
Permlanex16B32,
AbsI32,
IAddU32,
IAddCarryU32,
ISubBorrowU32,
ScalarAddCarryU32,
ScalarSubBorrowU32,
ScalarSubBorrowCarryU32,
ScalarSignedAddOverflowI32,
ScalarSignedSubOverflowI32,
ScalarShiftLeftAddCarryU32,
ISubU32,
IMulU32,
UMulHighU32,
SMulHighI32,
IMadI24U32,
UMadU24U32,
UMadU64U32,
SadU32,
IAdd3U32,
IMulI24U32,
UMulU24U32,
IMinI32,
IMaxI32,
IMin3I32,
IMax3I32,
IMed3I32,
UMinU32,
UMaxU32,
UMin3U32,
UMax3U32,
UMed3U32,
BitwiseAndU32,
BitwiseAndU64,
BitwiseAndNotU32,
BitwiseAndNotU64,
BitwiseOrU32,
BitwiseOrU64,
BitwiseOrNotU32,
BitwiseOrNotU64,
BitwiseAndOrU32,
BitwiseOr3U32,
BitwiseXorU32,
BitwiseXorU64,
BitwiseXor3U32,
BitwiseNandU32,
BitwiseNandU64,
BitwiseNorU32,
BitwiseNorU64,
BitwiseXnorU32,
BitwiseXnorU64,
BitwiseNotU32,
BitwiseNotU64,
BitClearU32,
BitSetU32,
BitReverseU32,
BitCountU32,
BitCountU64,
BitReplicateB64B32,
BitCountAddU32,
MaskedBitCountLowU32,
MaskedBitCountHighU32,
FindLsbU32,
FindMsbFromHighU32,
FindMsbFromHighU64,
BitFieldMaskU32,
BitFieldMaskU64,
BitFieldExtractU32,
BitFieldExtractU64,
BitFieldExtract3U32,
BitFieldExtract3I32,
BitFieldInsertSelectU32,
BitCompare0B32,
BitCompare1B32,
AlignBitU32,
ShiftLeftAddU32,
AddShiftLeftU32,
XorAddU32,
ShiftLeftOrU32,
ShiftLeftLogicalU32,
ShiftLeftLogicalU64,
ShiftLeftLogicalU16,
ShiftRightLogicalU32,
ShiftRightLogicalU64,
ShiftRightLogicalU16,
ShiftRightArithmeticI32,
ShiftRightArithmeticI16,
SelectU32,
SelectMaskU32,
SelectF32Bits,
SelectMaskF32Bits,
SelectU64,
PackLowLowU16,
PackLowHighU16,
PackHighHighU16,
CompareFalse,
CompareTrue,
CompareEqU32,
CompareNeU32,
CompareGtU32,
CompareGeU32,
CompareLtU32,
CompareLeU32,
CompareNeU64,
CompareMaskEqU32,
CompareMaskNeU32,
CompareMaskGtU32,
CompareMaskGeU32,
CompareMaskLtU32,
CompareMaskLeU32,
CompareEqI32,
CompareNeI32,
CompareGtI32,
CompareGeI32,
CompareLtI32,
CompareLeI32,
CompareEqI16,
CompareNeI16,
CompareGtI16,
CompareGeI16,
CompareLtI16,
CompareLeI16,
CompareMaskEqI32,
CompareMaskNeI32,
CompareMaskGtI32,
CompareMaskGeI32,
CompareMaskLtI32,
CompareMaskLeI32,
CompareEqU16,
CompareNeU16,
CompareGtU16,
CompareGeU16,
CompareLtU16,
CompareLeU16,
CompareEqF32,
CompareNeF32,
CompareGtF32,
CompareGeF32,
CompareLtF32,
CompareLeF32,
CompareOrderedF32,
CompareUnorderedF32,
CompareUnordEqF32,
CompareUnordNeF32,
CompareUnordGtF32,
CompareUnordGeF32,
CompareUnordLtF32,
CompareUnordLeF32,
CompareClassF32,
CompareEqF16,
CompareNeF16,
CompareGtF16,
CompareGeF16,
CompareLtF16,
CompareLeF16,
CompareUnordNeF16,
CompareMaskEqF16,
CompareMaskNeF16,
CompareMaskGtF16,
CompareMaskGeF16,
CompareMaskLtF16,
CompareMaskLeF16,
CompareMaskUnordNeF16,
CompareMaskUnordGeF16,
CompareMaskEqF32,
CompareMaskNeF32,
CompareMaskGtF32,
CompareMaskGeF32,
CompareMaskLtF32,
CompareMaskLeF32,
CompareMaskUnordEqF32,
CompareMaskUnordNeF32,
CompareMaskUnordGtF32,
CompareMaskUnordGeF32,
CompareMaskUnordLtF32,
CompareMaskUnordLeF32,
ConvertByteU32ToF32,
ConvertU32ToF32,
ConvertI32ToF32,
ConvertF32ToU32,
ConvertF32ToI32,
ConvertF32ToF16,
ConvertF16ToF32,
ConvertU16ToF16,
ConvertF16ToU16,
ConvertI16ToF16,
ConvertF16ToI16,
ConvertRoundPlusInfF32ToI32,
ConvertFloorF32ToI32,
ConvertI4ToOffsetF32,
LdexpF32,
PackF32ToF16Rtz,
PackSnorm2x16F32,
PackUnorm2x16F32,
PackU16U32,
PackU8F32,
PackB32F16,
PackedMadI16,
PackedMulLoU16,
PackedAddI16,
PackedSubI16,
PackedLshlrevB16,
PackedLshrrevB16,
PackedAshrrevI16,
PackedMaxI16,
PackedMinI16,
PackedMadU16,
PackedAddU16,
PackedSubU16,
PackedMaxU16,
PackedMinU16,
PackedAddF16,
PackedMulF16,
PackedMinF16,
PackedMaxF16,
PackedFmaF16,
AddF16,
SubF16,
MulF16,
MinF16,
MaxF16,
FmaF16,
MadMixF16,
IAddU16,
ISubI16,
IMinI16,
IMaxI16,
UMinU16,
UMaxU16,
RcpF32,
FractF32,
TruncF32,
CeilF32,
RoundEvenF32,
FloorF32,
Exp2F32,
Log2F32,
InverseSqrtF32,
SqrtF32,
RcpF16,
SqrtF16,
InverseSqrtF16,
Log2F16,
Exp2F16,
FloorF16,
CeilF16,
TruncF16,
RoundEvenF16,
SinF32,
CosF32,
CubeIdF32,
CubeScF32,
CubeTcF32,
CubeMaF32,
FAddF32,
FSubF32,
FMulF32,
FMinF32,
FMaxF32,
FMadF32,
Dot2AccF32F16,
FMin3F32,
FMax3F32,
FMed3F32,
Min3F16,
Max3F16,
Med3F16,
LoadSrtDword,
SLoadDword,
SBufferLoadDword,
BufferLoadUbyte,
BufferLoadSbyte,
BufferLoadUshort,
BufferLoadSshort,
BufferLoadDword,
BufferStoreByte,
BufferStoreShort,
BufferStoreDword,
AtomicSwapU32,
AtomicAddU32,
AtomicSubU32,
AtomicSMinI32,
AtomicUMinU32,
AtomicSMaxI32,
AtomicUMaxU32,
AtomicAndU32,
AtomicOrU32,
AtomicXorU32,
FlatLoadUbyte,
FlatLoadSbyte,
FlatLoadUshort,
FlatLoadSshort,
FlatLoadDword,
FlatStoreByte,
FlatStoreShort,
FlatStoreDword,
DsReadUbyte,
DsReadSbyte,
DsReadUshort,
DsReadSshort,
DsReadB32,
DsWriteByte,
DsWriteShort,
DsWriteB32,
DsMinF32,
DsMaxF32,
DsSwizzleB32,
DsConsume,
DsAppend,
DsWriteAddtidB32,
DsReadAddtidB32,
ImageGetResinfo,
ImageGetLod,
ImageLoad,
ImageStore,
ImageSample,
ImageGather4,
LoadInputF32,
Export,
#define IR_OPCODE(name, category) name,
#include "graphics/shader/recompiler/ir/ShaderIROpcodes.inc"
#undef IR_OPCODE
Count,
};
enum class OpcodeClass { General, Compare, Compare64 };
struct OpcodeInfo {
std::string_view name;
OpcodeClass opcode_class = OpcodeClass::General;
};
inline constexpr OpcodeInfo OPCODE_INFO[] = {
#define IR_OPCODE(name, category) {#name, OpcodeClass::category},
#include "graphics/shader/recompiler/ir/ShaderIROpcodes.inc"
#undef IR_OPCODE
};
static_assert(sizeof(OPCODE_INFO) / sizeof(OPCODE_INFO[0]) == static_cast<size_t>(Opcode::Count));
constexpr const OpcodeInfo& GetOpcodeInfo(Opcode opcode) {
return OPCODE_INFO[static_cast<size_t>(opcode)];
}
constexpr std::string_view OpcodeName(Opcode opcode) {
return GetOpcodeInfo(opcode).name;
}
constexpr bool IsCompareOpcode(Opcode opcode) {
return GetOpcodeInfo(opcode).opcode_class != OpcodeClass::General;
}
constexpr bool IsCompare64Opcode(Opcode opcode) {
return GetOpcodeInfo(opcode).opcode_class == OpcodeClass::Compare64;
}
enum class RegisterFile { Scalar, Vector, Vcc, Exec, Scc, M0 };
struct Register {
@@ -3,16 +3,17 @@
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include <optional>
namespace Libs::Graphics::ShaderRecompiler::IR {
void SetError(std::string* error, const char* message);
Opcode LookupIrOpcode(Decoder::Opcode opcode);
bool IsImplemented(Decoder::Opcode opcode);
bool IsReversedBinary(Decoder::Opcode opcode);
bool IsVectorCarryOutOpcode(Decoder::Opcode opcode);
bool ScalarResultWritesSccNonZero(Decoder::Opcode opcode);
bool ScalarResultIs64Bit(Decoder::Opcode opcode);
std::optional<Opcode> LookupIrOpcode(Decoder::Opcode opcode);
bool IsReversedBinary(Decoder::Opcode opcode);
bool IsVectorCarryOutOpcode(Decoder::Opcode opcode);
bool ScalarResultWritesSccNonZero(Decoder::Opcode opcode);
bool ScalarResultIs64Bit(Decoder::Opcode opcode);
Operand MakeSccOperand();
Operand MakeImmediateU32(uint32_t value);
@@ -157,345 +157,7 @@ std::string InstructionToString(const Instruction& inst) {
text += ExportInfoToString(inst.export_info);
return text;
}
switch (inst.op) {
case Opcode::ControlNop: text += "ControlNop"; break;
case Opcode::Waitcnt: text += "Waitcnt"; break;
case Opcode::Barrier: text += "Barrier"; break;
case Opcode::Sendmsg: text += "Sendmsg"; break;
case Opcode::TtraceData: text += "TtraceData"; break;
case Opcode::InstPrefetch: text += "InstPrefetch"; break;
case Opcode::MoveU32: text += "MoveU32"; break;
case Opcode::MoveF32Bits: text += "MoveF32Bits"; break;
case Opcode::MoveRelDestU32: text += "MoveRelDestU32"; break;
case Opcode::MoveRelSourceU32: text += "MoveRelSourceU32"; break;
case Opcode::MoveU64: text += "MoveU64"; break;
case Opcode::WqmB64: text += "WqmB64"; break;
case Opcode::SaveexecB32: text += "SaveexecB32"; break;
case Opcode::SaveexecB64: text += "SaveexecB64"; break;
case Opcode::ReadFirstLaneU32: text += "ReadFirstLaneU32"; break;
case Opcode::ReadLaneU32: text += "ReadLaneU32"; break;
case Opcode::WriteLaneU32: text += "WriteLaneU32"; break;
case Opcode::Permlane16B32: text += "Permlane16B32"; break;
case Opcode::Permlanex16B32: text += "Permlanex16B32"; break;
case Opcode::AbsI32: text += "AbsI32"; break;
case Opcode::IAddU32: text += "IAddU32"; break;
case Opcode::IAddCarryU32: text += "IAddCarryU32"; break;
case Opcode::ISubBorrowU32: text += "ISubBorrowU32"; break;
case Opcode::ScalarAddCarryU32: text += "ScalarAddCarryU32"; break;
case Opcode::ScalarSubBorrowU32: text += "ScalarSubBorrowU32"; break;
case Opcode::ScalarSubBorrowCarryU32: text += "ScalarSubBorrowCarryU32"; break;
case Opcode::ScalarSignedAddOverflowI32: text += "ScalarSignedAddOverflowI32"; break;
case Opcode::ScalarSignedSubOverflowI32: text += "ScalarSignedSubOverflowI32"; break;
case Opcode::ScalarShiftLeftAddCarryU32: text += "ScalarShiftLeftAddCarryU32"; break;
case Opcode::ISubU32: text += "ISubU32"; break;
case Opcode::IMulU32: text += "IMulU32"; break;
case Opcode::UMulHighU32: text += "UMulHighU32"; break;
case Opcode::SMulHighI32: text += "SMulHighI32"; break;
case Opcode::IMadI24U32: text += "IMadI24U32"; break;
case Opcode::UMadU24U32: text += "UMadU24U32"; break;
case Opcode::UMadU64U32: text += "UMadU64U32"; break;
case Opcode::SadU32: text += "SadU32"; break;
case Opcode::IAdd3U32: text += "IAdd3U32"; break;
case Opcode::IMulI24U32: text += "IMulI24U32"; break;
case Opcode::UMulU24U32: text += "UMulU24U32"; break;
case Opcode::IMinI32: text += "IMinI32"; break;
case Opcode::IMaxI32: text += "IMaxI32"; break;
case Opcode::IMin3I32: text += "IMin3I32"; break;
case Opcode::IMax3I32: text += "IMax3I32"; break;
case Opcode::IMed3I32: text += "IMed3I32"; break;
case Opcode::UMinU32: text += "UMinU32"; break;
case Opcode::UMaxU32: text += "UMaxU32"; break;
case Opcode::UMin3U32: text += "UMin3U32"; break;
case Opcode::UMax3U32: text += "UMax3U32"; break;
case Opcode::UMed3U32: text += "UMed3U32"; break;
case Opcode::BitwiseAndU32: text += "BitwiseAndU32"; break;
case Opcode::BitwiseAndU64: text += "BitwiseAndU64"; break;
case Opcode::BitwiseAndNotU32: text += "BitwiseAndNotU32"; break;
case Opcode::BitwiseAndNotU64: text += "BitwiseAndNotU64"; break;
case Opcode::BitwiseOrU32: text += "BitwiseOrU32"; break;
case Opcode::BitwiseOrU64: text += "BitwiseOrU64"; break;
case Opcode::BitwiseOrNotU32: text += "BitwiseOrNotU32"; break;
case Opcode::BitwiseOrNotU64: text += "BitwiseOrNotU64"; break;
case Opcode::BitwiseAndOrU32: text += "BitwiseAndOrU32"; break;
case Opcode::BitwiseOr3U32: text += "BitwiseOr3U32"; break;
case Opcode::BitwiseXorU32: text += "BitwiseXorU32"; break;
case Opcode::BitwiseXorU64: text += "BitwiseXorU64"; break;
case Opcode::BitwiseXor3U32: text += "BitwiseXor3U32"; break;
case Opcode::BitwiseNandU32: text += "BitwiseNandU32"; break;
case Opcode::BitwiseNandU64: text += "BitwiseNandU64"; break;
case Opcode::BitwiseNorU32: text += "BitwiseNorU32"; break;
case Opcode::BitwiseNorU64: text += "BitwiseNorU64"; break;
case Opcode::BitwiseXnorU32: text += "BitwiseXnorU32"; break;
case Opcode::BitwiseXnorU64: text += "BitwiseXnorU64"; break;
case Opcode::BitwiseNotU32: text += "BitwiseNotU32"; break;
case Opcode::BitwiseNotU64: text += "BitwiseNotU64"; break;
case Opcode::BitClearU32: text += "BitClearU32"; break;
case Opcode::BitSetU32: text += "BitSetU32"; break;
case Opcode::BitReverseU32: text += "BitReverseU32"; break;
case Opcode::BitCountU32: text += "BitCountU32"; break;
case Opcode::BitCountU64: text += "BitCountU64"; break;
case Opcode::BitReplicateB64B32: text += "BitReplicateB64B32"; break;
case Opcode::BitCountAddU32: text += "BitCountAddU32"; break;
case Opcode::MaskedBitCountLowU32: text += "MaskedBitCountLowU32"; break;
case Opcode::MaskedBitCountHighU32: text += "MaskedBitCountHighU32"; break;
case Opcode::FindLsbU32: text += "FindLsbU32"; break;
case Opcode::FindMsbFromHighU32: text += "FindMsbFromHighU32"; break;
case Opcode::FindMsbFromHighU64: text += "FindMsbFromHighU64"; break;
case Opcode::BitFieldMaskU32: text += "BitFieldMaskU32"; break;
case Opcode::BitFieldMaskU64: text += "BitFieldMaskU64"; break;
case Opcode::BitFieldExtractU32: text += "BitFieldExtractU32"; break;
case Opcode::BitFieldExtractU64: text += "BitFieldExtractU64"; break;
case Opcode::BitFieldExtract3U32: text += "BitFieldExtract3U32"; break;
case Opcode::BitFieldExtract3I32: text += "BitFieldExtract3I32"; break;
case Opcode::BitFieldInsertSelectU32: text += "BitFieldInsertSelectU32"; break;
case Opcode::BitCompare0B32: text += "BitCompare0B32"; break;
case Opcode::BitCompare1B32: text += "BitCompare1B32"; break;
case Opcode::AlignBitU32: text += "AlignBitU32"; break;
case Opcode::ShiftLeftAddU32: text += "ShiftLeftAddU32"; break;
case Opcode::AddShiftLeftU32: text += "AddShiftLeftU32"; break;
case Opcode::XorAddU32: text += "XorAddU32"; break;
case Opcode::ShiftLeftOrU32: text += "ShiftLeftOrU32"; break;
case Opcode::ShiftLeftLogicalU32: text += "ShiftLeftLogicalU32"; break;
case Opcode::ShiftLeftLogicalU64: text += "ShiftLeftLogicalU64"; break;
case Opcode::ShiftLeftLogicalU16: text += "ShiftLeftLogicalU16"; break;
case Opcode::ShiftRightLogicalU32: text += "ShiftRightLogicalU32"; break;
case Opcode::ShiftRightLogicalU64: text += "ShiftRightLogicalU64"; break;
case Opcode::ShiftRightLogicalU16: text += "ShiftRightLogicalU16"; break;
case Opcode::ShiftRightArithmeticI32: text += "ShiftRightArithmeticI32"; break;
case Opcode::ShiftRightArithmeticI16: text += "ShiftRightArithmeticI16"; break;
case Opcode::SelectU32: text += "SelectU32"; break;
case Opcode::SelectMaskU32: text += "SelectMaskU32"; break;
case Opcode::SelectF32Bits: text += "SelectF32Bits"; break;
case Opcode::SelectMaskF32Bits: text += "SelectMaskF32Bits"; break;
case Opcode::SelectU64: text += "SelectU64"; break;
case Opcode::PackLowLowU16: text += "PackLowLowU16"; break;
case Opcode::PackLowHighU16: text += "PackLowHighU16"; break;
case Opcode::PackHighHighU16: text += "PackHighHighU16"; break;
case Opcode::CompareFalse: text += "CompareFalse"; break;
case Opcode::CompareTrue: text += "CompareTrue"; break;
case Opcode::CompareEqU32: text += "CompareEqU32"; break;
case Opcode::CompareNeU32: text += "CompareNeU32"; break;
case Opcode::CompareGtU32: text += "CompareGtU32"; break;
case Opcode::CompareGeU32: text += "CompareGeU32"; break;
case Opcode::CompareLtU32: text += "CompareLtU32"; break;
case Opcode::CompareLeU32: text += "CompareLeU32"; break;
case Opcode::CompareNeU64: text += "CompareNeU64"; break;
case Opcode::CompareMaskEqU32: text += "CompareMaskEqU32"; break;
case Opcode::CompareMaskNeU32: text += "CompareMaskNeU32"; break;
case Opcode::CompareMaskGtU32: text += "CompareMaskGtU32"; break;
case Opcode::CompareMaskGeU32: text += "CompareMaskGeU32"; break;
case Opcode::CompareMaskLtU32: text += "CompareMaskLtU32"; break;
case Opcode::CompareMaskLeU32: text += "CompareMaskLeU32"; break;
case Opcode::CompareEqI32: text += "CompareEqI32"; break;
case Opcode::CompareNeI32: text += "CompareNeI32"; break;
case Opcode::CompareGtI32: text += "CompareGtI32"; break;
case Opcode::CompareGeI32: text += "CompareGeI32"; break;
case Opcode::CompareLtI32: text += "CompareLtI32"; break;
case Opcode::CompareLeI32: text += "CompareLeI32"; break;
case Opcode::CompareEqI16: text += "CompareEqI16"; break;
case Opcode::CompareNeI16: text += "CompareNeI16"; break;
case Opcode::CompareGtI16: text += "CompareGtI16"; break;
case Opcode::CompareGeI16: text += "CompareGeI16"; break;
case Opcode::CompareLtI16: text += "CompareLtI16"; break;
case Opcode::CompareLeI16: text += "CompareLeI16"; break;
case Opcode::CompareMaskEqI32: text += "CompareMaskEqI32"; break;
case Opcode::CompareMaskNeI32: text += "CompareMaskNeI32"; break;
case Opcode::CompareMaskGtI32: text += "CompareMaskGtI32"; break;
case Opcode::CompareMaskGeI32: text += "CompareMaskGeI32"; break;
case Opcode::CompareMaskLtI32: text += "CompareMaskLtI32"; break;
case Opcode::CompareMaskLeI32: text += "CompareMaskLeI32"; break;
case Opcode::CompareEqU16: text += "CompareEqU16"; break;
case Opcode::CompareNeU16: text += "CompareNeU16"; break;
case Opcode::CompareGtU16: text += "CompareGtU16"; break;
case Opcode::CompareGeU16: text += "CompareGeU16"; break;
case Opcode::CompareLtU16: text += "CompareLtU16"; break;
case Opcode::CompareLeU16: text += "CompareLeU16"; break;
case Opcode::CompareEqF32: text += "CompareEqF32"; break;
case Opcode::CompareNeF32: text += "CompareNeF32"; break;
case Opcode::CompareGtF32: text += "CompareGtF32"; break;
case Opcode::CompareGeF32: text += "CompareGeF32"; break;
case Opcode::CompareLtF32: text += "CompareLtF32"; break;
case Opcode::CompareLeF32: text += "CompareLeF32"; break;
case Opcode::CompareOrderedF32: text += "CompareOrderedF32"; break;
case Opcode::CompareUnorderedF32: text += "CompareUnorderedF32"; break;
case Opcode::CompareUnordEqF32: text += "CompareUnordEqF32"; break;
case Opcode::CompareUnordNeF32: text += "CompareUnordNeF32"; break;
case Opcode::CompareUnordGtF32: text += "CompareUnordGtF32"; break;
case Opcode::CompareUnordGeF32: text += "CompareUnordGeF32"; break;
case Opcode::CompareUnordLtF32: text += "CompareUnordLtF32"; break;
case Opcode::CompareUnordLeF32: text += "CompareUnordLeF32"; break;
case Opcode::CompareClassF32: text += "CompareClassF32"; break;
case Opcode::CompareEqF16: text += "CompareEqF16"; break;
case Opcode::CompareNeF16: text += "CompareNeF16"; break;
case Opcode::CompareGtF16: text += "CompareGtF16"; break;
case Opcode::CompareGeF16: text += "CompareGeF16"; break;
case Opcode::CompareLtF16: text += "CompareLtF16"; break;
case Opcode::CompareLeF16: text += "CompareLeF16"; break;
case Opcode::CompareUnordNeF16: text += "CompareUnordNeF16"; break;
case Opcode::CompareMaskEqF16: text += "CompareMaskEqF16"; break;
case Opcode::CompareMaskNeF16: text += "CompareMaskNeF16"; break;
case Opcode::CompareMaskGtF16: text += "CompareMaskGtF16"; break;
case Opcode::CompareMaskGeF16: text += "CompareMaskGeF16"; break;
case Opcode::CompareMaskLtF16: text += "CompareMaskLtF16"; break;
case Opcode::CompareMaskLeF16: text += "CompareMaskLeF16"; break;
case Opcode::CompareMaskUnordNeF16: text += "CompareMaskUnordNeF16"; break;
case Opcode::CompareMaskUnordGeF16: text += "CompareMaskUnordGeF16"; break;
case Opcode::CompareMaskEqF32: text += "CompareMaskEqF32"; break;
case Opcode::CompareMaskNeF32: text += "CompareMaskNeF32"; break;
case Opcode::CompareMaskGtF32: text += "CompareMaskGtF32"; break;
case Opcode::CompareMaskGeF32: text += "CompareMaskGeF32"; break;
case Opcode::CompareMaskLtF32: text += "CompareMaskLtF32"; break;
case Opcode::CompareMaskLeF32: text += "CompareMaskLeF32"; break;
case Opcode::CompareMaskUnordEqF32: text += "CompareMaskUnordEqF32"; break;
case Opcode::CompareMaskUnordNeF32: text += "CompareMaskUnordNeF32"; break;
case Opcode::CompareMaskUnordGtF32: text += "CompareMaskUnordGtF32"; break;
case Opcode::CompareMaskUnordGeF32: text += "CompareMaskUnordGeF32"; break;
case Opcode::CompareMaskUnordLtF32: text += "CompareMaskUnordLtF32"; break;
case Opcode::CompareMaskUnordLeF32: text += "CompareMaskUnordLeF32"; break;
case Opcode::ConvertByteU32ToF32: text += "ConvertByteU32ToF32"; break;
case Opcode::ConvertU32ToF32: text += "ConvertU32ToF32"; break;
case Opcode::ConvertI32ToF32: text += "ConvertI32ToF32"; break;
case Opcode::ConvertF32ToU32: text += "ConvertF32ToU32"; break;
case Opcode::ConvertF32ToI32: text += "ConvertF32ToI32"; break;
case Opcode::ConvertF32ToF16: text += "ConvertF32ToF16"; break;
case Opcode::ConvertF16ToF32: text += "ConvertF16ToF32"; break;
case Opcode::ConvertU16ToF16: text += "ConvertU16ToF16"; break;
case Opcode::ConvertF16ToU16: text += "ConvertF16ToU16"; break;
case Opcode::ConvertI16ToF16: text += "ConvertI16ToF16"; break;
case Opcode::ConvertF16ToI16: text += "ConvertF16ToI16"; break;
case Opcode::ConvertRoundPlusInfF32ToI32: text += "ConvertRoundPlusInfF32ToI32"; break;
case Opcode::ConvertFloorF32ToI32: text += "ConvertFloorF32ToI32"; break;
case Opcode::ConvertI4ToOffsetF32: text += "ConvertI4ToOffsetF32"; break;
case Opcode::LdexpF32: text += "LdexpF32"; break;
case Opcode::PackF32ToF16Rtz: text += "PackF32ToF16Rtz"; break;
case Opcode::PackSnorm2x16F32: text += "PackSnorm2x16F32"; break;
case Opcode::PackUnorm2x16F32: text += "PackUnorm2x16F32"; break;
case Opcode::PackU16U32: text += "PackU16U32"; break;
case Opcode::PackU8F32: text += "PackU8F32"; break;
case Opcode::PackB32F16: text += "PackB32F16"; break;
case Opcode::PackedMadI16: text += "PackedMadI16"; break;
case Opcode::PackedMulLoU16: text += "PackedMulLoU16"; break;
case Opcode::PackedAddI16: text += "PackedAddI16"; break;
case Opcode::PackedSubI16: text += "PackedSubI16"; break;
case Opcode::PackedLshlrevB16: text += "PackedLshlrevB16"; break;
case Opcode::PackedLshrrevB16: text += "PackedLshrrevB16"; break;
case Opcode::PackedAshrrevI16: text += "PackedAshrrevI16"; break;
case Opcode::PackedMaxI16: text += "PackedMaxI16"; break;
case Opcode::PackedMinI16: text += "PackedMinI16"; break;
case Opcode::PackedMadU16: text += "PackedMadU16"; break;
case Opcode::PackedAddU16: text += "PackedAddU16"; break;
case Opcode::PackedSubU16: text += "PackedSubU16"; break;
case Opcode::PackedMaxU16: text += "PackedMaxU16"; break;
case Opcode::PackedMinU16: text += "PackedMinU16"; break;
case Opcode::PackedAddF16: text += "PackedAddF16"; break;
case Opcode::PackedMulF16: text += "PackedMulF16"; break;
case Opcode::PackedMinF16: text += "PackedMinF16"; break;
case Opcode::PackedMaxF16: text += "PackedMaxF16"; break;
case Opcode::PackedFmaF16: text += "PackedFmaF16"; break;
case Opcode::AddF16: text += "AddF16"; break;
case Opcode::SubF16: text += "SubF16"; break;
case Opcode::MulF16: text += "MulF16"; break;
case Opcode::MinF16: text += "MinF16"; break;
case Opcode::MaxF16: text += "MaxF16"; break;
case Opcode::FmaF16: text += "FmaF16"; break;
case Opcode::MadMixF16: text += "MadMixF16"; break;
case Opcode::IAddU16: text += "IAddU16"; break;
case Opcode::ISubI16: text += "ISubI16"; break;
case Opcode::IMinI16: text += "IMinI16"; break;
case Opcode::IMaxI16: text += "IMaxI16"; break;
case Opcode::UMinU16: text += "UMinU16"; break;
case Opcode::UMaxU16: text += "UMaxU16"; break;
case Opcode::RcpF32: text += "RcpF32"; break;
case Opcode::FractF32: text += "FractF32"; break;
case Opcode::TruncF32: text += "TruncF32"; break;
case Opcode::CeilF32: text += "CeilF32"; break;
case Opcode::RoundEvenF32: text += "RoundEvenF32"; break;
case Opcode::FloorF32: text += "FloorF32"; break;
case Opcode::Exp2F32: text += "Exp2F32"; break;
case Opcode::Log2F32: text += "Log2F32"; break;
case Opcode::InverseSqrtF32: text += "InverseSqrtF32"; break;
case Opcode::SqrtF32: text += "SqrtF32"; break;
case Opcode::RcpF16: text += "RcpF16"; break;
case Opcode::SqrtF16: text += "SqrtF16"; break;
case Opcode::InverseSqrtF16: text += "InverseSqrtF16"; break;
case Opcode::Log2F16: text += "Log2F16"; break;
case Opcode::Exp2F16: text += "Exp2F16"; break;
case Opcode::FloorF16: text += "FloorF16"; break;
case Opcode::CeilF16: text += "CeilF16"; break;
case Opcode::TruncF16: text += "TruncF16"; break;
case Opcode::RoundEvenF16: text += "RoundEvenF16"; break;
case Opcode::SinF32: text += "SinF32"; break;
case Opcode::CosF32: text += "CosF32"; break;
case Opcode::CubeIdF32: text += "CubeIdF32"; break;
case Opcode::CubeScF32: text += "CubeScF32"; break;
case Opcode::CubeTcF32: text += "CubeTcF32"; break;
case Opcode::CubeMaF32: text += "CubeMaF32"; break;
case Opcode::FAddF32: text += "FAddF32"; break;
case Opcode::FSubF32: text += "FSubF32"; break;
case Opcode::FMulF32: text += "FMulF32"; break;
case Opcode::FMinF32: text += "FMinF32"; break;
case Opcode::FMaxF32: text += "FMaxF32"; break;
case Opcode::FMadF32: text += "FMadF32"; break;
case Opcode::Dot2AccF32F16: text += "Dot2AccF32F16"; break;
case Opcode::FMin3F32: text += "FMin3F32"; break;
case Opcode::FMax3F32: text += "FMax3F32"; break;
case Opcode::FMed3F32: text += "FMed3F32"; break;
case Opcode::Min3F16: text += "Min3F16"; break;
case Opcode::Max3F16: text += "Max3F16"; break;
case Opcode::Med3F16: text += "Med3F16"; break;
case Opcode::LoadSrtDword: text += "LoadSrtDword"; break;
case Opcode::SLoadDword: text += "SLoadDword"; break;
case Opcode::SBufferLoadDword: text += "SBufferLoadDword"; break;
case Opcode::BufferLoadUbyte: text += "BufferLoadUbyte"; break;
case Opcode::BufferLoadSbyte: text += "BufferLoadSbyte"; break;
case Opcode::BufferLoadUshort: text += "BufferLoadUshort"; break;
case Opcode::BufferLoadSshort: text += "BufferLoadSshort"; break;
case Opcode::BufferLoadDword: text += "BufferLoadDword"; break;
case Opcode::BufferStoreByte: text += "BufferStoreByte"; break;
case Opcode::BufferStoreShort: text += "BufferStoreShort"; break;
case Opcode::BufferStoreDword: text += "BufferStoreDword"; break;
case Opcode::AtomicSwapU32: text += "AtomicSwapU32"; break;
case Opcode::AtomicAddU32: text += "AtomicAddU32"; break;
case Opcode::AtomicSubU32: text += "AtomicSubU32"; break;
case Opcode::AtomicSMinI32: text += "AtomicSMinI32"; break;
case Opcode::AtomicUMinU32: text += "AtomicUMinU32"; break;
case Opcode::AtomicSMaxI32: text += "AtomicSMaxI32"; break;
case Opcode::AtomicUMaxU32: text += "AtomicUMaxU32"; break;
case Opcode::AtomicAndU32: text += "AtomicAndU32"; break;
case Opcode::AtomicOrU32: text += "AtomicOrU32"; break;
case Opcode::AtomicXorU32: text += "AtomicXorU32"; break;
case Opcode::FlatLoadUbyte: text += "FlatLoadUbyte"; break;
case Opcode::FlatLoadSbyte: text += "FlatLoadSbyte"; break;
case Opcode::FlatLoadUshort: text += "FlatLoadUshort"; break;
case Opcode::FlatLoadSshort: text += "FlatLoadSshort"; break;
case Opcode::FlatLoadDword: text += "FlatLoadDword"; break;
case Opcode::FlatStoreByte: text += "FlatStoreByte"; break;
case Opcode::FlatStoreShort: text += "FlatStoreShort"; break;
case Opcode::FlatStoreDword: text += "FlatStoreDword"; break;
case Opcode::DsReadUbyte: text += "DsReadUbyte"; break;
case Opcode::DsReadSbyte: text += "DsReadSbyte"; break;
case Opcode::DsReadUshort: text += "DsReadUshort"; break;
case Opcode::DsReadSshort: text += "DsReadSshort"; break;
case Opcode::DsReadB32: text += "DsReadB32"; break;
case Opcode::DsWriteByte: text += "DsWriteByte"; break;
case Opcode::DsWriteShort: text += "DsWriteShort"; break;
case Opcode::DsWriteB32: text += "DsWriteB32"; break;
case Opcode::DsMinF32: text += "DsMinF32"; break;
case Opcode::DsMaxF32: text += "DsMaxF32"; break;
case Opcode::DsSwizzleB32: text += "DsSwizzleB32"; break;
case Opcode::DsConsume: text += "DsConsume"; break;
case Opcode::DsAppend: text += "DsAppend"; break;
case Opcode::DsWriteAddtidB32: text += "DsWriteAddtidB32"; break;
case Opcode::DsReadAddtidB32: text += "DsReadAddtidB32"; break;
case Opcode::ImageGetResinfo: text += "ImageGetResinfo"; break;
case Opcode::ImageGetLod: text += "ImageGetLod"; break;
case Opcode::ImageLoad: text += "ImageLoad"; break;
case Opcode::ImageStore: text += "ImageStore"; break;
case Opcode::ImageSample: text += "ImageSample"; break;
case Opcode::ImageGather4: text += "ImageGather4"; break;
case Opcode::LoadInputF32: text += "LoadInputF32"; break;
case Opcode::Export: text += "Export"; break;
}
text += OpcodeName(inst.op);
text += " ";
text += OperandToString(inst.dst);
if (inst.dst2.kind != OperandKind::Null) {
@@ -569,6 +569,10 @@ bool LowerMemoryInstruction(const Decoder::Instruction& decoded, BasicBlock& blo
return LowerBufferAtomicDword(decoded, block, Opcode::AtomicOrU32, error);
case Decoder::Opcode::BufferAtomicXor:
return LowerBufferAtomicDword(decoded, block, Opcode::AtomicXorU32, error);
case Decoder::Opcode::BufferAtomicFMin:
return LowerBufferAtomicDword(decoded, block, Opcode::AtomicFMinF32, error);
case Decoder::Opcode::BufferAtomicFMax:
return LowerBufferAtomicDword(decoded, block, Opcode::AtomicFMaxF32, error);
case Decoder::Opcode::FlatLoadUbyte:
case Decoder::Opcode::FlatLoadSbyte:
case Decoder::Opcode::FlatLoadUshort:
@@ -673,7 +677,12 @@ bool LowerMemoryInstruction(const Decoder::Instruction& decoded, BasicBlock& blo
case Decoder::Opcode::ImageAtomicAnd:
case Decoder::Opcode::ImageAtomicOr:
case Decoder::Opcode::ImageAtomicXor: return LowerImageAtomicU32(decoded, block, error);
default: return false;
default:
if (error != nullptr) {
*error = fmt::format("memory opcode has no specialized IR lowering: {}",
Decoder::OpcodeToString(decoded.opcode));
}
return false;
}
}
@@ -729,6 +738,8 @@ bool IsMemoryOpcode(Decoder::Opcode opcode) {
case Decoder::Opcode::BufferAtomicAnd:
case Decoder::Opcode::BufferAtomicOr:
case Decoder::Opcode::BufferAtomicXor:
case Decoder::Opcode::BufferAtomicFMin:
case Decoder::Opcode::BufferAtomicFMax:
case Decoder::Opcode::FlatLoadUbyte:
case Decoder::Opcode::FlatLoadSbyte:
case Decoder::Opcode::FlatLoadUshort:
@@ -82,6 +82,7 @@ constexpr LowerMap LOWER_OPS[] = {
{Decoder::Opcode::SCmpGeU32, Opcode::CompareGeU32},
{Decoder::Opcode::SCmpLtU32, Opcode::CompareLtU32},
{Decoder::Opcode::SCmpLeU32, Opcode::CompareLeU32},
{Decoder::Opcode::SCmpEqU64, Opcode::CompareEqU64},
{Decoder::Opcode::SCmpLgU64, Opcode::CompareNeU64},
{Decoder::Opcode::VReadfirstlaneB32, Opcode::ReadFirstLaneU32},
{Decoder::Opcode::VReadlaneB32, Opcode::ReadLaneU32},
@@ -325,6 +326,7 @@ constexpr LowerMap LOWER_OPS[] = {
{Decoder::Opcode::VCmpNeU32, Opcode::CompareNeU32},
{Decoder::Opcode::VCmpGeU32, Opcode::CompareGeU32},
{Decoder::Opcode::VCmpTU32, Opcode::CompareTrue},
{Decoder::Opcode::VCmpEqI64, Opcode::CompareEqU64},
{Decoder::Opcode::VCmpNeU64, Opcode::CompareNeU64},
{Decoder::Opcode::VCmpxLtU32, Opcode::CompareMaskLtU32},
{Decoder::Opcode::VCmpxEqU32, Opcode::CompareMaskEqU32},
@@ -350,6 +352,8 @@ constexpr LowerMap LOWER_OPS[] = {
{Decoder::Opcode::BufferAtomicAnd, Opcode::AtomicAndU32},
{Decoder::Opcode::BufferAtomicOr, Opcode::AtomicOrU32},
{Decoder::Opcode::BufferAtomicXor, Opcode::AtomicXorU32},
{Decoder::Opcode::BufferAtomicFMin, Opcode::AtomicFMinF32},
{Decoder::Opcode::BufferAtomicFMax, Opcode::AtomicFMaxF32},
{Decoder::Opcode::FlatLoadUbyte, Opcode::FlatLoadUbyte},
{Decoder::Opcode::FlatLoadSbyte, Opcode::FlatLoadSbyte},
{Decoder::Opcode::FlatLoadSshort, Opcode::FlatLoadSshort},
@@ -432,22 +436,13 @@ void SetError(std::string* error, const char* message) {
}
}
Opcode LookupIrOpcode(Decoder::Opcode opcode) {
std::optional<Opcode> LookupIrOpcode(Decoder::Opcode opcode) {
for (const auto& op: LOWER_OPS) {
if (op.decoded == opcode) {
return op.ir;
}
}
return Opcode::MoveU32;
}
bool IsImplemented(Decoder::Opcode opcode) {
for (const auto& op: LOWER_OPS) {
if (op.decoded == opcode) {
return true;
}
}
return false;
return std::nullopt;
}
bool IsReversedBinary(Decoder::Opcode opcode) {
@@ -0,0 +1,342 @@
// Central IR opcode catalog. Keep semantic categories here so enum identity, logging,
// and generic analysis cannot drift apart.
IR_OPCODE(ControlNop, General)
IR_OPCODE(Waitcnt, General)
IR_OPCODE(Barrier, General)
IR_OPCODE(Sendmsg, General)
IR_OPCODE(TtraceData, General)
IR_OPCODE(InstPrefetch, General)
IR_OPCODE(MoveU32, General)
IR_OPCODE(MoveF32Bits, General)
IR_OPCODE(MoveRelDestU32, General)
IR_OPCODE(MoveRelSourceU32, General)
IR_OPCODE(MoveU64, General)
IR_OPCODE(WqmB64, General)
IR_OPCODE(SaveexecB32, General)
IR_OPCODE(SaveexecB64, General)
IR_OPCODE(ReadFirstLaneU32, General)
IR_OPCODE(ReadLaneU32, General)
IR_OPCODE(WriteLaneU32, General)
IR_OPCODE(Permlane16B32, General)
IR_OPCODE(Permlanex16B32, General)
IR_OPCODE(AbsI32, General)
IR_OPCODE(IAddU32, General)
IR_OPCODE(IAddCarryU32, General)
IR_OPCODE(ISubBorrowU32, General)
IR_OPCODE(ScalarAddCarryU32, General)
IR_OPCODE(ScalarSubBorrowU32, General)
IR_OPCODE(ScalarSubBorrowCarryU32, General)
IR_OPCODE(ScalarSignedAddOverflowI32, General)
IR_OPCODE(ScalarSignedSubOverflowI32, General)
IR_OPCODE(ScalarShiftLeftAddCarryU32, General)
IR_OPCODE(ISubU32, General)
IR_OPCODE(IMulU32, General)
IR_OPCODE(UMulHighU32, General)
IR_OPCODE(SMulHighI32, General)
IR_OPCODE(IMadI24U32, General)
IR_OPCODE(UMadU24U32, General)
IR_OPCODE(UMadU64U32, General)
IR_OPCODE(SadU32, General)
IR_OPCODE(IAdd3U32, General)
IR_OPCODE(IMulI24U32, General)
IR_OPCODE(UMulU24U32, General)
IR_OPCODE(IMinI32, General)
IR_OPCODE(IMaxI32, General)
IR_OPCODE(IMin3I32, General)
IR_OPCODE(IMax3I32, General)
IR_OPCODE(IMed3I32, General)
IR_OPCODE(UMinU32, General)
IR_OPCODE(UMaxU32, General)
IR_OPCODE(UMin3U32, General)
IR_OPCODE(UMax3U32, General)
IR_OPCODE(UMed3U32, General)
IR_OPCODE(BitwiseAndU32, General)
IR_OPCODE(BitwiseAndU64, General)
IR_OPCODE(BitwiseAndNotU32, General)
IR_OPCODE(BitwiseAndNotU64, General)
IR_OPCODE(BitwiseOrU32, General)
IR_OPCODE(BitwiseOrU64, General)
IR_OPCODE(BitwiseOrNotU32, General)
IR_OPCODE(BitwiseOrNotU64, General)
IR_OPCODE(BitwiseAndOrU32, General)
IR_OPCODE(BitwiseOr3U32, General)
IR_OPCODE(BitwiseXorU32, General)
IR_OPCODE(BitwiseXorU64, General)
IR_OPCODE(BitwiseXor3U32, General)
IR_OPCODE(BitwiseNandU32, General)
IR_OPCODE(BitwiseNandU64, General)
IR_OPCODE(BitwiseNorU32, General)
IR_OPCODE(BitwiseNorU64, General)
IR_OPCODE(BitwiseXnorU32, General)
IR_OPCODE(BitwiseXnorU64, General)
IR_OPCODE(BitwiseNotU32, General)
IR_OPCODE(BitwiseNotU64, General)
IR_OPCODE(BitClearU32, General)
IR_OPCODE(BitSetU32, General)
IR_OPCODE(BitReverseU32, General)
IR_OPCODE(BitCountU32, General)
IR_OPCODE(BitCountU64, General)
IR_OPCODE(BitReplicateB64B32, General)
IR_OPCODE(BitCountAddU32, General)
IR_OPCODE(MaskedBitCountLowU32, General)
IR_OPCODE(MaskedBitCountHighU32, General)
IR_OPCODE(FindLsbU32, General)
IR_OPCODE(FindMsbFromHighU32, General)
IR_OPCODE(FindMsbFromHighU64, General)
IR_OPCODE(BitFieldMaskU32, General)
IR_OPCODE(BitFieldMaskU64, General)
IR_OPCODE(BitFieldExtractU32, General)
IR_OPCODE(BitFieldExtractU64, General)
IR_OPCODE(BitFieldExtract3U32, General)
IR_OPCODE(BitFieldExtract3I32, General)
IR_OPCODE(BitFieldInsertSelectU32, General)
IR_OPCODE(BitCompare0B32, General)
IR_OPCODE(BitCompare1B32, General)
IR_OPCODE(AlignBitU32, General)
IR_OPCODE(ShiftLeftAddU32, General)
IR_OPCODE(AddShiftLeftU32, General)
IR_OPCODE(XorAddU32, General)
IR_OPCODE(ShiftLeftOrU32, General)
IR_OPCODE(ShiftLeftLogicalU32, General)
IR_OPCODE(ShiftLeftLogicalU64, General)
IR_OPCODE(ShiftLeftLogicalU16, General)
IR_OPCODE(ShiftRightLogicalU32, General)
IR_OPCODE(ShiftRightLogicalU64, General)
IR_OPCODE(ShiftRightLogicalU16, General)
IR_OPCODE(ShiftRightArithmeticI32, General)
IR_OPCODE(ShiftRightArithmeticI16, General)
IR_OPCODE(SelectU32, General)
IR_OPCODE(SelectMaskU32, General)
IR_OPCODE(SelectF32Bits, General)
IR_OPCODE(SelectMaskF32Bits, General)
IR_OPCODE(SelectU64, General)
IR_OPCODE(PackLowLowU16, General)
IR_OPCODE(PackLowHighU16, General)
IR_OPCODE(PackHighHighU16, General)
IR_OPCODE(CompareFalse, Compare)
IR_OPCODE(CompareTrue, Compare)
IR_OPCODE(CompareEqU32, Compare)
IR_OPCODE(CompareNeU32, Compare)
IR_OPCODE(CompareGtU32, Compare)
IR_OPCODE(CompareGeU32, Compare)
IR_OPCODE(CompareLtU32, Compare)
IR_OPCODE(CompareLeU32, Compare)
IR_OPCODE(CompareEqU64, Compare64)
IR_OPCODE(CompareNeU64, Compare64)
IR_OPCODE(CompareMaskEqU32, Compare)
IR_OPCODE(CompareMaskNeU32, Compare)
IR_OPCODE(CompareMaskGtU32, Compare)
IR_OPCODE(CompareMaskGeU32, Compare)
IR_OPCODE(CompareMaskLtU32, Compare)
IR_OPCODE(CompareMaskLeU32, Compare)
IR_OPCODE(CompareEqI32, Compare)
IR_OPCODE(CompareNeI32, Compare)
IR_OPCODE(CompareGtI32, Compare)
IR_OPCODE(CompareGeI32, Compare)
IR_OPCODE(CompareLtI32, Compare)
IR_OPCODE(CompareLeI32, Compare)
IR_OPCODE(CompareEqI16, Compare)
IR_OPCODE(CompareNeI16, Compare)
IR_OPCODE(CompareGtI16, Compare)
IR_OPCODE(CompareGeI16, Compare)
IR_OPCODE(CompareLtI16, Compare)
IR_OPCODE(CompareLeI16, Compare)
IR_OPCODE(CompareMaskEqI32, Compare)
IR_OPCODE(CompareMaskNeI32, Compare)
IR_OPCODE(CompareMaskGtI32, Compare)
IR_OPCODE(CompareMaskGeI32, Compare)
IR_OPCODE(CompareMaskLtI32, Compare)
IR_OPCODE(CompareMaskLeI32, Compare)
IR_OPCODE(CompareEqU16, Compare)
IR_OPCODE(CompareNeU16, Compare)
IR_OPCODE(CompareGtU16, Compare)
IR_OPCODE(CompareGeU16, Compare)
IR_OPCODE(CompareLtU16, Compare)
IR_OPCODE(CompareLeU16, Compare)
IR_OPCODE(CompareEqF32, Compare)
IR_OPCODE(CompareNeF32, Compare)
IR_OPCODE(CompareGtF32, Compare)
IR_OPCODE(CompareGeF32, Compare)
IR_OPCODE(CompareLtF32, Compare)
IR_OPCODE(CompareLeF32, Compare)
IR_OPCODE(CompareOrderedF32, Compare)
IR_OPCODE(CompareUnorderedF32, Compare)
IR_OPCODE(CompareUnordEqF32, Compare)
IR_OPCODE(CompareUnordNeF32, Compare)
IR_OPCODE(CompareUnordGtF32, Compare)
IR_OPCODE(CompareUnordGeF32, Compare)
IR_OPCODE(CompareUnordLtF32, Compare)
IR_OPCODE(CompareUnordLeF32, Compare)
IR_OPCODE(CompareClassF32, Compare)
IR_OPCODE(CompareEqF16, Compare)
IR_OPCODE(CompareNeF16, Compare)
IR_OPCODE(CompareGtF16, Compare)
IR_OPCODE(CompareGeF16, Compare)
IR_OPCODE(CompareLtF16, Compare)
IR_OPCODE(CompareLeF16, Compare)
IR_OPCODE(CompareUnordNeF16, Compare)
IR_OPCODE(CompareMaskEqF16, Compare)
IR_OPCODE(CompareMaskNeF16, Compare)
IR_OPCODE(CompareMaskGtF16, Compare)
IR_OPCODE(CompareMaskGeF16, Compare)
IR_OPCODE(CompareMaskLtF16, Compare)
IR_OPCODE(CompareMaskLeF16, Compare)
IR_OPCODE(CompareMaskUnordNeF16, Compare)
IR_OPCODE(CompareMaskUnordGeF16, Compare)
IR_OPCODE(CompareMaskEqF32, Compare)
IR_OPCODE(CompareMaskNeF32, Compare)
IR_OPCODE(CompareMaskGtF32, Compare)
IR_OPCODE(CompareMaskGeF32, Compare)
IR_OPCODE(CompareMaskLtF32, Compare)
IR_OPCODE(CompareMaskLeF32, Compare)
IR_OPCODE(CompareMaskUnordEqF32, Compare)
IR_OPCODE(CompareMaskUnordNeF32, Compare)
IR_OPCODE(CompareMaskUnordGtF32, Compare)
IR_OPCODE(CompareMaskUnordGeF32, Compare)
IR_OPCODE(CompareMaskUnordLtF32, Compare)
IR_OPCODE(CompareMaskUnordLeF32, Compare)
IR_OPCODE(ConvertByteU32ToF32, General)
IR_OPCODE(ConvertU32ToF32, General)
IR_OPCODE(ConvertI32ToF32, General)
IR_OPCODE(ConvertF32ToU32, General)
IR_OPCODE(ConvertF32ToI32, General)
IR_OPCODE(ConvertF32ToF16, General)
IR_OPCODE(ConvertF16ToF32, General)
IR_OPCODE(ConvertU16ToF16, General)
IR_OPCODE(ConvertF16ToU16, General)
IR_OPCODE(ConvertI16ToF16, General)
IR_OPCODE(ConvertF16ToI16, General)
IR_OPCODE(ConvertRoundPlusInfF32ToI32, General)
IR_OPCODE(ConvertFloorF32ToI32, General)
IR_OPCODE(ConvertI4ToOffsetF32, General)
IR_OPCODE(LdexpF32, General)
IR_OPCODE(PackF32ToF16Rtz, General)
IR_OPCODE(PackSnorm2x16F32, General)
IR_OPCODE(PackUnorm2x16F32, General)
IR_OPCODE(PackU16U32, General)
IR_OPCODE(PackU8F32, General)
IR_OPCODE(PackB32F16, General)
IR_OPCODE(PackedMadI16, General)
IR_OPCODE(PackedMulLoU16, General)
IR_OPCODE(PackedAddI16, General)
IR_OPCODE(PackedSubI16, General)
IR_OPCODE(PackedLshlrevB16, General)
IR_OPCODE(PackedLshrrevB16, General)
IR_OPCODE(PackedAshrrevI16, General)
IR_OPCODE(PackedMaxI16, General)
IR_OPCODE(PackedMinI16, General)
IR_OPCODE(PackedMadU16, General)
IR_OPCODE(PackedAddU16, General)
IR_OPCODE(PackedSubU16, General)
IR_OPCODE(PackedMaxU16, General)
IR_OPCODE(PackedMinU16, General)
IR_OPCODE(PackedAddF16, General)
IR_OPCODE(PackedMulF16, General)
IR_OPCODE(PackedMinF16, General)
IR_OPCODE(PackedMaxF16, General)
IR_OPCODE(PackedFmaF16, General)
IR_OPCODE(AddF16, General)
IR_OPCODE(SubF16, General)
IR_OPCODE(MulF16, General)
IR_OPCODE(MinF16, General)
IR_OPCODE(MaxF16, General)
IR_OPCODE(FmaF16, General)
IR_OPCODE(MadMixF16, General)
IR_OPCODE(IAddU16, General)
IR_OPCODE(ISubI16, General)
IR_OPCODE(IMinI16, General)
IR_OPCODE(IMaxI16, General)
IR_OPCODE(UMinU16, General)
IR_OPCODE(UMaxU16, General)
IR_OPCODE(RcpF32, General)
IR_OPCODE(FractF32, General)
IR_OPCODE(TruncF32, General)
IR_OPCODE(CeilF32, General)
IR_OPCODE(RoundEvenF32, General)
IR_OPCODE(FloorF32, General)
IR_OPCODE(Exp2F32, General)
IR_OPCODE(Log2F32, General)
IR_OPCODE(InverseSqrtF32, General)
IR_OPCODE(SqrtF32, General)
IR_OPCODE(RcpF16, General)
IR_OPCODE(SqrtF16, General)
IR_OPCODE(InverseSqrtF16, General)
IR_OPCODE(Log2F16, General)
IR_OPCODE(Exp2F16, General)
IR_OPCODE(FloorF16, General)
IR_OPCODE(CeilF16, General)
IR_OPCODE(TruncF16, General)
IR_OPCODE(RoundEvenF16, General)
IR_OPCODE(SinF32, General)
IR_OPCODE(CosF32, General)
IR_OPCODE(CubeIdF32, General)
IR_OPCODE(CubeScF32, General)
IR_OPCODE(CubeTcF32, General)
IR_OPCODE(CubeMaF32, General)
IR_OPCODE(FAddF32, General)
IR_OPCODE(FSubF32, General)
IR_OPCODE(FMulF32, General)
IR_OPCODE(FMinF32, General)
IR_OPCODE(FMaxF32, General)
IR_OPCODE(FMadF32, General)
IR_OPCODE(Dot2AccF32F16, General)
IR_OPCODE(FMin3F32, General)
IR_OPCODE(FMax3F32, General)
IR_OPCODE(FMed3F32, General)
IR_OPCODE(Min3F16, General)
IR_OPCODE(Max3F16, General)
IR_OPCODE(Med3F16, General)
IR_OPCODE(LoadSrtDword, General)
IR_OPCODE(SLoadDword, General)
IR_OPCODE(SBufferLoadDword, General)
IR_OPCODE(BufferLoadUbyte, General)
IR_OPCODE(BufferLoadSbyte, General)
IR_OPCODE(BufferLoadUshort, General)
IR_OPCODE(BufferLoadSshort, General)
IR_OPCODE(BufferLoadDword, General)
IR_OPCODE(BufferStoreByte, General)
IR_OPCODE(BufferStoreShort, General)
IR_OPCODE(BufferStoreDword, General)
IR_OPCODE(AtomicSwapU32, General)
IR_OPCODE(AtomicAddU32, General)
IR_OPCODE(AtomicSubU32, General)
IR_OPCODE(AtomicSMinI32, General)
IR_OPCODE(AtomicUMinU32, General)
IR_OPCODE(AtomicSMaxI32, General)
IR_OPCODE(AtomicUMaxU32, General)
IR_OPCODE(AtomicAndU32, General)
IR_OPCODE(AtomicOrU32, General)
IR_OPCODE(AtomicXorU32, General)
IR_OPCODE(AtomicFMinF32, General)
IR_OPCODE(AtomicFMaxF32, General)
IR_OPCODE(FlatLoadUbyte, General)
IR_OPCODE(FlatLoadSbyte, General)
IR_OPCODE(FlatLoadUshort, General)
IR_OPCODE(FlatLoadSshort, General)
IR_OPCODE(FlatLoadDword, General)
IR_OPCODE(FlatStoreByte, General)
IR_OPCODE(FlatStoreShort, General)
IR_OPCODE(FlatStoreDword, General)
IR_OPCODE(DsReadUbyte, General)
IR_OPCODE(DsReadSbyte, General)
IR_OPCODE(DsReadUshort, General)
IR_OPCODE(DsReadSshort, General)
IR_OPCODE(DsReadB32, General)
IR_OPCODE(DsWriteByte, General)
IR_OPCODE(DsWriteShort, General)
IR_OPCODE(DsWriteB32, General)
IR_OPCODE(DsMinF32, General)
IR_OPCODE(DsMaxF32, General)
IR_OPCODE(DsSwizzleB32, General)
IR_OPCODE(DsConsume, General)
IR_OPCODE(DsAppend, General)
IR_OPCODE(DsWriteAddtidB32, General)
IR_OPCODE(DsReadAddtidB32, General)
IR_OPCODE(ImageGetResinfo, General)
IR_OPCODE(ImageGetLod, General)
IR_OPCODE(ImageLoad, General)
IR_OPCODE(ImageStore, General)
IR_OPCODE(ImageSample, General)
IR_OPCODE(ImageGather4, General)
IR_OPCODE(LoadInputF32, General)
IR_OPCODE(Export, General)
+9 -3
View File
@@ -276,6 +276,10 @@ static std::filesystem::path ResolvePathIgnoringCase(const std::filesystem::path
return resolved;
}
#else
static bool HasWindowsForbiddenFilenameCharacter(const std::string& relative_path) {
return relative_path.find_first_of("<>:\"|?*") != std::string::npos;
}
#endif
std::filesystem::path MountPoints::GetRealFilename(const std::string& mounted_file_name) {
@@ -295,6 +299,10 @@ std::filesystem::path MountPoints::GetRealFilename(const std::string& mounted_fi
rel_path = Common::RemoveFirst(rel_path, 1);
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (HasWindowsForbiddenFilenameCharacter(rel_path)) {
::printf("FileSystem: Windows-incompatible guest filename: %s\n",
mounted_file_name.c_str());
}
return p.dir / rel_path;
#else
return ResolvePathIgnoringCase(p.dir / rel_path);
@@ -795,7 +803,7 @@ int64_t KYTY_SYSV_ABI KernelLseek(int d, int64_t offset, int whence) {
return KERNEL_ERROR_ESPIPE;
}
file->mutex.Lock();
Common::LockGuard lock(file->mutex);
bool is_invalid = file->f.IsInvalid();
@@ -820,8 +828,6 @@ int64_t KYTY_SYSV_ABI KernelLseek(int d, int64_t offset, int whence) {
EXIT_IF(pos != offset);
file->mutex.Unlock();
if (is_invalid) {
LOGF("\tfile is invalid\n");
return KERNEL_ERROR_EIO;
+1 -1
View File
@@ -65,7 +65,7 @@ constexpr uint64_t PAGE_TABLE_POOL_SIZE = 4ull * 1024ull * 1024ull * 1024ull;
constexpr uint64_t PAGE_TABLE_GRANULARITY = 2ull * 1024ull * 1024ull;
constexpr int PAGE_TABLE_POOL_ENTRIES =
static_cast<int>(PAGE_TABLE_POOL_SIZE / PAGE_TABLE_GRANULARITY);
constexpr uint64_t DEFAULT_FLEXIBLE_MEMORY_SIZE = 4ull * 1024ull * 1024ull * 1024ull;
constexpr uint64_t DEFAULT_FLEXIBLE_MEMORY_SIZE = 1ull * 1024ull * 1024ull * 1024ull;
static uint64_t g_flexible_memory_size = DEFAULT_FLEXIBLE_MEMORY_SIZE;
static bool g_flexible_memory_size_frozen = false;
+287
View File
@@ -0,0 +1,287 @@
#include "kernel/syncOnAddress.h"
#include "common/threads.h"
#include "libs/errno.h"
#include <algorithm>
#include <chrono>
#include <climits>
#include <cstdint>
#include <list>
#include <memory>
#include <mutex>
#include <unordered_map>
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
#include <cerrno>
#include <linux/futex.h>
#include <sys/syscall.h>
#include <unistd.h>
#endif
namespace Libs::LibKernel::SyncOnAddress {
namespace {
constexpr uint32_t SIGNAL_POLL_MICROS = 10000;
using Clock = std::chrono::steady_clock;
template <typename T>
[[nodiscard]] bool IsValidWaitAddress(const volatile T* address) {
return address != nullptr && (reinterpret_cast<uintptr_t>(address) & (alignof(T) - 1u)) == 0;
}
[[nodiscard]] bool IsValidWakeAddress(const volatile void* address) {
return address != nullptr &&
(reinterpret_cast<uintptr_t>(address) & (alignof(uint32_t) - 1u)) == 0;
}
template <typename T>
[[nodiscard]] T ReadWord(const volatile T* address) {
return __atomic_load_n(address, __ATOMIC_ACQUIRE);
}
struct WaitDeadline {
bool finite = false;
Clock::time_point end {};
};
[[nodiscard]] WaitDeadline MakeDeadline(const uint32_t* timeout_micros) {
if (timeout_micros == nullptr) {
return {};
}
return {true, Clock::now() + std::chrono::microseconds(*timeout_micros)};
}
[[nodiscard]] uint32_t GetWaitSliceMicros(const WaitDeadline& deadline, bool first_wait) {
if (!deadline.finite) {
return SIGNAL_POLL_MICROS;
}
const auto now = Clock::now();
if (now >= deadline.end) {
return first_wait ? 0u : UINT32_MAX;
}
const auto remaining =
std::chrono::duration_cast<std::chrono::microseconds>(deadline.end - now).count();
return static_cast<uint32_t>(std::min<int64_t>(remaining, SIGNAL_POLL_MICROS));
}
void PollSignals(signal_poll_func_t signal_poll) {
if (signal_poll != nullptr) {
signal_poll();
}
}
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
template <typename T>
int WaitLinux(volatile T* address, T expected, const uint32_t* timeout_micros,
signal_poll_func_t signal_poll) {
const auto deadline = MakeDeadline(timeout_micros);
bool first_wait = true;
for (;;) {
if (ReadWord(address) != expected) {
return OK;
}
const auto slice_micros = GetWaitSliceMicros(deadline, first_wait);
if (slice_micros == UINT32_MAX) {
return ReadWord(address) == expected ? KERNEL_ERROR_ETIMEDOUT : OK;
}
const timespec timeout = {
.tv_sec = static_cast<time_t>(slice_micros / 1000000u),
.tv_nsec = static_cast<long>(slice_micros % 1000000u) * 1000L,
};
long result = 0;
int wait_error = 0;
result = syscall(SYS_futex, const_cast<T*>(address), FUTEX_WAIT_PRIVATE,
static_cast<uint32_t>(expected), &timeout, nullptr, 0);
if (result != 0) {
wait_error = errno;
}
if (result == 0 || wait_error == EAGAIN) {
return OK;
}
if (wait_error != ETIMEDOUT && wait_error != EINTR) {
return KERNEL_ERROR_EINVAL;
}
PollSignals(signal_poll);
if (deadline.finite && Clock::now() >= deadline.end) {
return ReadWord(address) == expected ? KERNEL_ERROR_ETIMEDOUT : OK;
}
first_wait = false;
}
}
int WakeLinux(volatile void* address, int32_t count) {
const auto result = syscall(SYS_futex, const_cast<void*>(address), FUTEX_WAKE_PRIVATE, count,
nullptr, nullptr, 0);
return result < 0 ? KERNEL_ERROR_EINVAL : OK;
}
#endif
struct PortableWaiter {
Common::CondVar condition;
bool wake_requested = false;
};
struct PortableAddressEntry {
Common::Mutex mutex;
std::list<PortableWaiter*> waiters;
};
struct PortableAddressRegistry {
std::mutex mutex;
std::unordered_map<uintptr_t, std::shared_ptr<PortableAddressEntry>> entries;
};
PortableAddressRegistry& GetPortableRegistry() {
static PortableAddressRegistry registry;
return registry;
}
std::shared_ptr<PortableAddressEntry> RegisterPortableWaiter(volatile void* address,
PortableWaiter* waiter) {
auto& registry = GetPortableRegistry();
std::lock_guard registry_lock(registry.mutex);
auto& entry = registry.entries[reinterpret_cast<uintptr_t>(address)];
if (!entry) {
entry = std::make_shared<PortableAddressEntry>();
}
entry->mutex.Lock();
entry->waiters.push_back(waiter);
return entry;
}
void UnregisterPortableWaiter(volatile void* address,
const std::shared_ptr<PortableAddressEntry>& entry,
PortableWaiter* waiter) {
entry->waiters.remove(waiter);
const bool empty = entry->waiters.empty();
entry->mutex.Unlock();
if (!empty) {
return;
}
auto& registry = GetPortableRegistry();
std::lock_guard registry_lock(registry.mutex);
entry->mutex.Lock();
const auto it = registry.entries.find(reinterpret_cast<uintptr_t>(address));
if (it != registry.entries.end() && it->second == entry && entry->waiters.empty()) {
registry.entries.erase(it);
}
entry->mutex.Unlock();
}
template <typename T>
int WaitPortable(volatile T* address, T expected, const uint32_t* timeout_micros,
signal_poll_func_t signal_poll) {
PortableWaiter waiter;
auto entry = RegisterPortableWaiter(address, &waiter);
const auto deadline = MakeDeadline(timeout_micros);
bool first_wait = true;
int result = OK;
while (ReadWord(address) == expected && !waiter.wake_requested) {
const auto slice_micros = GetWaitSliceMicros(deadline, first_wait);
if (slice_micros == UINT32_MAX) {
result = KERNEL_ERROR_ETIMEDOUT;
break;
}
if (slice_micros == 0) {
result = KERNEL_ERROR_ETIMEDOUT;
break;
}
(void)waiter.condition.WaitFor(&entry->mutex, slice_micros);
entry->mutex.Unlock();
PollSignals(signal_poll);
entry->mutex.Lock();
if (deadline.finite && Clock::now() >= deadline.end && ReadWord(address) == expected &&
!waiter.wake_requested) {
result = KERNEL_ERROR_ETIMEDOUT;
break;
}
first_wait = false;
}
UnregisterPortableWaiter(address, entry, &waiter);
return result;
}
int WakePortable(volatile void* address, int32_t count) {
if (count == 0) {
return OK;
}
auto& registry = GetPortableRegistry();
std::unique_lock registry_lock(registry.mutex);
const auto it = registry.entries.find(reinterpret_cast<uintptr_t>(address));
if (it == registry.entries.end()) {
return OK;
}
auto entry = it->second;
entry->mutex.Lock();
registry_lock.unlock();
int32_t remaining = count;
for (auto* waiter: entry->waiters) {
if (!waiter->wake_requested) {
waiter->wake_requested = true;
waiter->condition.Signal();
if (remaining != INT_MAX && --remaining == 0) {
break;
}
}
}
entry->mutex.Unlock();
return OK;
}
template <typename T>
int WaitImpl(volatile T* address, T expected, const uint32_t* timeout_micros,
signal_poll_func_t signal_poll) {
if (!IsValidWaitAddress(address)) {
return KERNEL_ERROR_EINVAL;
}
int result = OK;
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
result = WaitLinux(address, expected, timeout_micros, signal_poll);
#else
result = WaitPortable(address, expected, timeout_micros, signal_poll);
#endif
PollSignals(signal_poll);
return result;
}
} // namespace
int Wait32(volatile uint32_t* address, uint32_t expected, const uint32_t* timeout_micros,
signal_poll_func_t signal_poll) {
return WaitImpl(address, expected, timeout_micros, signal_poll);
}
int Wait64(volatile uint64_t* address, uint64_t expected, const uint32_t* timeout_micros,
signal_poll_func_t signal_poll) {
return WaitImpl(address, expected, timeout_micros, signal_poll);
}
int Wake(volatile void* address, int32_t count) {
if (!IsValidWakeAddress(address) || count < 0) {
return KERNEL_ERROR_EINVAL;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
return WakeLinux(address, count);
#endif
return WakePortable(address, count);
}
} // namespace Libs::LibKernel::SyncOnAddress
+19
View File
@@ -0,0 +1,19 @@
#ifndef EMULATOR_INCLUDE_EMULATOR_KERNEL_SYNC_ON_ADDRESS_H_
#define EMULATOR_INCLUDE_EMULATOR_KERNEL_SYNC_ON_ADDRESS_H_
#include "common/abi.h"
#include "common/common.h"
namespace Libs::LibKernel::SyncOnAddress {
using signal_poll_func_t = void (*)();
int Wait32(volatile uint32_t* address, uint32_t expected, const uint32_t* timeout_micros,
signal_poll_func_t signal_poll = nullptr);
int Wait64(volatile uint64_t* address, uint64_t expected, const uint32_t* timeout_micros,
signal_poll_func_t signal_poll = nullptr);
int Wake(volatile void* address, int32_t count);
} // namespace Libs::LibKernel::SyncOnAddress
#endif /* EMULATOR_INCLUDE_EMULATOR_KERNEL_SYNC_ON_ADDRESS_H_ */
+2 -1
View File
@@ -1 +1,2 @@
#define KYTY_GIT_VERSION "@KYTY_GIT_VERSION@"
#define KYTY_GIT_VERSION "@KYTY_GIT_VERSION@"
#define KYTY_GIT_HASH "@KYTY_GIT_HASH@"
+1 -1
View File
@@ -16,6 +16,7 @@ file(GLOB launcher_forms
add_executable(launcher WIN32
${launcher_src}
${launcher_forms}
resources.qrc
)
@@ -37,7 +38,6 @@ elseif(LINUX)
set_target_properties(launcher PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS}")
endif()
add_dependencies(launcher KytyGitVersion)
add_dependencies(launcher kyty_emulator)
if(KYTY_CLANG_CL)
@@ -78,6 +78,16 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="checkBox_fullscreen">
<property name="toolTip">
<string>Run in borderless desktop fullscreen</string>
</property>
<property name="text">
<string>Fullscreen</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -69,6 +69,31 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="input_mapping_button">
<property name="minimumSize">
<size>
<width>36</width>
<height>36</height>
</size>
</property>
<property name="toolTip">
<string>Edit input mapping</string>
</property>
<property name="text">
<string/>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="edit_button">
<property name="minimumSize">
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#e8f1ff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M8 4h11l5 5v7M24 9h-5V4M16 27H8a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"/>
<path d="m15 24 1-4 8-8 4 4-8 8-4 1Z"/>
<path d="m22 14 4 4" opacity=".8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 336 B

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#e8f1ff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M5 8h3M14 8h13M5 16h13M24 16h3M5 24h6M17 24h10"/>
<circle cx="11" cy="8" r="3"/>
<circle cx="21" cy="16" r="3"/>
<circle cx="14" cy="24" r="3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 344 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#e8f1ff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M10 10h12c3.6 0 5.8 2.3 6.6 6.8l.9 5.1c.4 2.4-.8 4.1-2.8 4.1-1.4 0-2.5-.7-3.6-2.3L21.3 21H10.7l-1.8 2.7C7.8 25.3 6.7 26 5.3 26c-2 0-3.2-1.7-2.8-4.1l.9-5.1C4.2 12.3 6.4 10 10 10Z"/>
<path d="M8 16h6M11 13v6"/>
<circle cx="22" cy="15" r="1" fill="#e8f1ff" stroke="none"/>
<circle cx="25" cy="18" r="1" fill="#e8f1ff" stroke="none"/>
<path d="M13 7h6M16 4v6" opacity=".8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 574 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#e8f1ff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M6 9h20M12 9V5h8v4M9 9l1 18h12l1-18"/>
<path d="M14 14v8M18 14v8" opacity=".8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 272 B

+10 -2
View File
@@ -85,9 +85,10 @@ public:
QString game_comment;
Resolution screen_resolution = Resolution::R1280X720;
bool fullscreen_enabled = false;
int vblank_frequency = 60;
int console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = true;
bool vulkan_validation_enabled = false;
bool shader_validation_enabled = true;
ShaderOptimizationType shader_optimization_type = ShaderOptimizationType::Performance;
ShaderLogDirection shader_log_direction = ShaderLogDirection::Silent;
@@ -98,11 +99,13 @@ public:
QString printf_output_file = "_kyty.txt";
ProfilerDirection profiler_direction = ProfilerDirection::None;
bool renderdoc_enabled = false;
QStringList host_input_mapping;
QString elf = QStringLiteral("eboot.bin");
void CopyEmulatorSettingsFrom(const Configuration& other) {
screen_resolution = other.screen_resolution;
fullscreen_enabled = other.fullscreen_enabled;
vblank_frequency = other.vblank_frequency;
console_language = other.console_language;
vulkan_validation_enabled = other.vulkan_validation_enabled;
@@ -116,6 +119,7 @@ public:
printf_output_file = other.printf_output_file;
profiler_direction = other.profiler_direction;
renderdoc_enabled = other.renderdoc_enabled;
host_input_mapping = other.host_input_mapping;
}
void CopyFrom(const Configuration& other) {
@@ -138,6 +142,7 @@ public:
KYTY_CFG_SET(game_path);
KYTY_CFG_SET(custom_settings);
KYTY_CFG_SET(screen_resolution);
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
KYTY_CFG_SET(vulkan_validation_enabled);
@@ -151,6 +156,7 @@ public:
KYTY_CFG_SET(printf_output_file);
KYTY_CFG_SET(profiler_direction);
KYTY_CFG_SET(renderdoc_enabled);
s->setValue("host_input_mapping", host_input_mapping);
KYTY_CFG_SET(elf);
}
@@ -160,6 +166,7 @@ public:
KYTY_CFG_GET(game_path);
KYTY_CFG_GET(custom_settings);
KYTY_CFG_GET(screen_resolution);
KYTY_CFG_GET(fullscreen_enabled);
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
console_language = s->value("console_language", console_language).toInt();
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
@@ -176,7 +183,8 @@ public:
KYTY_CFG_GET(printf_output_file);
KYTY_CFG_GET(profiler_direction);
KYTY_CFG_GET(renderdoc_enabled);
elf = s->value("elf", elf).toString();
host_input_mapping = s->value("host_input_mapping", host_input_mapping).toStringList();
elf = s->value("elf", elf).toString();
}
};
@@ -38,7 +38,10 @@ public:
[[nodiscard]] const QString& GetSettingsFile() const { return m_settings_file; }
[[nodiscard]] const QStringList& GetGameDirectories() const { return m_game_dirs; }
[[nodiscard]] bool CanViewSelectedTrophies() const;
[[nodiscard]] const QStringList& GetHostInputMapping() const {
return m_global_info.host_input_mapping;
}
[[nodiscard]] bool CanViewSelectedTrophies() const;
bool EnsureGameDirectory();
void ScanGameDirectory();
@@ -58,6 +61,7 @@ protected slots:
void edit_configuration();
void delete_configuartion();
void edit_global_settings();
void edit_input_mapping();
void run_configuration();
void list_currentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous);
void list_itemDoubleClicked(QTreeWidgetItem* witem, int column);
+31
View File
@@ -0,0 +1,31 @@
#ifndef LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
#define LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
#include <QDialog>
#include <QStringList>
class QPushButton;
class QTreeWidget;
class QTreeWidgetItem;
class QWidget;
class InputMappingDialog final: public QDialog {
public:
explicit InputMappingDialog(const QStringList& mapping, QWidget* parent = nullptr);
[[nodiscard]] QStringList Mapping() const;
private:
void ChangeBinding();
void ClearBinding();
void RestoreDefaults();
void SetBinding(QTreeWidgetItem* item, const QString& binding);
void UpdateButtons();
QTreeWidget* m_bindings = nullptr;
QPushButton* m_change_button = nullptr;
QPushButton* m_clear_button = nullptr;
bool m_customized = false;
};
#endif /* LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_ */
+8
View File
@@ -0,0 +1,8 @@
<RCC>
<qresource prefix="/icons">
<file alias="edit-configuration.svg">icons/edit-configuration.svg</file>
<file alias="global-settings.svg">icons/global-settings.svg</file>
<file alias="input-mapping.svg">icons/input-mapping.svg</file>
<file alias="remove-configuration.svg">icons/remove-configuration.svg</file>
</qresource>
</RCC>
@@ -153,6 +153,7 @@ static void ListInit(QComboBox* combo, T value) {
void ConfigurationEditDialog::Init(const Configuration& info) {
ListInit(m_ui->comboBox_screen_resolution, info.screen_resolution);
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
m_ui->comboBox_console_language->clear();
m_ui->comboBox_console_language->addItems(CONSOLE_LANGUAGE_NAMES);
@@ -279,6 +280,7 @@ void ConfigurationEditDialog::moveEvent(QMoveEvent* event) {
static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) {
info.screen_resolution =
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
info.console_language = ui.comboBox_console_language->currentIndex();
info.vulkan_validation_enabled = ui.checkBox_vulkan_validation->isChecked();
+17 -3
View File
@@ -6,6 +6,7 @@
#include "configurationEditDialog.h"
#include "configurationItem.h"
#include "gameListTreeWidget.h"
#include "inputMappingDialog.h"
#include "mainDialog.h"
#include "patchesDialog.h"
#include "trophyViewerDialog.h"
@@ -169,6 +170,7 @@ static void ApplyGameListStyle(Ui::ConfigurationListWidget* ui) {
"QToolButton:disabled { background: transparent; }"));
ui->edit_button->setStyleSheet(ui->global_settings_button->styleSheet());
ui->delete_button->setStyleSheet(ui->global_settings_button->styleSheet());
ui->input_mapping_button->setStyleSheet(ui->global_settings_button->styleSheet());
}
static void AddSaveDataDir(QStringList* dirs, QSet<QString>* seen, const QString& root,
@@ -226,10 +228,12 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
m_ui->setupUi(this);
ApplyGameListStyle(m_ui);
m_ui->global_settings_button->setIcon(style()->standardIcon(QStyle::SP_FileDialogDetailedView));
m_ui->global_settings_button->setIcon(QIcon(QStringLiteral(":/icons/global-settings.svg")));
m_ui->global_settings_button->setToolTip(tr("Edit global settings and game folders"));
m_ui->edit_button->setIcon(style()->standardIcon(QStyle::SP_FileIcon));
m_ui->delete_button->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton));
m_ui->input_mapping_button->setIcon(QIcon(QStringLiteral(":/icons/input-mapping.svg")));
m_ui->input_mapping_button->setToolTip(tr("Edit global input mapping"));
m_ui->edit_button->setIcon(QIcon(QStringLiteral(":/icons/edit-configuration.svg")));
m_ui->delete_button->setIcon(QIcon(QStringLiteral(":/icons/remove-configuration.svg")));
m_ui->delete_button->setEnabled(false);
m_ui->edit_button->setEnabled(false);
@@ -249,6 +253,8 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
connect(m_ui->global_settings_button, &QToolButton::clicked, this,
&ConfigurationListWidget::edit_global_settings);
connect(m_ui->input_mapping_button, &QToolButton::clicked, this,
&ConfigurationListWidget::edit_input_mapping);
connect(m_ui->edit_button, &QToolButton::clicked, this,
&ConfigurationListWidget::edit_configuration);
connect(m_ui->delete_button, &QToolButton::clicked, this,
@@ -717,6 +723,14 @@ void ConfigurationListWidget::edit_global_settings() {
}
}
void ConfigurationListWidget::edit_input_mapping() {
InputMappingDialog dialog(m_global_info.host_input_mapping, this);
if (dialog.exec() == QDialog::Accepted) {
m_global_info.host_input_mapping = dialog.Mapping();
WriteSettings();
}
}
void ConfigurationListWidget::ClearCustomSettings(ConfigurationItem* item) {
delete m_custom_infos.take(item->GetInfo().game_path);
}
+311
View File
@@ -0,0 +1,311 @@
#include "inputMappingDialog.h"
#include <QAbstractItemView>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QHash>
#include <QHeaderView>
#include <QKeyEvent>
#include <QKeySequence>
#include <QLabel>
#include <QMouseEvent>
#include <QPushButton>
#include <QStringList>
#include <QTreeWidget>
#include <QVBoxLayout>
namespace {
constexpr int BINDING_COLUMN = 1;
struct PadButton {
const char* id;
const char* label;
const char* default_binding;
};
constexpr PadButton PAD_BUTTONS[] = {
{"Up", "D-pad Up", "Up"},
{"Down", "D-pad Down", "Down"},
{"Left", "D-pad Left", "Left"},
{"Right", "D-pad Right", "Right"},
{"Triangle", "Triangle", "I"},
{"Circle", "Circle", "L"},
{"Cross", "Cross", "J"},
{"Square", "Square", "K"},
{"L1", "L1", "Q"},
{"R1", "R1", "E"},
{"L2", "L2", ""},
{"R2", "R2", ""},
{"L3", "L3", "Left Shift"},
{"R3", "R3", "Left Ctrl"},
{"Options", "Options", "Return"},
{"TouchPad", "Touch pad", "Backspace"},
};
QString KeypadName(int key) {
if (key >= Qt::Key_0 && key <= Qt::Key_9) {
return QStringLiteral("Keypad %1").arg(key - Qt::Key_0);
}
switch (key) {
case Qt::Key_Return:
case Qt::Key_Enter: return QStringLiteral("Keypad Enter");
case Qt::Key_Slash: return QStringLiteral("Keypad /");
case Qt::Key_Asterisk: return QStringLiteral("Keypad *");
case Qt::Key_Minus: return QStringLiteral("Keypad -");
case Qt::Key_Plus: return QStringLiteral("Keypad +");
case Qt::Key_Period: return QStringLiteral("Keypad .");
case Qt::Key_Equal: return QStringLiteral("Keypad =");
case Qt::Key_Comma: return QStringLiteral("Keypad ,");
default: return {};
}
}
QString KeyName(const QKeyEvent& event) {
const int key = event.key();
if (event.modifiers().testFlag(Qt::KeypadModifier)) {
return KeypadName(key);
}
if (key >= Qt::Key_A && key <= Qt::Key_Z) {
return QChar(key);
}
if (key >= Qt::Key_0 && key <= Qt::Key_9) {
return QChar(key);
}
if (key >= Qt::Key_F1 && key <= Qt::Key_F24) {
return QStringLiteral("F%1").arg(key - Qt::Key_F1 + 1);
}
switch (key) {
case Qt::Key_Return:
case Qt::Key_Enter: return QStringLiteral("Return");
case Qt::Key_Backspace: return QStringLiteral("Backspace");
case Qt::Key_Tab: return QStringLiteral("Tab");
case Qt::Key_Shift: return QStringLiteral("Left Shift");
case Qt::Key_Control: return QStringLiteral("Left Ctrl");
case Qt::Key_Alt: return QStringLiteral("Left Alt");
case Qt::Key_Meta: return QStringLiteral("Left GUI");
case Qt::Key_Insert: return QStringLiteral("Insert");
case Qt::Key_Delete: return QStringLiteral("Delete");
case Qt::Key_Home: return QStringLiteral("Home");
case Qt::Key_End: return QStringLiteral("End");
case Qt::Key_PageUp: return QStringLiteral("PageUp");
case Qt::Key_PageDown: return QStringLiteral("PageDown");
case Qt::Key_Left: return QStringLiteral("Left");
case Qt::Key_Right: return QStringLiteral("Right");
case Qt::Key_Up: return QStringLiteral("Up");
case Qt::Key_Down: return QStringLiteral("Down");
case Qt::Key_CapsLock: return QStringLiteral("CapsLock");
case Qt::Key_NumLock: return QStringLiteral("Numlock");
case Qt::Key_ScrollLock: return QStringLiteral("ScrollLock");
case Qt::Key_Pause: return QStringLiteral("Pause");
case Qt::Key_Print: return QStringLiteral("PrintScreen");
default: break;
}
if (event.modifiers() != Qt::NoModifier) {
return {};
}
const auto name = QKeySequence(key).toString(QKeySequence::PortableText);
return name.size() == 1 ? name : QString();
}
class InputCaptureDialog final: public QDialog {
public:
explicit InputCaptureDialog(QWidget* parent): QDialog(parent) {
setWindowTitle(tr("Set Binding"));
setModal(true);
setMinimumWidth(360);
auto* layout = new QVBoxLayout(this);
m_label = new QLabel(
tr("Press a key or mouse button.\nSpace and F1 are reserved; Esc cancels."), this);
m_label->setAlignment(Qt::AlignCenter);
layout->addWidget(m_label);
}
[[nodiscard]] const QString& Binding() const { return m_binding; }
protected:
void keyPressEvent(QKeyEvent* event) override {
if (event->isAutoRepeat()) {
return;
}
if (event->key() == Qt::Key_Escape) {
reject();
return;
}
if (event->key() == Qt::Key_Space || event->key() == Qt::Key_F1) {
m_label->setText(tr("That key is reserved by the emulator."));
return;
}
m_binding = KeyName(*event);
if (!m_binding.isEmpty()) {
accept();
} else {
m_label->setText(tr("That key is not supported."));
}
}
void mousePressEvent(QMouseEvent* event) override {
switch (event->button()) {
case Qt::LeftButton: m_binding = QStringLiteral("Mouse:Left"); break;
case Qt::RightButton: m_binding = QStringLiteral("Mouse:Right"); break;
case Qt::MiddleButton: m_binding = QStringLiteral("Mouse:Middle"); break;
case Qt::BackButton: m_binding = QStringLiteral("Mouse:X1"); break;
case Qt::ForwardButton: m_binding = QStringLiteral("Mouse:X2"); break;
default: return;
}
accept();
}
private:
QLabel* m_label = nullptr;
QString m_binding;
};
QHash<QString, QString> ParseMapping(const QStringList& mapping) {
QHash<QString, QString> result;
for (const auto& entry: mapping) {
const auto separator = entry.indexOf(QLatin1Char('='));
if (separator > 0 && separator + 1 < entry.size()) {
const auto binding = entry.mid(separator + 1);
for (auto item = result.begin(); item != result.end();) {
if (item.value().compare(binding, Qt::CaseInsensitive) == 0) {
item = result.erase(item);
} else {
++item;
}
}
result.insert(entry.left(separator), binding);
}
}
return result;
}
} // namespace
InputMappingDialog::InputMappingDialog(const QStringList& mapping, QWidget* parent)
: QDialog(parent), m_customized(!mapping.isEmpty()) {
setWindowTitle(tr("Input Mapping"));
resize(440, 520);
auto* layout = new QVBoxLayout(this);
layout->addWidget(new QLabel(tr("Map keyboard or mouse input to DualSense buttons.\n"
"Defaults: left stick W/A/S/D, right stick T/F/G/H."),
this));
m_bindings = new QTreeWidget(this);
m_bindings->setColumnCount(2);
m_bindings->setHeaderLabels({tr("DualSense button"), tr("Host input")});
m_bindings->setRootIsDecorated(false);
m_bindings->setSelectionMode(QAbstractItemView::SingleSelection);
m_bindings->header()->setSectionResizeMode(0, QHeaderView::Stretch);
m_bindings->header()->setSectionResizeMode(1, QHeaderView::Stretch);
layout->addWidget(m_bindings);
const auto parsed = ParseMapping(mapping);
for (const auto& button: PAD_BUTTONS) {
auto* item = new QTreeWidgetItem(m_bindings);
item->setText(0, tr(button.label));
item->setData(0, Qt::UserRole, QString::fromLatin1(button.id));
SetBinding(item, m_customized ? parsed.value(QString::fromLatin1(button.id))
: QString::fromLatin1(button.default_binding));
}
m_bindings->setCurrentItem(m_bindings->topLevelItem(0));
auto* controls = new QHBoxLayout;
m_change_button = new QPushButton(tr("Change..."), this);
m_clear_button = new QPushButton(tr("Clear"), this);
auto* defaults = new QPushButton(tr("Defaults"), this);
controls->addWidget(m_change_button);
controls->addWidget(m_clear_button);
controls->addWidget(defaults);
controls->addStretch();
layout->addLayout(controls);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
layout->addWidget(buttons);
connect(m_bindings, &QTreeWidget::itemDoubleClicked, this,
[this](QTreeWidgetItem*, int) { ChangeBinding(); });
connect(m_bindings, &QTreeWidget::itemSelectionChanged, this, [this]() { UpdateButtons(); });
connect(m_change_button, &QPushButton::clicked, this, [this]() { ChangeBinding(); });
connect(m_clear_button, &QPushButton::clicked, this, [this]() { ClearBinding(); });
connect(defaults, &QPushButton::clicked, this, [this]() { RestoreDefaults(); });
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
UpdateButtons();
}
QStringList InputMappingDialog::Mapping() const {
QStringList result;
if (!m_customized) {
return result;
}
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
const auto* item = m_bindings->topLevelItem(index);
const auto binding = item->data(BINDING_COLUMN, Qt::UserRole).toString();
if (!binding.isEmpty()) {
result.append(item->data(0, Qt::UserRole).toString() + QLatin1Char('=') + binding);
}
}
return result;
}
void InputMappingDialog::ChangeBinding() {
auto* item = m_bindings->currentItem();
if (item == nullptr) {
return;
}
InputCaptureDialog dialog(this);
if (dialog.exec() != QDialog::Accepted) {
return;
}
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
auto* other = m_bindings->topLevelItem(index);
if (other != item && other->data(BINDING_COLUMN, Qt::UserRole)
.toString()
.compare(dialog.Binding(), Qt::CaseInsensitive) == 0) {
SetBinding(other, {});
}
}
SetBinding(item, dialog.Binding());
m_customized = true;
}
void InputMappingDialog::ClearBinding() {
SetBinding(m_bindings->currentItem(), {});
m_customized = true;
}
void InputMappingDialog::RestoreDefaults() {
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
SetBinding(m_bindings->topLevelItem(index),
QString::fromLatin1(PAD_BUTTONS[index].default_binding));
}
m_customized = false;
}
void InputMappingDialog::SetBinding(QTreeWidgetItem* item, const QString& binding) {
if (item == nullptr) {
return;
}
item->setData(BINDING_COLUMN, Qt::UserRole, binding);
item->setText(BINDING_COLUMN, binding.isEmpty() ? tr("None") : binding);
UpdateButtons();
}
void InputMappingDialog::UpdateButtons() {
if (m_change_button == nullptr) {
return;
}
const auto* item = m_bindings->currentItem();
m_change_button->setEnabled(item != nullptr);
m_clear_button->setEnabled(item != nullptr &&
!item->data(BINDING_COLUMN, Qt::UserRole).toString().isEmpty());
}
+10 -1
View File
@@ -204,6 +204,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--screen-width" << r.at(0);
args << "--screen-height" << r.at(1);
if (info.fullscreen_enabled) {
args << "--fullscreen";
}
args << "--vblank-frequency" << QString::number(info.vblank_frequency);
args << "--console-language" << QString::number(info.console_language);
args << "--vulkan-validation" << BoolArg(info.vulkan_validation_enabled);
@@ -217,6 +220,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--printf-output-file" << info.printf_output_file;
args << "--profiler-direction" << EnumToText(info.profiler_direction);
args << "--spirv-debug-printf" << "false";
for (const auto& binding: info.host_input_mapping) {
args << "--keymap" << binding;
}
if (info.renderdoc_enabled) {
args << "--rd";
}
@@ -435,7 +441,10 @@ void MainDialogPrivate::Run() {
m_running_item->SetRunning(true);
m_main_dialog->RunInterpreter(&m_process, m_running_item->GetInfo());
Configuration info;
info.CopyFrom(m_running_item->GetInfo());
info.host_input_mapping = m_ui->widget->GetHostInputMapping();
m_main_dialog->RunInterpreter(&m_process, info);
Update();
}
+57 -13
View File
@@ -6,6 +6,7 @@
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "common/virtualMemory.h"
#include "graphics/guest_gpu/command_processor/commandProcessor.h"
#include "graphics/guest_gpu/gpu_defs.h"
#include "graphics/guest_gpu/graphicsRun.h"
#include "graphics/guest_gpu/hardwareContext.h"
@@ -1444,29 +1445,72 @@ int KYTY_SYSV_ABI GraphicsSuspendPoint() {
return OK;
}
uint32_t* KYTY_SYSV_ABI GraphicsUnknownQj7QZpgr9Uw(CommandBuffer* buf, uint32_t mode,
uint32_t value) {
PRINT_NAME();
// ResetContextState?
LOGF("\t mode = 0x%08" PRIx32 "\n"
"\t value = 0x%08" PRIx32 "\n",
mode, value);
[[nodiscard]] static constexpr uint32_t context_state_op_size_dw(uint32_t operation) {
switch (static_cast<ContextStateOperation>(operation)) {
case ContextStateOperation::Clear: return 5;
case ContextStateOperation::Push:
case ContextStateOperation::Pop: return 27;
case ContextStateOperation::PushClear: return 32;
default: return 0;
}
}
if (buf == nullptr) {
uint32_t* KYTY_SYSV_ABI GraphicsDcbContextStateOp(CommandBuffer* buf, uint32_t operation) {
PRINT_NAME();
LOGF("\t operation = 0x%08" PRIx32 "\n", operation);
const auto size_dw = context_state_op_size_dw(operation);
if (buf == nullptr || size_dw == 0) {
return nullptr;
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(1);
uint32_t* first = nullptr;
const auto append = [&](uint32_t segment_size_dw) {
auto* segment = buf->AllocateDW(segment_size_dw);
if (segment == nullptr) {
return false;
}
std::fill_n(segment, segment_size_dw, 0u);
if (first == nullptr) {
first = segment;
segment[0] = KYTY_PM4(segment_size_dw, Pm4::IT_NOP, Pm4::R_CONTEXT_STATE);
segment[1] = operation;
} else {
segment[0] = KYTY_PM4(segment_size_dw, Pm4::IT_NOP, Pm4::R_ZERO);
}
return true;
};
const auto append_save_restore = [&] {
// The native helper reserves 22 dwords, then emits 5-, 8-, and 9-dword packets.
return buf->ReserveDW(22) && append(5) && append(8) && append(9);
};
if (cmd == nullptr) {
return nullptr;
// Preserve native packet sizes, allocation boundaries, Push/Pop ordering, and
// returned first-packet address. The native packets reference process-private state buffers,
// so their net Cx operation is represented by the first HLE packet and type-3 NOPs.
bool complete = false;
switch (static_cast<ContextStateOperation>(operation)) {
case ContextStateOperation::Clear: complete = append(5); break;
case ContextStateOperation::Push:
complete = append_save_restore() && append(3) && append(2);
break;
case ContextStateOperation::Pop:
complete = append(3) && append_save_restore() && append(2);
break;
case ContextStateOperation::PushClear:
complete = append_save_restore() && append(3) && append(2) && append(5);
break;
default: break;
}
cmd[0] = 0x80000000u;
return complete ? first : nullptr;
}
return cmd;
uint64_t KYTY_SYSV_ABI GraphicsDcbContextStateOpGetSize(uint32_t operation) {
PRINT_NAME();
return static_cast<uint64_t>(context_state_op_size_dw(operation)) * sizeof(uint32_t);
}
uint64_t KYTY_SYSV_ABI GraphicsGetIsTrinityMode() {
+2 -2
View File
@@ -67,8 +67,8 @@ int KYTY_SYSV_ABI GraphicsUnknownJumpPatchSetTarget(uint32_t* cmd, const volatil
int KYTY_SYSV_ABI GraphicsJumpPatchSetTarget(uint32_t* cmd, const volatile uint32_t* target,
uint32_t size_in_dwords);
int KYTY_SYSV_ABI GraphicsSuspendPoint();
uint32_t* KYTY_SYSV_ABI GraphicsUnknownQj7QZpgr9Uw(CommandBuffer* buf, uint32_t mode,
uint32_t value);
uint32_t* KYTY_SYSV_ABI GraphicsDcbContextStateOp(CommandBuffer* buf, uint32_t operation);
uint64_t KYTY_SYSV_ABI GraphicsDcbContextStateOpGetSize(uint32_t operation);
uint64_t KYTY_SYSV_ABI GraphicsGetIsTrinityMode();
uint32_t KYTY_SYSV_ABI GraphicsDriverGetDefaultOwner();
+19 -5
View File
@@ -82,6 +82,7 @@ public:
Id AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format);
bool AudioOutClose(Id handle);
bool AudioOutValid(Id handle);
bool AudioOutHasDevice(Id handle);
bool AudioOutSetVolume(Id handle, uint32_t bitflag, const int* volume);
uint32_t AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking = true);
bool AudioOutGetStatus(Id handle, int* type, int* channels_num);
@@ -152,6 +153,10 @@ void AudioOutClose(int handle) {
}
}
bool AudioOutHasDevice(int handle) {
return g_audio != nullptr && handle > 0 && g_audio->AudioOutHasDevice(Audio::Id(handle));
}
uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking) {
if (g_audio == nullptr || params == nullptr || num == 0) {
return 0;
@@ -451,6 +456,13 @@ bool Audio::AudioOutValid(Id handle) {
m_out_ports[handle.GetId()].used);
}
bool Audio::AudioOutHasDevice(Id handle) {
Common::LockGuard lock(m_mutex);
return (handle.GetId() >= 0 && handle.GetId() < OUT_PORTS_MAX &&
m_out_ports[handle.GetId()].used && m_out_ports[handle.GetId()].audio_device != 0);
}
bool Audio::AudioOutGetStatus(Id handle, int* type, int* channels_num) {
Common::LockGuard lock(m_mutex);
@@ -516,16 +528,18 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
max_wait_time = (wait_time > max_wait_time ? wait_time : max_wait_time);
}
bool all_ports_have_device = true;
bool any_port_has_device = false;
for (uint32_t i = 0; i < num; i++) {
if (m_out_ports[params[i].handle.GetId()].audio_device == 0) {
all_ports_have_device = false;
if (m_out_ports[params[i].handle.GetId()].audio_device != 0) {
any_port_has_device = true;
break;
}
}
// Device-backed ports are paced by the SDL queue above.
if (blocking && max_wait_time != 0 && !all_ports_have_device) {
// One real output device is enough to pace the whole synchronized batch. Applying the fallback
// when a vibration port is present would rate-limit the device-backed ports to exactly 1x and
// prevent their SDL queues from building an underrun cushion.
if (blocking && max_wait_time != 0 && !any_port_has_device) {
Common::Thread::SleepMicro(max_wait_time);
}
+1
View File
@@ -26,6 +26,7 @@ static constexpr int OUT_PORTS_MAX = 32;
int AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format);
void AudioOutClose(int handle);
bool AudioOutHasDevice(int handle);
uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking = true);
} // namespace Libs::Audio::AudioInternal
+17 -4
View File
@@ -56,6 +56,7 @@ public:
void Disconnect(int id);
void Button(int id, uint32_t button, bool down);
void Axis(int id, Axis axis, int value);
void ResetInputState();
void GetConnectionInfo(bool* flag, int* count);
void ReadState(ControllerState* state, bool* flag, int* count);
int ReadStates(ControllerState* states, int states_num, bool* flag, int* count);
@@ -85,8 +86,6 @@ private:
static GameController* g_controller = nullptr;
static constexpr int KEYBOARD_CONTROLLER_ID = -1000;
static uint8_t pad_connected_count_to_u8(int connected_count) {
return static_cast<uint8_t>(connected_count > 255 ? 255 : connected_count);
}
@@ -117,7 +116,7 @@ KYTY_SUBSYSTEM_INIT(Controller) {
EXIT_IF(g_controller != nullptr);
g_controller = new GameController;
g_controller->Connect(KEYBOARD_CONTROLLER_ID);
g_controller->Connect(HOST_INPUT_CONTROLLER_ID);
}
KYTY_SUBSYSTEM_UNEXPECTED_SHUTDOWN(Controller) {}
@@ -155,7 +154,7 @@ void GameController::CheckActive() {
if (!m_connected_ids.empty()) {
new_active_id = m_connected_ids[0];
for (const auto id: m_connected_ids) {
if (id != KEYBOARD_CONTROLLER_ID) {
if (id != HOST_INPUT_CONTROLLER_ID) {
new_active_id = id;
break;
}
@@ -258,6 +257,15 @@ void GameController::Axis(int id, Controller::Axis axis, int value) {
}
}
void GameController::ResetInputState() {
Common::LockGuard lock(m_mutex);
ControllerState state {};
state.time = LibKernel::KernelGetProcessTime();
m_states_num = 0;
m_first_state = 0;
AddState(state);
}
void GameController::GetConnectionInfo(bool* flag, int* count) {
EXIT_IF(flag == nullptr);
EXIT_IF(count == nullptr);
@@ -336,6 +344,11 @@ void ControllerAxis(int id, Axis axis, int value) {
g_controller->Axis(id, axis, value);
}
void ControllerResetInputState() {
EXIT_IF(g_controller == nullptr);
g_controller->ResetInputState();
}
int KYTY_SYSV_ABI PadInit() {
PRINT_NAME();
+3
View File
@@ -9,6 +9,8 @@ namespace Libs::Controller {
KYTY_SUBSYSTEM_DEFINE(Controller);
constexpr int HOST_INPUT_CONTROLLER_ID = -1000;
constexpr uint32_t PAD_BUTTON_L3 = 0x00000002;
constexpr uint32_t PAD_BUTTON_R3 = 0x00000004;
constexpr uint32_t PAD_BUTTON_OPTIONS = 0x00000008;
@@ -51,6 +53,7 @@ void ControllerConnect(int id);
void ControllerDisconnect(int id);
void ControllerButton(int id, uint32_t button, bool down);
void ControllerAxis(int id, Axis axis, int value);
void ControllerResetInputState();
int KYTY_SYSV_ABI PadInit();
int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param);
-14
View File
@@ -23,20 +23,6 @@ int KYTY_SYSV_ABI CommonDialogInitialize() {
} // namespace CommonDialog
namespace ImeDialog {
LIB_NAME("ImeDialog", "ImeDialog");
constexpr int IME_STATUS_NONE = 0;
int KYTY_SYSV_ABI ImeDialogGetStatus() {
PRINT_NAME();
return IME_STATUS_NONE;
}
} // namespace ImeDialog
namespace LoginDialog {
LIB_NAME("LoginDialog", "LoginDialog");
-6
View File
@@ -12,12 +12,6 @@ int KYTY_SYSV_ABI CommonDialogInitialize();
} // namespace CommonDialog
namespace ImeDialog {
int KYTY_SYSV_ABI ImeDialogGetStatus();
} // namespace ImeDialog
namespace LoginDialog {
int KYTY_SYSV_ABI LoginDialogInitialize();
+781
View File
@@ -0,0 +1,781 @@
#include "libs/imeDialog.h"
#include "libs/errno.h"
#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstring>
#include <mutex>
#include <vector>
namespace Libs::Dialog::ImeDialog {
namespace {
constexpr int Error(uint32_t value) {
return static_cast<int32_t>(value);
}
constexpr int ERROR_BUSY = Error(0x80bc0001);
constexpr int ERROR_INVALID_USER_ID = Error(0x80bc0010);
constexpr int ERROR_INVALID_TYPE = Error(0x80bc0011);
constexpr int ERROR_INVALID_LANGUAGES = Error(0x80bc0012);
constexpr int ERROR_INVALID_ENTER_LABEL = Error(0x80bc0013);
constexpr int ERROR_INVALID_INPUT_METHOD = Error(0x80bc0014);
constexpr int ERROR_INVALID_OPTION = Error(0x80bc0015);
constexpr int ERROR_INVALID_MAX_TEXT_LENGTH = Error(0x80bc0016);
constexpr int ERROR_INVALID_TEXT_BUFFER = Error(0x80bc0017);
constexpr int ERROR_INVALID_POSX = Error(0x80bc0018);
constexpr int ERROR_INVALID_POSY = Error(0x80bc0019);
constexpr int ERROR_INVALID_HALIGN = Error(0x80bc001a);
constexpr int ERROR_INVALID_VALIGN = Error(0x80bc001b);
constexpr int ERROR_INVALID_EXTENDED = Error(0x80bc001c);
constexpr int ERROR_INVALID_PARAM = Error(0x80bc0030);
constexpr int ERROR_INVALID_ADDRESS = Error(0x80bc0031);
constexpr int ERROR_INVALID_RESERVED = Error(0x80bc0032);
constexpr int ERROR_INVALID_TITLE = Error(0x80bc0101);
constexpr int ERROR_NOT_RUNNING = Error(0x80bc0105);
constexpr int ERROR_NOT_FINISHED = Error(0x80bc0106);
constexpr int ERROR_NOT_IN_USE = Error(0x80bc0107);
constexpr uint32_t VALID_OPTIONS = 0x00007bff;
constexpr uint32_t VALID_EXTENDED_OPTIONS = 0x00005fde;
constexpr uint32_t VALID_EXT_KEYBOARD_MODE = 0x1c000003;
constexpr uint64_t VALID_LANGUAGES = 0x00000001ff1fffffULL;
struct State {
Status status = Status::None;
EndStatus end_status = EndStatus::Ok;
uint64_t generation = 0;
uint64_t revision = 0;
Param param {};
ExtendedParam extended {};
bool input_changed = false;
bool commit_pending = false;
uint32_t cursor = 0;
std::u16string original_text;
std::u16string current_text;
std::u16string title;
std::u16string placeholder;
std::vector<ExternalInput> external_inputs;
};
std::mutex g_mutex;
State g_state;
std::atomic<Status> g_status {Status::None};
std::atomic<uint64_t> g_revision {0};
std::atomic<VisibilityCallback> g_visibility_callback {nullptr};
bool AllZero(const int8_t* data, size_t size) {
return std::all_of(data, data + size, [](int8_t value) { return value == 0; });
}
bool IsValidUtf16(std::u16string_view text);
bool ReadBounded(const char16_t* text, uint32_t limit, std::u16string* out) {
out->clear();
if (text == nullptr) {
return true;
}
out->reserve(limit);
for (uint32_t i = 0; i <= limit; ++i) {
const char16_t value = text[i];
if (value == u'\0') {
return IsValidUtf16(*out);
}
if (i == limit) {
return false;
}
out->push_back(value);
}
return false;
}
bool IsValidUtf16(std::u16string_view text) {
for (size_t i = 0; i < text.size(); i++) {
const char16_t current = text[i];
if (current >= 0xd800 && current <= 0xdbff) {
if (++i >= text.size() || text[i] < 0xdc00 || text[i] > 0xdfff) {
return false;
}
} else if (current >= 0xdc00 && current <= 0xdfff) {
return false;
}
}
return true;
}
bool IsAllowedInput(char16_t value, Type type, uint32_t option) {
if (value == u'\r') {
return false;
}
if (value == u'\n') {
return (option & OPTION_MULTILINE) != 0;
}
if (value == u'\0') {
return false;
}
if (type == Type::Number) {
return (value >= u'0' && value <= u'9') || value == u',' || value == u'-' || value == u'.';
}
if (type == Type::BasicLatin) {
return value >= u' ' && value <= u'~';
}
return true;
}
char16_t HidCharacter(uint16_t keycode, uint32_t status) {
const bool shift = (status & 0x00002200) != 0;
const bool caps = (status & 0x00020000) != 0;
if (keycode >= 4 && keycode <= 29) {
const bool upper = shift != caps;
return static_cast<char16_t>((upper ? u'A' : u'a') + keycode - 4);
}
if (keycode >= 30 && keycode <= 39) {
static constexpr char16_t plain[] = u"1234567890";
static constexpr char16_t shifted[] = u"!@#$%^&*()";
return (shift ? shifted : plain)[keycode - 30];
}
if (keycode == 44) {
return u' ';
}
if (keycode >= 45 && keycode <= 56) {
static constexpr char16_t plain[] = u"-=[]\\#;'`,./";
static constexpr char16_t shifted[] = u"_+{}|~:\"~<>?";
return (shift ? shifted : plain)[keycode - 45];
}
if (keycode >= 89 && keycode <= 98) {
static constexpr char16_t keypad[] = u"1234567890";
return keypad[keycode - 89];
}
if (keycode == 99) {
return u'.';
}
return u'\0';
}
bool ApplyKeyboardFilterOutput(ExternalInput* input, uint16_t keycode, uint32_t status,
bool multiline) {
constexpr uint32_t KEYCODE_VALID = 0x00000001;
constexpr uint32_t CHARACTER_VALID = 0x00000002;
if (keycode == input->key.keycode && status == input->key.status) {
return true;
}
if ((status & KEYCODE_VALID) == 0 || keycode == 0) {
return input->action == ExternalAction::Text && (status & CHARACTER_VALID) != 0;
}
input->key.keycode = keycode;
input->key.status = status;
input->text.clear();
switch (keycode) {
case 40:
case 88:
case 158:
input->action = multiline ? ExternalAction::Newline : ExternalAction::Accept;
break;
case 41: input->action = ExternalAction::Cancel; break;
case 42:
case 187: input->action = ExternalAction::Backspace; break;
case 43:
case 186: input->action = ExternalAction::None; break;
case 79: input->action = ExternalAction::MoveRight; break;
case 80: input->action = ExternalAction::MoveLeft; break;
default: {
const char16_t character = HidCharacter(keycode, status);
if (character == u'\0') {
return false;
}
input->action = ExternalAction::Text;
input->key.character = character;
input->text.push_back(character);
break;
}
}
return true;
}
void ClampText(std::u16string* text, uint32_t limit) {
if (text->size() <= limit) {
return;
}
text->resize(limit);
if (!text->empty() && text->back() >= 0xd800 && text->back() <= 0xdbff) {
text->pop_back();
}
}
uint32_t NormalizeCursor(std::u16string_view text, uint32_t cursor) {
cursor = std::min<uint32_t>(cursor, static_cast<uint32_t>(text.size()));
if (cursor > 0 && cursor < text.size() && text[cursor - 1] >= 0xd800 &&
text[cursor - 1] <= 0xdbff && text[cursor] >= 0xdc00 && text[cursor] <= 0xdfff) {
cursor++;
}
return cursor;
}
void WriteGuestText(const State& state, const std::u16string& text) {
if (state.param.input_text_buffer == nullptr) {
return;
}
const size_t size = std::min<size_t>(text.size(), state.param.max_text_length);
std::memcpy(state.param.input_text_buffer, text.data(), size * sizeof(char16_t));
state.param.input_text_buffer[size] = u'\0';
}
void NotifyVisibility(bool visible, uint64_t generation) {
if (const auto callback = g_visibility_callback.load(std::memory_order_acquire);
callback != nullptr) {
callback(visible, generation);
}
}
int ValidateExtended(const ExtendedParam* extended) {
if (extended == nullptr) {
return OK;
}
if ((extended->option & ~VALID_EXTENDED_OPTIONS) != 0 ||
((extended->option & 0x00004000) != 0 && (extended->option & 0x00000080) == 0) ||
extended->priority > 3 || extended->disable_device > 7 ||
(extended->ext_keyboard_mode & ~VALID_EXT_KEYBOARD_MODE) != 0 ||
!AllZero(extended->reserved, sizeof(extended->reserved))) {
return ERROR_INVALID_EXTENDED;
}
return OK;
}
int ValidateParam(const Param* param, const ExtendedParam* extended, std::u16string* initial,
std::u16string* title, std::u16string* placeholder) {
if (param == nullptr) {
return ERROR_INVALID_ADDRESS;
}
if (static_cast<uint32_t>(param->type) > static_cast<uint32_t>(Type::Number)) {
return ERROR_INVALID_TYPE;
}
if ((param->option & ~VALID_OPTIONS) != 0) {
return ERROR_INVALID_OPTION;
}
if ((param->supported_languages & ~VALID_LANGUAGES) != 0) {
return ERROR_INVALID_LANGUAGES;
}
const bool over_2k = (param->option & OPTION_USE_OVER_2K) != 0;
const float max_x = over_2k ? 3840.0f : 1920.0f;
const float max_y = over_2k ? 2160.0f : 1080.0f;
if (!std::isfinite(param->posx) || param->posx < 0.0f || param->posx >= max_x) {
return ERROR_INVALID_POSX;
}
if (!std::isfinite(param->posy) || param->posy < 0.0f || param->posy >= max_y) {
return ERROR_INVALID_POSY;
}
if (static_cast<uint32_t>(param->horizontal_alignment) > 2) {
return ERROR_INVALID_HALIGN;
}
if (static_cast<uint32_t>(param->vertical_alignment) > 2) {
return ERROR_INVALID_VALIGN;
}
const bool multiline = (param->option & OPTION_MULTILINE) != 0;
const bool password = (param->option & OPTION_PASSWORD) != 0;
if ((multiline && password) ||
(multiline && param->type != Type::Default && param->type != Type::BasicLatin) ||
(password && param->type != Type::BasicLatin && param->type != Type::Number)) {
return ERROR_INVALID_PARAM;
}
if (param->user_id < 0 || param->user_id == 0xff) {
return ERROR_INVALID_USER_ID;
}
if (!AllZero(param->reserved, sizeof(param->reserved))) {
return ERROR_INVALID_RESERVED;
}
if (param->input_text_buffer == nullptr) {
return ERROR_INVALID_TEXT_BUFFER;
}
const int extended_result = ValidateExtended(extended);
if (extended_result != OK) {
return extended_result;
}
if (static_cast<uint32_t>(param->enter_label) > static_cast<uint32_t>(EnterLabel::Go)) {
return ERROR_INVALID_ENTER_LABEL;
}
if (param->input_method != 0) {
return ERROR_INVALID_INPUT_METHOD;
}
if (param->max_text_length == 0 || param->max_text_length > IME_DIALOG_MAX_TEXT_LENGTH) {
return ERROR_INVALID_MAX_TEXT_LENGTH;
}
if (!ReadBounded(param->input_text_buffer, param->max_text_length, initial)) {
return ERROR_INVALID_TEXT_BUFFER;
}
if (!ReadBounded(param->title, IME_DIALOG_MAX_TITLE_LENGTH, title)) {
return ERROR_INVALID_TITLE;
}
if (!ReadBounded(param->placeholder, IME_DIALOG_MAX_PLACEHOLDER_LENGTH, placeholder)) {
return ERROR_INVALID_PARAM;
}
return OK;
}
void ComputePanelSize(const Param& param, const ExtendedParam* extended, uint32_t* width,
uint32_t* height) {
const bool multiline = (param.option & OPTION_MULTILINE) != 0;
const bool hide_keyboard = extended != nullptr && (param.option & OPTION_EXT_KEYBOARD) != 0 &&
(extended->option & 0x00000400) != 0;
if (param.type == Type::Number) {
*width = 370;
*height = hide_keyboard ? 102 : 522;
} else if (param.type == Type::BasicLatin) {
*width = 793;
if (hide_keyboard) {
*height = multiline ? 203 : 103;
} else {
*height = multiline ? 628 : 528;
}
} else {
*width = 793;
if (hide_keyboard) {
*height = multiline ? 268 : 168;
} else {
*height = multiline ? 628 : 528;
}
}
if ((param.option & OPTION_USE_OVER_2K) != 0) {
*width *= 2;
*height *= 2;
}
}
void ApplyFilterAndCommit() {
TextFilter filter = nullptr;
std::u16string source;
uint32_t max_length = 0;
uint64_t generation = 0;
uint64_t revision = 0;
{
std::scoped_lock lock(g_mutex);
if (g_state.status == Status::None || (!g_state.input_changed && !g_state.commit_pending)) {
return;
}
filter = g_state.input_changed ? g_state.param.filter : nullptr;
source = g_state.current_text;
max_length = g_state.param.max_text_length;
generation = g_state.generation;
revision = g_state.revision;
g_state.input_changed = false;
}
if (filter != nullptr) {
std::vector<char16_t> output(IME_DIALOG_MAX_TEXT_LENGTH + 1, u'\0');
uint32_t output_length = IME_DIALOG_MAX_TEXT_LENGTH;
if (filter(output.data(), &output_length, source.c_str(),
static_cast<uint32_t>(source.size())) == 0 &&
output_length <= IME_DIALOG_MAX_TEXT_LENGTH &&
IsValidUtf16(std::u16string_view(output.data(), output_length))) {
source.assign(output.data(), output.data() + output_length);
ClampText(&source, max_length);
}
}
std::scoped_lock lock(g_mutex);
if (g_state.generation != generation || g_state.revision != revision ||
g_state.status == Status::None) {
if (g_state.generation == generation &&
(g_state.status == Status::Running ||
(g_state.status == Status::Finished && g_state.end_status == EndStatus::Ok))) {
g_state.input_changed = true;
g_state.commit_pending = true;
}
return;
}
if (!g_state.input_changed) {
g_state.current_text = std::move(source);
g_state.cursor = NormalizeCursor(g_state.current_text, g_state.cursor);
}
const auto& committed =
g_state.status == Status::Finished && g_state.end_status != EndStatus::Ok
? g_state.original_text
: g_state.current_text;
WriteGuestText(g_state, committed);
g_state.commit_pending = false;
}
bool MatchRunningGeneration(uint64_t generation) {
return g_state.status == Status::Running && g_state.generation == generation;
}
bool FinishFromHost(uint64_t generation, EndStatus end_status) {
uint64_t notify_generation = 0;
{
std::scoped_lock lock(g_mutex);
if (!MatchRunningGeneration(generation)) {
return false;
}
g_state.status = Status::Finished;
g_state.end_status = end_status;
g_state.commit_pending = true;
g_state.revision++;
notify_generation = g_state.generation;
g_status.store(Status::Finished, std::memory_order_release);
g_revision.store(g_state.revision, std::memory_order_release);
}
NotifyVisibility(false, notify_generation);
return true;
}
void ApplyExternalInputs() {
std::vector<ExternalInput> inputs;
ExtKeyboardFilter filter = nullptr;
uint64_t generation = 0;
int32_t user_id = 0;
bool multiline = false;
{
std::scoped_lock lock(g_mutex);
if (g_state.status != Status::Running || g_state.external_inputs.empty()) {
return;
}
inputs.swap(g_state.external_inputs);
filter = g_state.extended.ext_keyboard_filter;
generation = g_state.generation;
user_id = g_state.param.user_id;
multiline = (g_state.param.option & OPTION_MULTILINE) != 0;
}
for (auto& input: inputs) {
{
std::scoped_lock lock(g_mutex);
if (!MatchRunningGeneration(generation)) {
break;
}
}
input.key.user_id = user_id;
bool accepted = true;
if (filter != nullptr) {
uint16_t output_keycode = input.key.keycode;
uint32_t output_status = input.key.status;
if (filter(&input.key, &output_keycode, &output_status, nullptr) == 0) {
accepted =
ApplyKeyboardFilterOutput(&input, output_keycode, output_status, multiline);
}
}
if (!accepted) {
continue;
}
switch (input.action) {
case ExternalAction::None: break;
case ExternalAction::Text: HostInsertText(generation, input.text); break;
case ExternalAction::Backspace: HostBackspace(generation); break;
case ExternalAction::MoveLeft: HostMoveCursor(generation, -1); break;
case ExternalAction::MoveRight: HostMoveCursor(generation, 1); break;
case ExternalAction::Cancel: HostCancel(generation); break;
case ExternalAction::Accept: HostAccept(generation); break;
case ExternalAction::Newline: HostInsertText(generation, u"\n"); break;
}
}
}
} // namespace
int KYTY_SYSV_ABI ImeDialogGetPanelSize(const Param* param, uint32_t* width, uint32_t* height) {
return ImeDialogGetPanelSizeExtended(param, nullptr, width, height);
}
int KYTY_SYSV_ABI ImeDialogGetPanelSizeExtended(const Param* param, const ExtendedParam* extended,
uint32_t* width, uint32_t* height) {
if (param == nullptr || width == nullptr || height == nullptr) {
return ERROR_INVALID_ADDRESS;
}
if (static_cast<uint32_t>(param->type) > static_cast<uint32_t>(Type::Number)) {
return ERROR_INVALID_TYPE;
}
if ((param->option & ~VALID_OPTIONS) != 0) {
return ERROR_INVALID_OPTION;
}
if ((param->supported_languages & ~VALID_LANGUAGES) != 0) {
return ERROR_INVALID_LANGUAGES;
}
const int extended_result = ValidateExtended(extended);
if (extended_result != OK) {
return extended_result;
}
ComputePanelSize(*param, extended, width, height);
return OK;
}
int KYTY_SYSV_ABI ImeDialogInit(const Param* param, const ExtendedParam* extended) {
if (g_status.load(std::memory_order_acquire) != Status::None) {
return ERROR_BUSY;
}
std::u16string initial;
std::u16string title;
std::u16string placeholder;
const int validation = ValidateParam(param, extended, &initial, &title, &placeholder);
if (validation != OK) {
return validation;
}
uint64_t generation = 0;
{
std::scoped_lock lock(g_mutex);
if (g_state.status != Status::None) {
return ERROR_BUSY;
}
const uint64_t next_generation = g_state.generation + 1;
const uint64_t next_revision = g_state.revision + 1;
g_state = {};
g_state.status = Status::Running;
g_state.generation = next_generation;
g_state.revision = next_revision;
g_state.param = *param;
if (extended != nullptr) {
g_state.extended = *extended;
}
g_state.original_text = initial;
g_state.current_text = std::move(initial);
g_state.cursor = static_cast<uint32_t>(g_state.current_text.size());
g_state.title = std::move(title);
g_state.placeholder = std::move(placeholder);
g_state.commit_pending = true;
generation = g_state.generation;
g_status.store(Status::Running, std::memory_order_release);
g_revision.store(g_state.revision, std::memory_order_release);
}
NotifyVisibility(true, generation);
return OK;
}
int KYTY_SYSV_ABI ImeDialogGetStatus() {
ApplyExternalInputs();
ApplyFilterAndCommit();
return static_cast<int>(g_status.load(std::memory_order_acquire));
}
int KYTY_SYSV_ABI ImeDialogAbort() {
uint64_t generation = 0;
{
std::scoped_lock lock(g_mutex);
if (g_state.status == Status::None) {
return ERROR_NOT_IN_USE;
}
if (g_state.status != Status::Running) {
return ERROR_NOT_RUNNING;
}
generation = g_state.generation;
}
if (!FinishFromHost(generation, EndStatus::Aborted)) {
return ERROR_NOT_RUNNING;
}
ApplyFilterAndCommit();
return OK;
}
int KYTY_SYSV_ABI ImeDialogGetResult(Result* result) {
{
std::scoped_lock lock(g_mutex);
if (g_state.status == Status::None) {
return ERROR_NOT_IN_USE;
}
}
if (result == nullptr) {
return ERROR_INVALID_ADDRESS;
}
if (!AllZero(result->reserved, sizeof(result->reserved))) {
return ERROR_INVALID_RESERVED;
}
{
std::scoped_lock lock(g_mutex);
if (g_state.status != Status::Finished) {
return ERROR_NOT_FINISHED;
}
}
ApplyFilterAndCommit();
std::scoped_lock lock(g_mutex);
if (g_state.status == Status::None) {
return ERROR_NOT_IN_USE;
}
if (g_state.status != Status::Finished) {
return ERROR_NOT_FINISHED;
}
result->endstatus = g_state.end_status;
return OK;
}
int KYTY_SYSV_ABI ImeDialogTerm() {
{
std::scoped_lock lock(g_mutex);
if (g_state.status == Status::None) {
return ERROR_NOT_IN_USE;
}
if (g_state.status != Status::Finished) {
return ERROR_NOT_FINISHED;
}
}
ApplyFilterAndCommit();
std::scoped_lock lock(g_mutex);
if (g_state.status == Status::None) {
return ERROR_NOT_IN_USE;
}
if (g_state.status != Status::Finished) {
return ERROR_NOT_FINISHED;
}
const uint64_t generation = g_state.generation;
const uint64_t revision = g_state.revision;
g_state = {};
g_state.generation = generation;
g_state.revision = revision;
g_status.store(Status::None, std::memory_order_release);
return OK;
}
int KYTY_SYSV_ABI ImeDialogGetPanelPositionAndForm(PositionAndForm* form) {
std::scoped_lock lock(g_mutex);
if (g_state.status == Status::None) {
return ERROR_NOT_IN_USE;
}
if (form == nullptr) {
return ERROR_INVALID_ADDRESS;
}
form->type = 2;
form->posx = g_state.param.posx;
form->posy = g_state.param.posy;
form->horizontal_alignment = g_state.param.horizontal_alignment;
form->vertical_alignment = g_state.param.vertical_alignment;
ComputePanelSize(g_state.param, &g_state.extended, &form->width, &form->height);
return OK;
}
VisualState GetVisualState() noexcept {
return {g_status.load(std::memory_order_acquire) == Status::Running,
g_revision.load(std::memory_order_acquire)};
}
void SetVisibilityCallback(VisibilityCallback callback) noexcept {
g_visibility_callback.store(callback, std::memory_order_release);
}
bool GetHostSnapshot(HostSnapshot* snapshot) {
if (snapshot == nullptr) {
return false;
}
std::scoped_lock lock(g_mutex);
if (g_state.status != Status::Running) {
return false;
}
snapshot->generation = g_state.generation;
snapshot->type = g_state.param.type;
snapshot->enter_label = g_state.param.enter_label;
snapshot->option = g_state.param.option;
snapshot->max_text_length = g_state.param.max_text_length;
snapshot->cursor = g_state.cursor;
snapshot->disable_device = g_state.extended.disable_device;
snapshot->key_panel_visible = (g_state.param.option & OPTION_EXT_KEYBOARD) == 0 ||
(g_state.extended.option & 0x00000400) == 0;
snapshot->posx = g_state.param.posx;
snapshot->posy = g_state.param.posy;
snapshot->horizontal_alignment = g_state.param.horizontal_alignment;
snapshot->vertical_alignment = g_state.param.vertical_alignment;
ComputePanelSize(g_state.param, &g_state.extended, &snapshot->panel_width,
&snapshot->panel_height);
snapshot->text = g_state.current_text;
snapshot->title = g_state.title;
snapshot->placeholder = g_state.placeholder;
return true;
}
bool HostInsertText(uint64_t generation, std::u16string_view text) {
std::scoped_lock lock(g_mutex);
if (!MatchRunningGeneration(generation) || text.empty()) {
return false;
}
if (!IsValidUtf16(text)) {
return false;
}
std::u16string allowed;
allowed.reserve(text.size());
for (const char16_t value: text) {
if (IsAllowedInput(value, g_state.param.type, g_state.param.option)) {
allowed.push_back(value);
}
}
if (allowed.empty()) {
return false;
}
const size_t available = g_state.param.max_text_length - g_state.current_text.size();
if (available == 0) {
return false;
}
std::u16string insertion(std::u16string_view(allowed).substr(0, available));
if (insertion.size() < allowed.size() && !insertion.empty() && insertion.back() >= 0xd800 &&
insertion.back() <= 0xdbff) {
insertion.pop_back();
}
if (insertion.empty()) {
return false;
}
std::u16string candidate = g_state.current_text;
candidate.insert(g_state.cursor, insertion);
g_state.current_text = std::move(candidate);
g_state.cursor += static_cast<uint32_t>(insertion.size());
g_state.input_changed = true;
g_state.commit_pending = true;
return true;
}
bool HostBackspace(uint64_t generation) {
std::scoped_lock lock(g_mutex);
if (!MatchRunningGeneration(generation) || g_state.cursor == 0) {
return false;
}
uint32_t first = g_state.cursor - 1;
if (first > 0 && g_state.current_text[first] >= 0xdc00 &&
g_state.current_text[first] <= 0xdfff && g_state.current_text[first - 1] >= 0xd800 &&
g_state.current_text[first - 1] <= 0xdbff) {
first--;
}
g_state.current_text.erase(first, g_state.cursor - first);
g_state.cursor = first;
g_state.input_changed = true;
g_state.commit_pending = true;
return true;
}
bool HostMoveCursor(uint64_t generation, int delta) {
std::scoped_lock lock(g_mutex);
if (!MatchRunningGeneration(generation) || delta == 0) {
return false;
}
int next = std::clamp(static_cast<int>(g_state.cursor) + delta, 0,
static_cast<int>(g_state.current_text.size()));
if (delta < 0 && next > 0 && next < static_cast<int>(g_state.current_text.size()) &&
g_state.current_text[next] >= 0xdc00 && g_state.current_text[next] <= 0xdfff &&
g_state.current_text[next - 1] >= 0xd800 && g_state.current_text[next - 1] <= 0xdbff) {
next--;
} else if (delta > 0 && next > 0 && next < static_cast<int>(g_state.current_text.size()) &&
g_state.current_text[next - 1] >= 0xd800 &&
g_state.current_text[next - 1] <= 0xdbff && g_state.current_text[next] >= 0xdc00 &&
g_state.current_text[next] <= 0xdfff) {
next++;
}
if (next == static_cast<int>(g_state.cursor)) {
return false;
}
g_state.cursor = static_cast<uint32_t>(next);
return true;
}
bool HostAccept(uint64_t generation) {
return FinishFromHost(generation, EndStatus::Ok);
}
bool HostCancel(uint64_t generation) {
return FinishFromHost(generation, EndStatus::UserCanceled);
}
bool HostQueueExternalInput(uint64_t generation, ExternalInput input) {
std::scoped_lock lock(g_mutex);
if (!MatchRunningGeneration(generation) || g_state.external_inputs.size() >= 128) {
return false;
}
g_state.external_inputs.push_back(std::move(input));
return true;
}
} // namespace Libs::Dialog::ImeDialog
+199
View File
@@ -0,0 +1,199 @@
#ifndef EMULATOR_INCLUDE_EMULATOR_LIBS_IMEDIALOG_H_
#define EMULATOR_INCLUDE_EMULATOR_LIBS_IMEDIALOG_H_
#include "common/abi.h"
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
namespace Libs::Dialog::ImeDialog {
constexpr uint32_t IME_DIALOG_MAX_TEXT_LENGTH = 2048;
constexpr uint32_t IME_DIALOG_MAX_TITLE_LENGTH = 128;
constexpr uint32_t IME_DIALOG_MAX_PLACEHOLDER_LENGTH = 64;
enum class Status : uint32_t { None = 0, Running = 1, Finished = 2 };
enum class EndStatus : uint32_t { Ok = 0, UserCanceled = 1, Aborted = 2 };
enum class Type : uint32_t { Default = 0, BasicLatin = 1, Url = 2, Mail = 3, Number = 4 };
enum class EnterLabel : uint32_t { Default = 0, Send = 1, Search = 2, Go = 3 };
enum class Alignment : uint32_t { Start = 0, Center = 1, End = 2 };
enum Option : uint32_t {
OPTION_MULTILINE = 0x00000001,
OPTION_NO_AUTO_CAPITALIZE = 0x00000002,
OPTION_PASSWORD = 0x00000004,
OPTION_LANGUAGES_FORCED = 0x00000008,
OPTION_EXT_KEYBOARD = 0x00000010,
OPTION_NO_LEARNING = 0x00000020,
OPTION_FIXED_POSITION = 0x00000040,
OPTION_DISABLE_COPY_PASTE = 0x00000080,
OPTION_DISABLE_RESUME = 0x00000100,
OPTION_DISABLE_AUTO_SPACE = 0x00000200,
OPTION_DISABLE_POSITION_ADJ = 0x00000800,
OPTION_EXPANDED_PREEDIT = 0x00001000,
OPTION_JAPANESE_CAPS_LOCK = 0x00002000,
OPTION_USE_OVER_2K = 0x00004000,
};
enum DisableDevice : uint32_t {
DISABLE_DEVICE_CONTROLLER = 0x00000001,
DISABLE_DEVICE_EXT_KEYBOARD = 0x00000002,
DISABLE_DEVICE_REMOTE_OSK = 0x00000004,
};
struct Color {
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a;
};
struct Keycode;
using TextFilter = int32_t(KYTY_SYSV_ABI*)(char16_t* out_text, uint32_t* out_text_length,
const char16_t* source_text,
uint32_t source_text_length);
using ExtKeyboardFilter = int(KYTY_SYSV_ABI*)(const Keycode* source_keycode, uint16_t* out_keycode,
uint32_t* out_status, void* reserved);
struct Param {
int32_t user_id;
Type type;
uint64_t supported_languages;
EnterLabel enter_label;
uint32_t input_method;
TextFilter filter;
uint32_t option;
uint32_t max_text_length;
char16_t* input_text_buffer;
float posx;
float posy;
Alignment horizontal_alignment;
Alignment vertical_alignment;
const char16_t* placeholder;
const char16_t* title;
int8_t reserved[16];
};
struct Result {
EndStatus endstatus;
int8_t reserved[12];
};
struct ExtendedParam {
uint32_t option;
Color color_base;
Color color_line;
Color color_text_field;
Color color_preedit;
Color color_button_default;
Color color_button_function;
Color color_button_symbol;
Color color_text;
Color color_special;
uint32_t priority;
const char* additional_dictionary_path;
ExtKeyboardFilter ext_keyboard_filter;
uint32_t disable_device;
uint32_t ext_keyboard_mode;
int8_t reserved[60];
};
struct PositionAndForm {
uint32_t type;
float posx;
float posy;
Alignment horizontal_alignment;
Alignment vertical_alignment;
uint32_t width;
uint32_t height;
};
struct Keycode {
uint16_t keycode;
char16_t character;
uint32_t status;
uint32_t type;
int32_t user_id;
uint32_t resource_id;
uint64_t timestamp;
};
enum class ExternalAction : uint8_t {
None,
Text,
Backspace,
MoveLeft,
MoveRight,
Cancel,
Accept,
Newline,
};
struct ExternalInput {
Keycode key;
ExternalAction action;
std::u16string text;
};
static_assert(sizeof(Param) == 0x60);
static_assert(offsetof(Param, input_text_buffer) == 0x28);
static_assert(offsetof(Param, title) == 0x48);
static_assert(sizeof(Result) == 0x10);
static_assert(sizeof(ExtendedParam) == 0x88);
static_assert(offsetof(ExtendedParam, additional_dictionary_path) == 0x30);
static_assert(sizeof(PositionAndForm) == 0x1c);
static_assert(sizeof(Keycode) == 0x20);
struct VisualState {
bool active;
uint64_t revision;
};
struct HostSnapshot {
uint64_t generation;
Type type;
EnterLabel enter_label;
uint32_t option;
uint32_t max_text_length;
uint32_t cursor;
uint32_t disable_device;
bool key_panel_visible;
float posx;
float posy;
Alignment horizontal_alignment;
Alignment vertical_alignment;
uint32_t panel_width;
uint32_t panel_height;
std::u16string text;
std::u16string title;
std::u16string placeholder;
};
using VisibilityCallback = void (*)(bool visible, uint64_t generation);
int KYTY_SYSV_ABI ImeDialogGetPanelSize(const Param* param, uint32_t* width, uint32_t* height);
int KYTY_SYSV_ABI ImeDialogGetPanelSizeExtended(const Param* param, const ExtendedParam* extended,
uint32_t* width, uint32_t* height);
int KYTY_SYSV_ABI ImeDialogInit(const Param* param, const ExtendedParam* extended);
int KYTY_SYSV_ABI ImeDialogGetStatus();
int KYTY_SYSV_ABI ImeDialogAbort();
int KYTY_SYSV_ABI ImeDialogGetResult(Result* result);
int KYTY_SYSV_ABI ImeDialogTerm();
int KYTY_SYSV_ABI ImeDialogGetPanelPositionAndForm(PositionAndForm* form);
VisualState GetVisualState() noexcept;
void SetVisibilityCallback(VisibilityCallback callback) noexcept;
bool GetHostSnapshot(HostSnapshot* snapshot);
bool HostInsertText(uint64_t generation, std::u16string_view text);
bool HostBackspace(uint64_t generation);
bool HostMoveCursor(uint64_t generation, int delta);
bool HostAccept(uint64_t generation);
bool HostCancel(uint64_t generation);
bool HostQueueExternalInput(uint64_t generation, ExternalInput input);
} // namespace Libs::Dialog::ImeDialog
#endif // EMULATOR_INCLUDE_EMULATOR_LIBS_IMEDIALOG_H_
+21 -2
View File
@@ -336,6 +336,17 @@ static AudioOut2PortStateEntry* audioout2_find_port_locked(AudioOut2PortHandle p
return nullptr;
}
static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) {
Common::LockGuard lock(g_audioout2_port_mutex);
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
state.pcm_data != nullptr && AudioInternal::AudioOutHasDevice(state.audio_handle)) {
return true;
}
}
return false;
}
static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, bool blocking) {
std::vector<AudioInternal::OutputParam> params;
params.reserve(AudioInternal::OUT_PORTS_MAX);
@@ -493,15 +504,23 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
uint32_t sleep_micros = audioout2_grain_micros(512);
for (;;) {
// Only a synchronous submission carrying PCM to a real device can rely on the SDL queue for
// pacing. Async pushes must retain queue-depth backpressure, and a handle without PCM (or a
// vibration/failed-open handle) has no downstream operation that can block this call.
const bool use_device_clock =
blocking != 0 && audioout2_context_has_queueable_device(ctx);
g_audioout2_context_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
audioout2_update_context_locked(state);
sleep_micros = audioout2_grain_micros(state->num_grains);
if (state->queued < state->queue_depth) {
if (state->queued < state->queue_depth || use_device_clock) {
if (state->queued == 0) {
state->last_update = LibKernel::KernelGetProcessTime();
}
state->queued++;
if (state->queued < state->queue_depth) {
state->queued++;
}
g_audioout2_context_mutex.Unlock();
audioout2_queue_context_audio(ctx, blocking != 0);
return OK;
+8
View File
@@ -1,5 +1,6 @@
#include "common/abi.h"
#include "libs/dialog.h"
#include "libs/imeDialog.h"
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
@@ -24,7 +25,14 @@ LIB_VERSION("ImeDialog", 1, "ImeDialog", 1, 1);
namespace ImeDialog = Dialog::ImeDialog;
LIB_DEFINE(InitDialog_1_ImeDialog) {
LIB_FUNC("CRD+jSErEJQ", ImeDialog::ImeDialogGetPanelSizeExtended);
LIB_FUNC("IADmD4tScBY", ImeDialog::ImeDialogGetStatus);
LIB_FUNC("gyTyVn+bXMw", ImeDialog::ImeDialogTerm);
LIB_FUNC("x01jxu+vxlc", ImeDialog::ImeDialogGetResult);
LIB_FUNC("wqsJvRXwl58", ImeDialog::ImeDialogGetPanelSize);
LIB_FUNC("NUeBrN7hzf0", ImeDialog::ImeDialogInit);
LIB_FUNC("oBmw4xrmfKs", ImeDialog::ImeDialogAbort);
LIB_FUNC("8jqzzPioYl8", ImeDialog::ImeDialogGetPanelPositionAndForm);
}
} // namespace LibImeDialog
+2 -1
View File
@@ -178,7 +178,8 @@ LIB_DEFINE(InitGraphicsDriver_1) {
LIB_FUNC("bxca0BK4FNg", Gen5::GraphicsUnknownJumpPatchSetTarget);
LIB_FUNC("2BS4EtAaF28", Gen5::GraphicsJumpPatchSetTarget);
LIB_FUNC("h9z6+0hEydk", Gen5::GraphicsSuspendPoint);
LIB_FUNC("qj7QZpgr9Uw", Gen5::GraphicsUnknownQj7QZpgr9Uw);
LIB_FUNC("qj7QZpgr9Uw", Gen5::GraphicsDcbContextStateOp);
LIB_FUNC("H6vHS5cidSA", Gen5::GraphicsDcbContextStateOpGetSize);
LIB_FUNC("BfBDZGbti7A", Gen5::GraphicsGetIsTrinityMode);
LIB_FUNC("dbOlWdppb4o", LibGen5::Graphics5UnknownDb);
+33 -19
View File
@@ -13,6 +13,7 @@
#include "kernel/memory.h"
#include "kernel/pthread.h"
#include "kernel/semaphore.h"
#include "kernel/syncOnAddress.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "libs/network.h"
@@ -2134,26 +2135,37 @@ uint64_t KYTY_SYSV_ABI cfwBSQyr5Ys(uint64_t a1, uint64_t a2, uint64_t a3, uint64
return 0;
}
uint64_t KYTY_SYSV_ABI KernelSyncOnAddressV1(uint64_t op, uint64_t address, uint64_t value,
uint64_t size, uint64_t timeout, uint64_t flags) {
static std::atomic_uint32_t log_count = 0;
const auto index = log_count.fetch_add(1, std::memory_order_relaxed);
if (index < 16) {
LOGF("\t libkernel_sync_on_address_v1: op=0x%016" PRIx64 ", address=0x%016" PRIx64
", value=0x%016" PRIx64 ", size=0x%016" PRIx64 ", timeout=0x%016" PRIx64
", flags=0x%016" PRIx64 "\n",
op, address, value, size, timeout, flags);
static void LogExperimentalSyncOnAddress(std::atomic_bool& logged, const char* function_name) {
if (!logged.exchange(true, std::memory_order_relaxed)) {
::printf("WARNING: %s is experimental\n", function_name);
LOGF("WARNING: %s is experimental\n", function_name);
}
}
if (op != 0 && address == 0 && value == 0 && size == 0) {
// This unsupported form is used as a yield/wait by some Unity jobs.
// SleepMicro() uses a sub-millisecond busy wait on Windows, which can
// pin every worker thread when the guest polls this path.
Common::Thread::Sleep(timeout == 0 ? 1 : 2);
}
int KYTY_SYSV_ABI KernelSyncOnAddressWait(volatile uint32_t* address, uint32_t expected,
const uint32_t* timeout_micros) {
return LibKernel::SyncOnAddress::Wait32(address, expected, timeout_micros,
LibKernel::KernelDispatchPendingSignalForCurrentThread);
}
return 0;
int KYTY_SYSV_ABI KernelSyncOnAddressWait32(volatile uint32_t* address, uint32_t expected,
const uint32_t* timeout_micros) {
static std::atomic_bool logged {false};
LogExperimentalSyncOnAddress(logged, "sceKernelSyncOnAddressWait32");
return LibKernel::SyncOnAddress::Wait32(address, expected, timeout_micros,
LibKernel::KernelDispatchPendingSignalForCurrentThread);
}
int KYTY_SYSV_ABI KernelSyncOnAddressWait64(volatile uint64_t* address, uint64_t expected,
const uint32_t* timeout_micros) {
static std::atomic_bool logged {false};
LogExperimentalSyncOnAddress(logged, "sceKernelSyncOnAddressWait64");
return LibKernel::SyncOnAddress::Wait64(address, expected, timeout_micros,
LibKernel::KernelDispatchPendingSignalForCurrentThread);
}
int KYTY_SYSV_ABI KernelSyncOnAddressWake(volatile void* address, int32_t count) {
return LibKernel::SyncOnAddress::Wake(address, count);
}
LIB_DEFINE(InitLibKernel_1_Posix) {
@@ -3405,8 +3417,10 @@ LIB_DEFINE(InitLibKernel_1) {
LIB_FUNC("Xjoosiw+XPI", LibKernel::KernelUuidCreate);
LIB_FUNC("DLORcroUqbc", LibKernel::KernelGetOpenPsId);
LIB_FUNC("zE-wXIZjLoM", LibKernel::KernelDebugRaiseExceptionOnReleaseMode);
LIB_FUNC("Hc4CaR6JBL0", Posix::KernelSyncOnAddressV1);
LIB_FUNC("q2y-wDIVWZA", Posix::KernelSyncOnAddressV1);
LIB_FUNC("Hc4CaR6JBL0", Posix::KernelSyncOnAddressWait);
LIB_FUNC("B2n8aDorSH4", Posix::KernelSyncOnAddressWait32);
LIB_FUNC("PZQhiiLXRFs", Posix::KernelSyncOnAddressWait64);
LIB_FUNC("q2y-wDIVWZA", Posix::KernelSyncOnAddressWake);
AddLibkernelUnityFunc(s, "Qhv5ARAoOEc",
reinterpret_cast<uint64_t>(LibKernel::KernelRemoveExceptionHandler),
+14
View File
@@ -47,6 +47,7 @@ static void PrintUsage() {
" --game-patch <json> Validated patch plan to apply before entry.\n");
::printf(" --screen-width <num> Window width. Default: 1280.\n");
::printf(" --screen-height <num> Window height. Default: 720.\n");
::printf(" --fullscreen Run in borderless desktop fullscreen.\n");
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n");
::printf(" --vulkan-validation <true|false> Enable Vulkan validation.\n");
@@ -63,6 +64,7 @@ static void PrintUsage() {
::printf(" --spirv-debug-printf <true|false> Enable SPIR-V debug printf.\n");
::printf(
" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
::printf(" --keymap <Pad=Key> DualSense mapping; may be repeated.\n");
::printf(" --rd Enable RenderDoc capture.\n");
}
@@ -131,6 +133,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
continue;
}
if (arg == "--fullscreen") {
options.config.fullscreen_enabled = true;
continue;
}
if (!Common::StartsWith(arg, "--")) {
::printf("game input must be provided with --game\n");
return false;
@@ -241,6 +248,13 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--keymap") {
const auto split = value.find('=');
if (split == std::string::npos || split == 0 || split + 1 == value.size()) {
::printf("invalid keymap: %s\n", value.c_str());
return false;
}
options.config.keymap.push_back(value);
} else {
::printf("unknown option: %s\n", arg.c_str());
return false;
-2
View File
@@ -101,8 +101,6 @@ if(KYTY_CLANG_CL)
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")
add_link_options("$<$<CONFIG:RELEASE>:/OPT:NOREF>")
if(KYTY_WARNINGS_ARE_ERRORS)
#set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} /WX")
+123 -4
View File
@@ -19,6 +19,8 @@ namespace AudioOut2 = Libs::Audio::AudioOut2;
std::mutex g_device_mutex;
std::condition_variable g_device_cv;
std::vector<int> g_live_devices;
std::vector<int> g_device_backed_handles;
std::vector<bool> g_output_blocking;
int g_next_device = 1;
int g_open_waiters = 0;
bool g_block_opens = false;
@@ -61,6 +63,17 @@ struct PortState {
uint64_t reserved[6];
};
struct Attribute {
uint32_t attribute_id;
int32_t reserved;
const void* value;
size_t value_size;
};
struct Pcm {
const void* data;
};
const auto* AsParam(const PortParam* param) {
return reinterpret_cast<const AudioOut2::AudioOut2PortParam*>(param);
}
@@ -73,6 +86,10 @@ auto* AsState(PortState* state) {
return reinterpret_cast<AudioOut2::AudioOut2PortState*>(state);
}
const auto* AsAttribute(const Attribute* attribute) {
return reinterpret_cast<const AudioOut2::AudioOut2Attribute*>(attribute);
}
PortParam MakeParam(uint32_t data_format = 0x200) {
PortParam param {};
param.data_format = data_format;
@@ -80,9 +97,9 @@ PortParam MakeParam(uint32_t data_format = 0x200) {
return param;
}
AudioOut2::AudioOut2ContextHandle CreateContext() {
AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4) {
ContextParam param {};
param.queue_depth = 4;
param.queue_depth = queue_depth;
param.num_grains = 512;
AudioOut2::AudioOut2ContextHandle context = 0;
Check(AudioOut2::AudioOut2ContextCreate(AsParam(&param), nullptr, 0, &context) == OK,
@@ -112,6 +129,23 @@ int LiveDeviceCount() {
return static_cast<int>(g_live_devices.size());
}
void SetPcm(AudioOut2::AudioOut2PortHandle port, const void* data) {
const Pcm pcm {data};
const Attribute attribute {0, 0, &pcm, sizeof(pcm)};
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) == OK,
"setting PCM failed");
}
void ResetOutputCalls() {
std::lock_guard lock(g_device_mutex);
g_output_blocking.clear();
}
std::vector<bool> OutputCalls() {
std::lock_guard lock(g_device_mutex);
return g_output_blocking;
}
void TestSlotReuse() {
const auto context = CreateContext();
const auto param = MakeParam();
@@ -202,14 +236,83 @@ void TestContextDestroyCancelsPendingCreate() {
Check(LiveDeviceCount() == 0, "cancelled port create leaked a device");
}
void TestSynchronousDevicePushBypassesModelledQueue() {
const auto context = CreateContext(1);
const auto param = MakeParam();
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK,
"device port create failed");
uint32_t pcm[512] {};
SetPcm(port, pcm);
ResetOutputCalls();
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "first sync push failed");
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK,
"device-paced sync push was blocked by modelled queue");
const auto calls = OutputCalls();
Check(calls.size() == 2, "sync pushes did not reach the device backend");
Check(calls[0] && calls[1], "sync pushes lost their blocking mode");
AudioOut2::AudioOut2PortDestroy(port);
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestAsynchronousDevicePushKeepsQueueBounded() {
const auto context = CreateContext(1);
const auto param = MakeParam();
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK,
"device port create failed");
uint32_t pcm[512] {};
SetPcm(port, pcm);
ResetOutputCalls();
Check(AudioOut2::AudioOut2ContextPush(context, 0) == OK, "first async push failed");
Check(AudioOut2::AudioOut2ContextPush(context, 0) != OK,
"full async queue accepted another buffer");
const auto calls = OutputCalls();
Check(calls.size() == 1 && !calls[0], "rejected async push reached the device backend");
uint32_t queued = 0;
uint32_t available = 0;
Check(AudioOut2::AudioOut2ContextGetQueueLevel(context, &queued, &available) == OK,
"queue-level query failed");
Check(queued == 1 && available == 0, "async queue level does not match accepted pushes");
AudioOut2::AudioOut2PortDestroy(port);
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestHandleWithoutPcmDoesNotBypassQueue() {
const auto context = CreateContext(1);
const auto param = MakeParam();
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK,
"device port create failed");
ResetOutputCalls();
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "empty sync push failed");
Check(AudioOut2::AudioOut2ContextPush(context, 0) != OK,
"handle without PCM bypassed queue backpressure");
Check(OutputCalls().empty(), "empty push reached the device backend");
AudioOut2::AudioOut2PortDestroy(port);
AudioOut2::AudioOut2ContextDestroy(context);
}
} // namespace
namespace Libs::Audio::AudioInternal {
int AudioOutOpen(int /*type*/, uint32_t /*samples_num*/, uint32_t /*freq*/, Format /*format*/) {
int AudioOutOpen(int type, uint32_t /*samples_num*/, uint32_t /*freq*/, Format /*format*/) {
std::unique_lock lock(g_device_mutex);
const int handle = g_next_device++;
g_live_devices.push_back(handle);
if (type != 10) {
g_device_backed_handles.push_back(handle);
}
g_open_waiters++;
g_device_cv.notify_all();
g_device_cv.wait(lock, []() { return !g_block_opens; });
@@ -222,9 +325,22 @@ void AudioOutClose(int handle) {
if (it != g_live_devices.end()) {
g_live_devices.erase(it);
}
const auto device_it =
std::find(g_device_backed_handles.begin(), g_device_backed_handles.end(), handle);
if (device_it != g_device_backed_handles.end()) {
g_device_backed_handles.erase(device_it);
}
}
uint32_t AudioOutOutputs(const OutputParam* /*params*/, uint32_t /*num*/, bool /*blocking*/) {
bool AudioOutHasDevice(int handle) {
std::lock_guard lock(g_device_mutex);
return std::find(g_device_backed_handles.begin(), g_device_backed_handles.end(), handle) !=
g_device_backed_handles.end();
}
uint32_t AudioOutOutputs(const OutputParam* /*params*/, uint32_t /*num*/, bool blocking) {
std::lock_guard lock(g_device_mutex);
g_output_blocking.push_back(blocking);
return 0;
}
@@ -244,6 +360,9 @@ int main() {
TestFullTableRecovers();
TestConcurrentCreates();
TestContextDestroyCancelsPendingCreate();
TestSynchronousDevicePushBypassesModelledQueue();
TestAsynchronousDevicePushKeepsQueueBounded();
TestHandleWithoutPcmDoesNotBypassQueue();
std::printf("AudioOut2PortTests: all cases passed\n");
return 0;
}
+320
View File
@@ -0,0 +1,320 @@
#include "libs/imeDialog.h"
#include <array>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <utility>
namespace Ime = Libs::Dialog::ImeDialog;
#define CHECK(condition) \
do { \
if (!(condition)) { \
std::abort(); \
} \
} while (false)
namespace {
constexpr int Error(uint32_t value) { return static_cast<int32_t>(value); }
int g_filter_calls = 0;
int g_keyboard_filter_calls = 0;
bool g_block_keyboard = false;
uint16_t g_remap_keyboard = 0;
int32_t KYTY_SYSV_ABI CopyFilter(char16_t *out_text, uint32_t *out_length,
const char16_t *source,
uint32_t source_length) {
g_filter_calls++;
CHECK(*out_length >= source_length);
std::memcpy(out_text, source, source_length * sizeof(char16_t));
*out_length = source_length;
return 0;
}
int32_t KYTY_SYSV_ABI SupplementaryFilter(char16_t *out_text,
uint32_t *out_length,
const char16_t *, uint32_t) {
CHECK(*out_length >= 2);
out_text[0] = 0xd83d;
out_text[1] = 0xde00;
*out_length = 2;
return 0;
}
int KYTY_SYSV_ABI KeyboardFilter(const Ime::Keycode *source,
uint16_t *out_keycode, uint32_t *out_status,
void *) {
g_keyboard_filter_calls++;
if (g_block_keyboard) {
*out_keycode = 0;
*out_status = 0;
return 0;
}
if (g_remap_keyboard != 0) {
*out_keycode = g_remap_keyboard;
*out_status = source->status;
return 0;
}
return -1;
}
Ime::Param MakeParam(char16_t *text) {
Ime::Param param{};
param.user_id = 1000;
param.type = Ime::Type::Default;
param.supported_languages = 1;
param.enter_label = Ime::EnterLabel::Default;
param.input_text_buffer = text;
param.max_text_length = 31;
return param;
}
void TestLayoutAndPanelSize() {
static_assert(sizeof(Ime::Param) == 0x60);
static_assert(sizeof(Ime::ExtendedParam) == 0x88);
static_assert(sizeof(Ime::Result) == 0x10);
static_assert(sizeof(Ime::PositionAndForm) == 0x1c);
std::array<char16_t, 32> text{};
auto param = MakeParam(text.data());
uint32_t width = 0;
uint32_t height = 0;
CHECK(Ime::ImeDialogGetPanelSize(&param, &width, &height) == 0);
CHECK(width == 793 && height == 528);
param.option = Ime::OPTION_MULTILINE;
CHECK(Ime::ImeDialogGetPanelSize(&param, &width, &height) == 0);
CHECK(width == 793 && height == 628);
param.option = 0;
param.type = Ime::Type::Number;
CHECK(Ime::ImeDialogGetPanelSize(&param, &width, &height) == 0);
CHECK(width == 370 && height == 522);
param = MakeParam(text.data());
param.option = Ime::OPTION_EXT_KEYBOARD;
Ime::ExtendedParam extended{};
extended.option = 0x00000400;
CHECK(Ime::ImeDialogGetPanelSizeExtended(&param, &extended, &width,
&height) == 0);
CHECK(width == 793 && height == 168);
param.type = Ime::Type::BasicLatin;
CHECK(Ime::ImeDialogGetPanelSizeExtended(&param, &extended, &width,
&height) == 0);
CHECK(width == 793 && height == 103);
}
void TestAcceptLifecycle() {
std::array<char16_t, 32> text{u'A', u'l', u'i', u'c', u'e'};
auto param = MakeParam(text.data());
CHECK(Ime::ImeDialogInit(&param, nullptr) == 0);
CHECK(Ime::ImeDialogGetStatus() == static_cast<int>(Ime::Status::Running));
CHECK(Ime::ImeDialogInit(nullptr, nullptr) == Error(0x80bc0001));
Ime::HostSnapshot snapshot;
CHECK(Ime::GetHostSnapshot(&snapshot));
CHECK(snapshot.text == u"Alice");
CHECK(Ime::HostInsertText(snapshot.generation, u" 2"));
CHECK(Ime::HostBackspace(snapshot.generation));
CHECK(Ime::HostAccept(snapshot.generation));
CHECK(Ime::ImeDialogGetStatus() == static_cast<int>(Ime::Status::Finished));
CHECK(std::u16string(text.data()) == u"Alice ");
Ime::Result result{};
CHECK(Ime::ImeDialogGetResult(&result) == 0);
CHECK(result.endstatus == Ime::EndStatus::Ok);
CHECK(Ime::ImeDialogTerm() == 0);
CHECK(Ime::ImeDialogGetStatus() == static_cast<int>(Ime::Status::None));
}
void TestCancelAndAbortRestoreText() {
for (const auto expected :
{Ime::EndStatus::UserCanceled, Ime::EndStatus::Aborted}) {
std::array<char16_t, 32> text{u'o', u'l', u'd'};
auto param = MakeParam(text.data());
CHECK(Ime::ImeDialogInit(&param, nullptr) == 0);
Ime::HostSnapshot snapshot;
CHECK(Ime::GetHostSnapshot(&snapshot));
CHECK(Ime::HostInsertText(snapshot.generation, u" value"));
if (expected == Ime::EndStatus::UserCanceled) {
CHECK(Ime::HostCancel(snapshot.generation));
} else {
CHECK(Ime::ImeDialogAbort() == 0);
}
Ime::Result result{};
CHECK(Ime::ImeDialogGetResult(&result) == 0);
CHECK(result.endstatus == expected);
CHECK(std::u16string(text.data()) == u"old");
CHECK(Ime::ImeDialogTerm() == 0);
}
}
void TestValidation() {
std::array<char16_t, 32> text{};
auto param = MakeParam(text.data());
param.reserved[0] = 1;
CHECK(Ime::ImeDialogInit(&param, nullptr) == Error(0x80bc0032));
param.reserved[0] = 0;
param.max_text_length = Ime::IME_DIALOG_MAX_TEXT_LENGTH + 1;
CHECK(Ime::ImeDialogInit(&param, nullptr) == Error(0x80bc0016));
CHECK(Ime::ImeDialogGetResult(nullptr) == Error(0x80bc0107));
param = MakeParam(text.data());
Ime::ExtendedParam extended{};
extended.ext_keyboard_mode = 0x00000004;
CHECK(Ime::ImeDialogInit(&param, &extended) == Error(0x80bc001c));
extended = {};
extended.option = 0x00000200;
extended.disable_device =
Ime::DISABLE_DEVICE_CONTROLLER | Ime::DISABLE_DEVICE_EXT_KEYBOARD;
CHECK(Ime::ImeDialogInit(&param, &extended) == 0);
Ime::HostSnapshot snapshot;
CHECK(Ime::GetHostSnapshot(&snapshot));
CHECK(snapshot.disable_device == 3);
CHECK(Ime::ImeDialogAbort() == 0);
Ime::Result result{};
CHECK(Ime::ImeDialogGetResult(&result) == 0);
CHECK(Ime::ImeDialogTerm() == 0);
}
void TestFilteringAndInputPolicy() {
std::array<char16_t, 32> text{u'1'};
auto param = MakeParam(text.data());
param.type = Ime::Type::Number;
param.filter = CopyFilter;
g_filter_calls = 0;
CHECK(Ime::ImeDialogInit(&param, nullptr) == 0);
CHECK(Ime::ImeDialogGetStatus() == static_cast<int>(Ime::Status::Running));
CHECK(g_filter_calls == 0);
CHECK(Ime::ImeDialogGetResult(nullptr) == Error(0x80bc0031));
CHECK(Ime::ImeDialogTerm() == Error(0x80bc0106));
CHECK(g_filter_calls == 0);
Ime::HostSnapshot snapshot;
CHECK(Ime::GetHostSnapshot(&snapshot));
CHECK(Ime::HostInsertText(snapshot.generation, u"a2.\n"));
CHECK(Ime::ImeDialogGetStatus() == static_cast<int>(Ime::Status::Running));
CHECK(g_filter_calls == 1);
CHECK(std::u16string(text.data()) == u"12.");
CHECK(Ime::HostCancel(snapshot.generation));
Ime::Result result{};
CHECK(Ime::ImeDialogGetResult(&result) == 0);
CHECK(Ime::ImeDialogTerm() == 0);
}
void TestExternalKeyboardFilter() {
std::array<char16_t, 32> text{};
auto param = MakeParam(text.data());
Ime::ExtendedParam extended{};
extended.ext_keyboard_filter = KeyboardFilter;
CHECK(Ime::ImeDialogInit(&param, &extended) == 0);
Ime::HostSnapshot snapshot;
CHECK(Ime::GetHostSnapshot(&snapshot));
Ime::ExternalInput input{};
input.key.status = 3;
input.key.keycode = 4;
input.action = Ime::ExternalAction::Text;
input.text = u"x";
g_keyboard_filter_calls = 0;
g_block_keyboard = true;
CHECK(Ime::HostQueueExternalInput(snapshot.generation, input));
CHECK(Ime::ImeDialogGetStatus() == static_cast<int>(Ime::Status::Running));
CHECK(g_keyboard_filter_calls == 1);
CHECK(text[0] == u'\0');
g_block_keyboard = false;
CHECK(Ime::HostQueueExternalInput(snapshot.generation, std::move(input)));
CHECK(Ime::ImeDialogGetStatus() == static_cast<int>(Ime::Status::Running));
CHECK(g_keyboard_filter_calls == 2);
CHECK(std::u16string(text.data()) == u"x");
input = {};
input.key.status = 3;
input.key.keycode = 4;
input.action = Ime::ExternalAction::Text;
input.text = u"x";
g_remap_keyboard = 5;
CHECK(Ime::HostQueueExternalInput(snapshot.generation, std::move(input)));
CHECK(Ime::ImeDialogGetStatus() == static_cast<int>(Ime::Status::Running));
CHECK(g_keyboard_filter_calls == 3);
CHECK(std::u16string(text.data()) == u"xb");
input = {};
input.key.status = 0x00000201;
input.key.keycode = 4;
input.action = Ime::ExternalAction::Text;
input.text = u"x";
g_remap_keyboard = 50;
CHECK(Ime::HostQueueExternalInput(snapshot.generation, std::move(input)));
CHECK(Ime::ImeDialogGetStatus() == static_cast<int>(Ime::Status::Running));
CHECK(g_keyboard_filter_calls == 4);
CHECK(std::u16string(text.data()) == u"xb~");
input = {};
input.key.status = 3;
input.key.keycode = 4;
input.action = Ime::ExternalAction::Text;
input.text = u"x";
g_remap_keyboard = 88;
CHECK(Ime::HostQueueExternalInput(snapshot.generation, std::move(input)));
CHECK(Ime::ImeDialogGetStatus() == static_cast<int>(Ime::Status::Finished));
CHECK(g_keyboard_filter_calls == 5);
Ime::Result result{};
CHECK(Ime::ImeDialogGetResult(&result) == 0);
CHECK(result.endstatus == Ime::EndStatus::Ok);
CHECK(Ime::ImeDialogTerm() == 0);
g_remap_keyboard = 0;
CHECK(Ime::ImeDialogInit(&param, &extended) == 0);
CHECK(Ime::GetHostSnapshot(&snapshot));
input = {};
input.key.status = 1;
input.key.keycode = 41;
input.action = Ime::ExternalAction::Cancel;
CHECK(Ime::HostQueueExternalInput(snapshot.generation, std::move(input)));
input = {};
input.key.status = 3;
input.key.keycode = 4;
input.action = Ime::ExternalAction::Text;
input.text = u"ignored";
CHECK(Ime::HostQueueExternalInput(snapshot.generation, std::move(input)));
CHECK(Ime::ImeDialogGetStatus() == static_cast<int>(Ime::Status::Finished));
CHECK(g_keyboard_filter_calls == 6);
result = {};
CHECK(Ime::ImeDialogGetResult(&result) == 0);
CHECK(Ime::ImeDialogTerm() == 0);
}
void TestFilteredCursorBoundary() {
std::array<char16_t, 8> text{};
auto param = MakeParam(text.data());
param.filter = SupplementaryFilter;
CHECK(Ime::ImeDialogInit(&param, nullptr) == 0);
Ime::HostSnapshot snapshot;
CHECK(Ime::GetHostSnapshot(&snapshot));
CHECK(Ime::HostInsertText(snapshot.generation, u"x"));
CHECK(Ime::ImeDialogGetStatus() == static_cast<int>(Ime::Status::Running));
CHECK(Ime::GetHostSnapshot(&snapshot));
CHECK(snapshot.cursor == 2);
CHECK(text[0] == 0xd83d && text[1] == 0xde00 && text[2] == 0);
CHECK(Ime::HostCancel(snapshot.generation));
Ime::Result result{};
CHECK(Ime::ImeDialogGetResult(&result) == 0);
CHECK(Ime::ImeDialogTerm() == 0);
}
} // namespace
int main() {
TestLayoutAndPanelSize();
TestAcceptLifecycle();
TestCancelAndAbortRestoreText();
TestValidation();
TestFilteringAndInputPolicy();
TestExternalKeyboardFilter();
TestFilteredCursorBoundary();
return 0;
}
+51
View File
@@ -6,7 +6,9 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <semaphore>
#include <string>
#include <thread>
#include <vector>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
@@ -265,6 +267,7 @@ void TestRangeInvalidation() {
flushes++;
tracker.ForEachDownloadRange<true>(address + 16, size - 32,
[](uint64_t, uint64_t) noexcept {});
tracker.MarkRegionAsCpuModified(address + 16, size - 32);
});
Check(flushes == 1 && !tracker.IsRegionGpuModified(address, size) &&
tracker.IsRegionCpuModified(address, size) && IsWritable(memory) &&
@@ -277,6 +280,53 @@ void TestRangeInvalidation() {
Release(page_manager, memory, size);
}
void TestGpuReacquisitionAfterInvalidation() {
TrackerHarness harness;
auto &tracker = harness.tracker;
auto &page_manager = harness.page_manager;
const auto page_size = page_manager.GetPageSize();
auto *memory = Allocate(page_manager, 1);
const auto address = reinterpret_cast<uint64_t>(memory);
tracker.ForEachUploadRange(
address, page_size, true, [](uint64_t, uint64_t) noexcept {},
[]() noexcept {});
Check(tracker.IsRegionGpuModified(address, page_size) &&
!tracker.IsRegionCpuModified(address, page_size),
"reacquisition setup did not establish GPU ownership");
uint32_t flushes = 0;
uint32_t uploads = 0;
std::binary_semaphore reacquire{0};
std::binary_semaphore reacquired{0};
std::jthread publisher([&] {
reacquire.acquire();
tracker.ForEachUploadRange(
address + 16, 32, true,
[&](uint64_t, uint64_t) noexcept { uploads++; }, []() noexcept {});
reacquired.release();
});
tracker.InvalidateRegion(address + 16, 32, [&] {
flushes++;
tracker.ForEachDownloadRange<true>(address + 16, 32,
[](uint64_t, uint64_t) noexcept {});
tracker.MarkRegionAsCpuModified(address + 16, 32);
reacquire.release();
reacquired.acquire();
});
publisher.join();
Check(flushes == 1 && uploads == 1 &&
tracker.IsRegionGpuModified(address, page_size) &&
!tracker.IsRegionCpuModified(address, page_size) &&
!IsWritable(memory),
"invalidation rejected a new generation of GPU ownership");
tracker.UnmarkRegionAsGpuModified(address, page_size);
tracker.MarkRegionAsCpuModified(address, page_size);
tracker.UntrackMemory(address, page_size);
Release(page_manager, memory, page_size);
}
void TestGpuDirtyBits() {
TrackerHarness harness;
auto &tracker = harness.tracker;
@@ -635,6 +685,7 @@ int main(int argc, char **argv) {
TestQueriesDoNotRequireMappedOwnership();
TestCpuDirtyUpload();
TestRangeInvalidation();
TestGpuReacquisitionAfterInvalidation();
TestGpuDirtyBits();
TestExactDirtyIntervalsSharingTrackerPage();
TestGpuDownloadProtectionMirrors();
+28 -152
View File
@@ -35,171 +35,50 @@ void YieldMany() {
}
}
void TestFaultBlocksPublisher() {
ResourceMutex mutex;
std::atomic_bool publisher_started {false};
std::atomic_bool publisher_entered {false};
std::thread publisher;
void TestOwnership() {
ResourceMutex mutex;
Check(!mutex.IsOwnedByCurrentThread(), "fresh mutex reported an owner");
{
ResourceMutex::FaultScope fault(mutex);
publisher = std::thread([&] {
publisher_started.store(true, std::memory_order_release);
std::lock_guard lock(mutex);
publisher_entered.store(true, std::memory_order_release);
});
while (!publisher_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"publisher entered during active fault transaction");
std::lock_guard lock(mutex);
Check(mutex.IsOwnedByCurrentThread(), "owner tracking was not installed");
}
publisher.join();
Check(publisher_entered.load(std::memory_order_acquire),
"publisher did not resume after fault transaction");
Check(!mutex.IsOwnedByCurrentThread(), "owner tracking survived unlock");
}
void TestFaultDrainsExistingOwner() {
void TestSerializesTransactions() {
ResourceMutex mutex;
std::unique_lock owner(mutex);
std::atomic_bool fault_started {false};
std::atomic_bool fault_entered {false};
std::atomic_bool release_fault {false};
std::atomic_bool publisher_started {false};
std::atomic_bool publisher_entered {false};
std::thread fault([&] {
fault_started.store(true, std::memory_order_release);
ResourceMutex::FaultScope scope(mutex);
fault_entered.store(true, std::memory_order_release);
while (!release_fault.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
});
while (!fault_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!fault_entered.load(std::memory_order_acquire),
"fault transaction did not wait for existing owner");
owner.unlock();
while (!fault_entered.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
std::thread publisher([&] {
publisher_started.store(true, std::memory_order_release);
std::atomic_bool contender_started {false};
std::atomic_bool contender_entered {false};
std::thread contender([&] {
contender_started.store(true, std::memory_order_release);
std::lock_guard lock(mutex);
publisher_entered.store(true, std::memory_order_release);
contender_entered.store(true, std::memory_order_release);
});
while (!publisher_started.load(std::memory_order_acquire)) {
while (!contender_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"publisher entered before drained fault completed");
release_fault.store(true, std::memory_order_release);
fault.join();
publisher.join();
Check(publisher_entered.load(std::memory_order_acquire),
"publisher did not resume after drained fault");
}
void TestFaultScopesSerialize() {
ResourceMutex mutex;
std::atomic_bool first_entered {false};
std::atomic_bool second_started {false};
std::atomic_bool second_entered {false};
std::atomic_bool release_first {false};
std::atomic_bool release_second {false};
std::atomic_bool publisher_entered {false};
std::thread first([&] {
ResourceMutex::FaultScope scope(mutex);
first_entered.store(true, std::memory_order_release);
while (!release_first.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
});
while (!first_entered.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
std::thread second([&] {
second_started.store(true, std::memory_order_release);
ResourceMutex::FaultScope scope(mutex);
second_entered.store(true, std::memory_order_release);
while (!release_second.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
});
while (!second_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!second_entered.load(std::memory_order_acquire),
"second fault transaction entered before first completed");
release_first.store(true, std::memory_order_release);
while (!second_entered.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
std::thread publisher([&] {
std::lock_guard lock(mutex);
publisher_entered.store(true, std::memory_order_release);
});
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"publisher entered during second fault transaction");
release_second.store(true, std::memory_order_release);
first.join();
second.join();
publisher.join();
Check(publisher_entered.load(std::memory_order_acquire),
"publisher did not resume after serialized faults");
}
void TestPreownedFaultKeepsResourceTransaction() {
ResourceMutex mutex;
std::atomic_bool publisher_started {false};
std::atomic_bool publisher_entered {false};
std::thread publisher;
std::unique_lock owner(mutex);
{
ResourceMutex::FaultScope fault(mutex);
publisher = std::thread([&] {
publisher_started.store(true, std::memory_order_release);
std::lock_guard lock(mutex);
publisher_entered.store(true, std::memory_order_release);
});
while (!publisher_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"publisher entered during preowned fault transaction");
}
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"preowned fault released its outer resource transaction");
Check(!contender_entered.load(std::memory_order_acquire),
"contender entered an active resource transaction");
owner.unlock();
publisher.join();
Check(publisher_entered.load(std::memory_order_acquire),
"publisher did not resume after outer transaction");
contender.join();
Check(contender_entered.load(std::memory_order_acquire),
"contender did not resume after resource transaction");
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
[[noreturn]] void RunDeathCase(const char* name) {
[[noreturn]] void RunDeathCase() {
ResourceMutex mutex;
if (std::strcmp(name, "recursive-lock") == 0) {
std::lock_guard first(mutex);
std::lock_guard second(mutex);
} else if (std::strcmp(name, "nested-fault") == 0) {
ResourceMutex::FaultScope first(mutex);
ResourceMutex::FaultScope second(mutex);
}
std::lock_guard first(mutex);
std::lock_guard second(mutex);
std::_Exit(0x7f);
}
void CheckDeathCase(const char* name) {
void CheckDeathCase() {
char path[MAX_PATH] {};
Check(GetModuleFileNameA(nullptr, path, MAX_PATH) != 0, "GetModuleFileName failed");
std::string command = std::string("\"") + path + "\" --death " + name;
std::string command = std::string("\"") + path + "\" --death";
std::vector<char> mutable_command(command.begin(), command.end());
mutable_command.push_back('\0');
STARTUPINFOA startup {sizeof(startup)};
@@ -224,20 +103,17 @@ void CheckDeathCase(const char* name) {
int main(int argc, char** argv) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (argc == 3 && std::strcmp(argv[1], "--death") == 0) {
RunDeathCase(argv[2]);
if (argc == 2 && std::strcmp(argv[1], "--death") == 0) {
RunDeathCase();
}
#else
(void)argc;
(void)argv;
#endif
TestFaultBlocksPublisher();
TestFaultDrainsExistingOwner();
TestFaultScopesSerialize();
TestPreownedFaultKeepsResourceTransaction();
TestOwnership();
TestSerializesTransactions();
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
CheckDeathCase("recursive-lock");
CheckDeathCase("nested-fault");
CheckDeathCase();
#endif
std::puts("ResourceMutexTests: all cases passed");
return 0;
+694 -29
View File
@@ -42,6 +42,7 @@
#include "graphics/shader/rectListShader.h"
#include "graphics/shader/shader.h"
#include "kernel/memory.h"
#include "libs/agc.h"
#include "spirv-tools/libspirv.hpp"
#if __has_include("graphics/host_gpu/renderer/renderTargetBarriers.h")
@@ -58,6 +59,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <bit>
#include <chrono>
#include <cinttypes>
@@ -1372,12 +1374,14 @@ public:
void CheckGpuMappedRangeLifecycle() {
EnsureRuntimeContext();
CommandScheduler scheduler(Renderer(), m_runtime_context);
auto& context = Renderer();
CommandScheduler scheduler(context, m_runtime_context);
HW::Context registers {};
HW::UserConfig user_config {};
HW::Shader shaders {};
scheduler.Begin(registers, user_config, shaders);
Gpu gpu(Renderer());
context.InitializeGpu(nullptr);
auto& gpu = context.GetGpu();
GpuResourceManager resources(m_runtime_context, scheduler);
resources.SetGpu(&gpu);
@@ -1416,6 +1420,7 @@ public:
resources.SetGpu(nullptr);
scheduler.Finish();
context.ShutdownGpu();
std::printf("[host] %-32s ok\n", "GpuMappedRangeLifecycle");
}
@@ -1567,21 +1572,73 @@ public:
gpu_thread = std::this_thread::get_id();
Require("GpuCommandLane", "FIFO", order == 1,
"synchronous command overtook an older host command");
Require("GpuCommandLane", "generic context",
!Gpu::IsCommandProcessorThread() && Gpu::CurrentCommandProcessor() == nullptr,
"generic host command manufactured a PM4 processor context");
Require("GpuCommandLane", "GPU context", Gpu::IsGpuThread(),
"host command did not run on the GPU thread");
gpu.SendCommandSync([&nested_thread] { nested_thread = std::this_thread::get_id(); });
order = 2;
});
gpu.SendCommandSyncWithProcessor([&](CommandProcessor& processor) {
Require("GpuCommandLane", "processor context",
Gpu::IsCommandProcessorThread() && Gpu::CurrentCommandProcessor() == &processor,
"resource command did not receive its explicit processor context");
});
Require("GpuCommandLane", "dispatch thread",
order == 2 && gpu_thread != caller_thread && nested_thread == gpu_thread,
"host command did not run on the GPU thread or nested sync deadlocked");
alignas(uint32_t) uint32_t packet_marker_a = 0;
alignas(uint32_t) uint32_t packet_marker_b = 0;
const auto write_packet = [](uint32_t* packet, uint32_t* destination, uint32_t value) {
const auto destination_address = reinterpret_cast<uint64_t>(destination);
packet[0] = KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0);
packet[1] = 0;
packet[2] = static_cast<uint32_t>(destination_address);
packet[3] = static_cast<uint32_t>(destination_address >> 32u);
packet[4] = value;
};
std::array<uint32_t, 1024> polling_loop {};
for (size_t i = 0; i < polling_loop.size() - 4; i += 2) {
polling_loop[i] = KYTY_PM4(2, Pm4::IT_NOP, Pm4::R_ZERO);
polling_loop[i + 1] = 0;
}
const auto loop_address = reinterpret_cast<uint64_t>(polling_loop.data());
polling_loop[1020] = KYTY_PM4(4, Pm4::IT_INDIRECT_BUFFER, 0);
polling_loop[1021] = static_cast<uint32_t>(loop_address);
polling_loop[1022] = static_cast<uint32_t>(loop_address >> 32u);
polling_loop[1023] = 0x0f200000u | static_cast<uint32_t>(polling_loop.size());
std::array<uint32_t, 14> polling_commands {};
write_packet(polling_commands.data(), &packet_marker_a, 11);
polling_commands[5] = KYTY_PM4(4, Pm4::IT_INDIRECT_BUFFER, 0);
polling_commands[6] = static_cast<uint32_t>(loop_address);
polling_commands[7] = static_cast<uint32_t>(loop_address >> 32u);
polling_commands[8] = 0x0f200000u | static_cast<uint32_t>(polling_loop.size());
write_packet(polling_commands.data() + 9, &packet_marker_b, 22);
std::binary_semaphore stream_gate_entered {0};
std::binary_semaphore stream_gate_release {0};
gpu.SendCommand([&] {
stream_gate_entered.release();
stream_gate_release.acquire();
});
gpu.Submit(polling_commands.data(), polling_commands.size(), nullptr, 0);
stream_gate_entered.acquire();
stream_gate_release.release();
uint32_t packet_marker_a_at_callback = UINT32_MAX;
uint32_t packet_marker_b_at_callback = UINT32_MAX;
for (uint32_t attempt = 0; attempt < 8 && packet_marker_a_at_callback != 11; attempt++) {
gpu.SendCommandSync([&] {
if (packet_marker_a == 11 && packet_marker_b == 0) {
packet_marker_a_at_callback = packet_marker_a;
packet_marker_b_at_callback = packet_marker_b;
polling_loop[1020] = KYTY_PM4(2, Pm4::IT_NOP, Pm4::R_ZERO);
polling_loop[1021] = 0;
polling_loop[1022] = KYTY_PM4(2, Pm4::IT_NOP, Pm4::R_ZERO);
polling_loop[1023] = 0;
}
});
std::this_thread::yield();
}
gpu.Done();
Require("GpuCommandLane", "packet-boundary command polling",
packet_marker_a_at_callback == 11 && packet_marker_b_at_callback == 0 &&
packet_marker_a == 11 && packet_marker_b == 22,
"host command waited for an entire non-suspended PM4 stream");
uint32_t label = 0;
uint32_t prefix = 0;
uint32_t suffix = 0;
@@ -1760,7 +1817,7 @@ public:
gpu.Submit(dma_commands.data(), static_cast<uint32_t>(dma_commands.size()), nullptr, 0);
gpu.Done();
constexpr uint32_t clean_fill_value = 0xdecafbad;
gpu.SendCommandSyncWithProcessor([&](CommandProcessor&) {
gpu.SendCommandSync([&] {
auto& buffer_cache = resources.GetBufferCache();
Require("GpuCommandLane", "clean cached setup",
buffer_cache.HasPageOverlap(clean_cached_fill, sizeof(source_words)) &&
@@ -1840,14 +1897,116 @@ public:
clean_copy_words[1]},
"host-memory fill/copy did not update the clean cached mirror");
gpu.SendCommandSyncWithProcessor([&](CommandProcessor& processor) {
Require("GpuCommandLane", "processor fault context",
Gpu::CurrentCommandProcessor() == &processor,
"processor resource test lost its command context");
Require("GpuCommandLane", "processor memory invalidation",
gpu.SendCommandSync([&] {
Require("GpuCommandLane", "GPU memory invalidation",
resources.InvalidateMemory(fault_base, sizeof(uint32_t)),
"processor memory invalidation did not find its mapped range");
"GPU memory invalidation did not find its mapped range");
});
auto& buffer_cache = resources.GetBufferCache();
Require("GpuCommandLane", "clean tracked fault setup",
buffer_cache.HasPageOverlap(immediate_dst, sizeof(uint32_t)) &&
!buffer_cache.HasGpuDirtyBytes(immediate_dst, sizeof(uint32_t)),
"clean write-fault test address is not backed by a clean cached Buffer");
constexpr uint64_t dirty_fault_address = fault_base + 0xd000;
constexpr uint32_t dirty_fault_value = 0x5a17c0deu;
constexpr uint32_t dirty_fault_stale = 0x0ddba11u;
std::memcpy(reinterpret_cast<void*>(dirty_fault_address), &dirty_fault_stale,
sizeof(dirty_fault_stale));
gpu.SendCommandSync([&] {
auto dirty = buffer_cache.ObtainBuffer(scheduler.Current(), dirty_fault_address,
sizeof(dirty_fault_value), true, false);
Require("GpuCommandLane", "dirty tracked fault setup", dirty.owner != nullptr,
"dirty write-fault test could not create a cached Buffer");
scheduler.Current().RetainResourceUntilFence(dirty.owner);
buffer_cache.FillBuffer(dirty_fault_address, sizeof(dirty_fault_value),
dirty_fault_value);
});
Require("GpuCommandLane", "dirty tracked fault ownership",
buffer_cache.HasGpuDirtyBytes(dirty_fault_address, sizeof(dirty_fault_value)),
"dirty write-fault test address did not acquire GPU ownership");
std::binary_semaphore block_entered {0};
std::binary_semaphore block_release {0};
std::binary_semaphore clean_fault_complete {0};
bool clean_fault_handled = false;
gpu.SendCommand([&] {
block_entered.release();
block_release.acquire();
});
block_entered.acquire();
std::jthread clean_fault_thread([&] {
clean_fault_handled = resources.HandleFault(PageFaultAccess::Write, immediate_dst);
clean_fault_complete.release();
});
const bool clean_fault_was_direct =
clean_fault_complete.try_acquire_for(std::chrono::seconds(1));
if (!clean_fault_was_direct) {
block_release.release();
clean_fault_complete.acquire();
}
clean_fault_thread.join();
Require("GpuCommandLane", "clean fault direct path",
clean_fault_was_direct && clean_fault_handled,
"a clean CPU write fault waited for the blocked GPU command lane");
std::binary_semaphore dirty_fault_complete {0};
bool dirty_fault_handled = false;
std::jthread dirty_fault_thread([&] {
dirty_fault_handled =
resources.HandleFault(PageFaultAccess::Write, dirty_fault_address);
dirty_fault_complete.release();
});
const bool dirty_fault_bypassed_lane =
dirty_fault_complete.try_acquire_for(std::chrono::milliseconds(200));
block_release.release();
if (!dirty_fault_bypassed_lane) {
dirty_fault_complete.acquire();
}
dirty_fault_thread.join();
uint32_t dirty_fault_backing = 0;
std::memcpy(&dirty_fault_backing, reinterpret_cast<const void*>(dirty_fault_address),
sizeof(dirty_fault_backing));
Require("GpuCommandLane", "dirty fault synchronization",
!dirty_fault_bypassed_lane && dirty_fault_handled &&
dirty_fault_backing == dirty_fault_value &&
!buffer_cache.HasGpuDirtyBytes(dirty_fault_address, sizeof(dirty_fault_value)),
"a GPU-dirty write fault bypassed synchronization or lost the published bytes");
constexpr uint64_t empty_copy_fault_address = fault_base + 0xe000;
constexpr uint32_t empty_copy_fault_value = 0x6b28d1efu;
gpu.SendCommandSync([&] {
auto dirty = buffer_cache.ObtainBuffer(scheduler.Current(), empty_copy_fault_address,
sizeof(empty_copy_fault_value), true, false);
Require("GpuCommandLane", "empty-copy write setup", dirty.owner != nullptr,
"empty-copy write test could not create a cached Buffer");
scheduler.Current().RetainResourceUntilFence(dirty.owner);
buffer_cache.FillBuffer(empty_copy_fault_address, sizeof(empty_copy_fault_value),
empty_copy_fault_value);
buffer_cache.ReadMemory(empty_copy_fault_address, sizeof(empty_copy_fault_value));
const bool gpu_owned = buffer_cache.IsRegionGpuModified(
empty_copy_fault_address, sizeof(empty_copy_fault_value));
const bool cpu_owned = buffer_cache.IsRegionCpuModified(
empty_copy_fault_address, sizeof(empty_copy_fault_value));
Require("GpuCommandLane", "empty-copy read ownership",
!gpu_owned && !cpu_owned,
"readback did not leave clean ownership for the queued write invalidation");
buffer_cache.ReadMemory(empty_copy_fault_address, sizeof(empty_copy_fault_value), true);
});
uint32_t empty_copy_fault_backing = 0;
std::memcpy(&empty_copy_fault_backing, reinterpret_cast<const void*>(empty_copy_fault_address),
sizeof(empty_copy_fault_backing));
const bool empty_copy_cpu_owned = buffer_cache.IsRegionCpuModified(
empty_copy_fault_address, sizeof(empty_copy_fault_value));
const bool empty_copy_gpu_owned = buffer_cache.IsRegionGpuModified(
empty_copy_fault_address, sizeof(empty_copy_fault_value));
const bool empty_copy_dirty =
buffer_cache.HasGpuDirtyBytes(empty_copy_fault_address, sizeof(empty_copy_fault_value));
Require("GpuCommandLane", "empty-copy write ownership",
empty_copy_fault_backing == empty_copy_fault_value &&
empty_copy_cpu_owned && !empty_copy_gpu_owned && !empty_copy_dirty,
"write invalidation without a remaining copy did not establish CPU ownership");
resources.UnmapMemory(fault_base, fault_size);
Require("GpuCommandLane", "processor fault unmap",
Libs::LibKernel::Memory::KernelMunmap(fault_base, fault_size) == 0,
@@ -2073,12 +2232,14 @@ public:
constexpr uint32_t ring_fault_second_value = 0x4e5f6071u;
EnsureRuntimeContext();
CommandScheduler scheduler(Renderer(), m_runtime_context);
auto& context = Renderer();
CommandScheduler scheduler(context, m_runtime_context);
HW::Context registers {};
HW::UserConfig user_config {};
HW::Shader shaders {};
scheduler.Begin(registers, user_config, shaders);
Gpu gpu(Renderer());
context.InitializeGpu(nullptr);
auto& gpu = context.GetGpu();
int64_t direct_offset = -1;
Require(name, "direct allocation",
@@ -2471,7 +2632,7 @@ public:
resources.UnmapMemory(base, allocation_size);
scheduler.Finish();
}
gpu.Shutdown();
context.ShutdownGpu();
Require(name, "unmap direct backing",
Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0,
"dirty-GC direct-memory mapping release failed");
@@ -2488,12 +2649,14 @@ public:
constexpr uint64_t allocation_size = 0x2800000;
constexpr uint64_t allocation_alignment = 0x200000;
EnsureRuntimeContext();
CommandScheduler scheduler(Renderer(), m_runtime_context);
auto& context = Renderer();
CommandScheduler scheduler(context, m_runtime_context);
HW::Context registers {};
HW::UserConfig user_config {};
HW::Shader shaders {};
scheduler.Begin(registers, user_config, shaders);
Gpu gpu(Renderer());
context.InitializeGpu(nullptr);
auto& gpu = context.GetGpu();
int64_t direct_offset = -1;
Require(name, "direct allocation",
Libs::LibKernel::Memory::KernelAllocateDirectMemory(
@@ -4949,7 +5112,7 @@ public:
resources.UnmapMemory(base, allocation_size);
scheduler.Finish();
}
gpu.Shutdown();
context.ShutdownGpu();
Require(name, "unmap direct backing",
Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0,
"cache direct-memory mapping release failed");
@@ -8887,6 +9050,8 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode, const std::set<ShaderOpcode>&
case Opcode::BufferAtomicAnd:
case Opcode::BufferAtomicOr:
case Opcode::BufferAtomicXor:
case Opcode::BufferAtomicFMin:
case Opcode::BufferAtomicFMax:
case Opcode::FlatLoadUbyte:
case Opcode::FlatLoadSbyte:
case Opcode::FlatLoadUshort:
@@ -9417,6 +9582,8 @@ TestCase ScalarCompareOps() {
code.push_back(EncodeSMovB32(11, InlineU32(0)));
code.push_back(EncodeSMovB32(12, InlineU32(2)));
code.push_back(EncodeSMovB32(13, InlineU32(0)));
code.push_back(EncodeSMovB32(14, InlineU32(1)));
code.push_back(EncodeSMovB32(15, InlineU32(1)));
u32 dst = 20;
auto append_compare = [&](u32 opcode, u32 src0, u32 src1) {
@@ -9434,9 +9601,13 @@ TestCase ScalarCompareOps() {
append_compare(0x08, 4, 3);
append_compare(0x09, 4, 4);
append_compare(0x0b, 3, 4);
append_compare(0x12, 10, 10);
append_compare(0x12, 10, 12);
append_compare(0x12, 10, 14);
append_compare(0x13, 10, 12);
append_compare(0x13, 10, 14);
for (u32 i = 0; i < 11u; i++) {
for (u32 i = 0; i < 15u; i++) {
AppendStoreSgpr(&code, 20u + i, i);
}
AppendEnd(&code);
@@ -9444,10 +9615,10 @@ TestCase ScalarCompareOps() {
return {"ScalarCompareOps",
code,
{},
std::vector<u32>(11, 1),
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1},
{O::SMovB32, O::SCmpEqI32, O::SCmpLgI32, O::SCmpGtI32, O::SCmpGeI32, O::SCmpLtI32,
O::SCmpLeI32, O::SCmpLgU32, O::SCmpGtU32, O::SCmpGeU32, O::SCmpLeU32, O::SCmpLgU64,
O::SCselectB32, O::VMovB32, O::BufferStoreDword, O::SEndpgm}};
O::SCmpLeI32, O::SCmpLgU32, O::SCmpGtU32, O::SCmpGeU32, O::SCmpLeU32, O::SCmpEqU64,
O::SCmpLgU64, O::SCselectB32, O::VMovB32, O::BufferStoreDword, O::SEndpgm}};
}
TestCase ScalarShiftAddAndMaskOps() {
@@ -9947,6 +10118,73 @@ TestCase VectorIntegerOps() {
O::VCndmaskB32, O::BufferStoreDword, O::SEndpgm}};
}
TestCase Vop2SdwaSubNcExactByte2Destination() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovU32(&code, 11, 2);
code.push_back(0x4c1616f9u);
code.push_back(0x0686128du);
AppendStoreVgpr(&code, 11, 0);
AppendEnd(&code);
TestCase test;
test.name = "Vop2SdwaSubNcExactByte2Destination";
test.code = code;
test.expected = {0x000b0002u};
test.opcodes = {O::VMovB32, O::VSubNcU32, O::BufferStoreDword, O::SEndpgm};
test.required_spirv = {"OpISub", "OpShiftLeftLogical", "OpBitwiseOr"};
return test;
}
TestCase Vop2SdwaSubNcPreservesByteAndWordDestinations() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovU32(&code, 0, 3);
for (u32 dst = 10; dst <= 15; dst++) {
AppendVMovLiteral(&code, dst, 0xa1b2c3d4u);
}
for (u32 sel = 0; sel <= 5; sel++) {
code.push_back(EncodeVop2(0x26, 10 + sel, 249, 0));
code.push_back(EncodeVop2Sdwa(InlineU32(20), sel, 2, 6, 6, 0, 0, 0, 0, 0, 0, 1));
AppendStoreVgpr(&code, 10 + sel, sel);
}
AppendEnd(&code);
return {"Vop2SdwaSubNcPreservesByteAndWordDestinations",
code,
{},
{0xa1b2c311u, 0xa1b211d4u, 0xa111c3d4u, 0x11b2c3d4u, 0xa1b20011u,
0x0011c3d4u},
{O::VMovB32, O::VSubNcU32, O::BufferStoreDword, O::SEndpgm}};
}
TestCase Vop2SdwaMinU32PreservesWordDestination() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovLiteral(&code, 10, 0xa1b2c3d4u);
AppendVMovU32(&code, 12, 7);
code.push_back(0x261418f9u);
code.push_back(0x0686149fu);
AppendStoreVgpr(&code, 10, 0);
AppendVMovLiteral(&code, 10, 0xa1b2c3d4u);
AppendVMovU32(&code, 12, 64);
code.push_back(0x261418f9u);
code.push_back(0x0686149fu);
AppendStoreVgpr(&code, 10, 1);
AppendEnd(&code);
TestCase test;
test.name = "Vop2SdwaMinU32PreservesWordDestination";
test.code = code;
test.expected = {0xa1b20007u, 0xa1b2001fu};
test.opcodes = {O::VMovB32, O::VMinU32, O::BufferStoreDword, O::SEndpgm};
test.required_spirv = {"OpULessThan", "OpSelect", "OpBitwiseOr"};
return test;
}
TestCase VectorShiftCountsMaskLowBits() {
using O = ShaderOpcode;
@@ -11430,6 +11668,33 @@ TestCase VectorVop3CompareNeU64OnGpu() {
O::SEndpgm}};
}
TestCase VectorVop3CompareEqI64OnGpu() {
using O = ShaderOpcode;
std::vector<u32> code;
code.push_back(EncodeVop1(0x01, 1, InlineU32(1)));
code.push_back(EncodeSop1(0x04, 4, 126)); // s_mov_b64 s[4:5], exec
code.push_back(0xd4a2006au);
code.push_back(0x0000087eu); // v_cmp_eq_i64 vcc, exec, s[4:5]
code.push_back(EncodeVop2(0x01, 2, InlineU32(0), 1));
AppendStoreVgpr(&code, 2, 0);
AppendSMovLiteral(&code, 4, 0);
code.push_back(0xd4a2006au);
code.push_back(0x0000087eu); // low dword mismatch
code.push_back(EncodeVop2(0x01, 3, InlineU32(0), 1));
AppendStoreVgpr(&code, 3, 1);
AppendEnd(&code);
return {"VectorVop3CompareEqI64OnGpu",
code,
{},
{1, 0},
{O::VMovB32, O::SMovB64, O::VCmpEqI64, O::VCndmaskB32, O::SMovB32,
O::BufferStoreDword, O::SEndpgm}};
}
TestCase VectorCompareClassF32() {
using O = ShaderOpcode;
@@ -11545,6 +11810,32 @@ TestCase Vop2SdwaCndmaskSourceModifier() {
{O::VMovB32, O::VCmpTU32, O::VCndmaskB32, O::BufferStoreDword, O::SEndpgm}};
}
TestCase Vop2SdwaCndmaskFullDestinationWithSubDwordSource() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovLiteral(&code, 0, 0xabcd1234u);
AppendVMovLiteral(&code, 1, 0x55667788u);
code.push_back(EncodeVopc(0xc0, Vgpr(0), 0)); // v_cmp_f_u32
code.push_back(0x020e02f9u);
code.push_back(0x06040600u);
AppendStoreVgpr(&code, 7, 0);
code.push_back(EncodeVopc(0xc7, Vgpr(0), 0)); // v_cmp_t_u32
code.push_back(0x020e02f9u);
code.push_back(0x06040600u);
AppendStoreVgpr(&code, 7, 1);
AppendEnd(&code);
TestCase test;
test.name = "Vop2SdwaCndmaskFullDestinationWithSubDwordSource";
test.code = code;
test.expected = {0x00001234u, 0x55667788u};
test.opcodes = {O::VMovB32, O::VCmpFU32, O::VCmpTU32, O::VCndmaskB32,
O::BufferStoreDword, O::SEndpgm};
test.required_spirv = {"OpBitFieldUExtract", "OpSelect"};
return test;
}
TestCase Vop3CndmaskUsesSgprMaskLaneBits() {
using O = ShaderOpcode;
@@ -13640,6 +13931,192 @@ TestCase BufferAtomicGlc0DoesNotReturnOldValue() {
{O::VMovB32, O::BufferAtomicAdd, O::BufferStoreDword, O::SEndpgm}};
}
TestCase BufferAtomicFMinExactRawGlcModes() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovLiteral(&code, 0, 0x40000000u); // 2.0
code.push_back(0xe0fc0000u);
code.push_back(0x80010000u); // exact failing buffer_atomic_fmin, GLC=0
AppendStoreVgpr(&code, 0, 1);
AppendVMovLiteral(&code, 0, 0x3f800000u); // 1.0
code.push_back(0xe0fc4000u);
code.push_back(0x80010000u); // same instruction with GLC=1
AppendStoreVgpr(&code, 0, 2);
AppendEnd(&code);
TestCase test;
test.name = "BufferAtomicFMinExactRawGlcModes";
test.code = code;
test.initial = {0x40800000u, 0, 0}; // 4.0
test.expected = {0x3f800000u, 0x40000000u, 0x40000000u};
test.opcodes = {O::VMovB32, O::BufferAtomicFMin, O::BufferStoreDword, O::SEndpgm};
const auto descriptor =
MakeStructuredStorageBufferData(0, static_cast<u32>(test.initial.size() * sizeof(u32)));
std::copy_n(descriptor.begin(), 4, test.user_data.begin() + 4);
test.user_data[50] = 1u << 20u;
test.has_user_data = true;
return test;
}
TestCase BufferAtomicFMinSpecialValues() {
using O = ShaderOpcode;
const u32 values[] = {
0x40000000u, // 2.0
0x7f800000u, // +infinity
0xff800000u, // -infinity
0x3f800000u, // 1.0
0x7fc00000u, // quiet NaN
0x00000000u, // +0.0
0x80000000u, // -0.0
0x00000000u, // +0.0
0x80000001u, // smallest negative denorm
};
std::vector<u32> code;
for (u32 i = 0; i < static_cast<u32>(std::size(values)); i++) {
AppendVMovU32(&code, 20, i * 4u);
AppendVMovLiteral(&code, i, values[i]);
AppendBufferStoreOpcode(&code, 0x3f, i, 20, true);
}
for (u32 i = 0; i < static_cast<u32>(std::size(values)); i++) {
AppendStoreVgpr(&code, i, i + static_cast<u32>(std::size(values)));
}
AppendEnd(&code);
return {"BufferAtomicFMinSpecialValues",
code,
{0x40800000u, 0xbf800000u, 0x7f800000u, 0x7fc00000u, 0x3f800000u,
0x80000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0x40000000u, 0xbf800000u, 0xff800000u, 0x7fc00000u, 0x3f800000u,
0x80000000u, 0x00000000u, 0x00000000u, 0x80000001u, 0x40800000u,
0xbf800000u, 0x7f800000u, 0x7fc00000u, 0x3f800000u, 0x80000000u,
0x00000000u, 0x00000001u, 0x00000000u},
{O::VMovB32, O::BufferAtomicFMin, O::BufferStoreDword, O::SEndpgm}};
}
TestCase BufferAtomicFMinContendedWorkgroup() {
using O = ShaderOpcode;
std::vector<u32> code;
code.push_back(EncodeVop1(0x06, 1, Vgpr(0))); // v_cvt_f32_u32 v1, thread_id.x
AppendVMovU32(&code, 20, 0);
AppendBufferStoreOpcode(&code, 0x3f, 1, 20);
AppendEnd(&code);
TestCase test;
test.name = "BufferAtomicFMinContendedWorkgroup";
test.code = code;
test.initial = {0x42c80000u}; // 100.0
test.expected = {0x00000000u}; // min(100.0, 0.0 .. 63.0)
test.opcodes = {O::VCvtF32U32, O::VMovB32, O::BufferAtomicFMin, O::SEndpgm};
test.compute_info.threads_num[0] = 64;
test.compute_info.threads_num[1] = 1;
test.compute_info.threads_num[2] = 1;
test.compute_info.thread_ids_num = 1;
test.has_compute_info = true;
return test;
}
TestCase BufferAtomicFMaxExactRawGlcModes() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovLiteral(&code, 3, 0x40000000u); // 2.0
code.push_back(0xe100000cu);
code.push_back(0x80010300u); // exact failing buffer_atomic_fmax v3, offset 12, GLC=0
AppendStoreVgpr(&code, 3, 4);
AppendVMovLiteral(&code, 0, 0x40800000u); // 4.0
code.push_back(0xe100400cu);
code.push_back(0x80010000u); // same address with GLC=1
AppendStoreVgpr(&code, 0, 5);
AppendEnd(&code);
TestCase test;
test.name = "BufferAtomicFMaxExactRawGlcModes";
test.code = code;
test.initial = {0, 0, 0, 0x3f800000u, 0, 0}; // memory[3] = 1.0
test.expected = {0, 0, 0, 0x40800000u, 0x40000000u, 0x40000000u};
test.opcodes = {O::VMovB32, O::BufferAtomicFMax, O::BufferStoreDword, O::SEndpgm};
const auto descriptor =
MakeStructuredStorageBufferData(0, static_cast<u32>(test.initial.size() * sizeof(u32)));
std::copy_n(descriptor.begin(), 4, test.user_data.begin() + 4);
test.user_data[50] = 1u << 20u;
test.has_user_data = true;
return test;
}
TestCase BufferAtomicFMaxSpecialValues() {
using O = ShaderOpcode;
const u32 values[] = {
0x40000000u, // 2.0, finite update
0x40000000u, // 2.0, finite no-update
0x7f800000u, // incoming +infinity
0xff800000u, // incoming -infinity
0x3f800000u, // finite against old +infinity
0x3f800000u, // finite against old -infinity
0x3f800000u, // finite against old quiet NaN
0x7fcabcdeu, // incoming quiet NaN
0x3f800000u, // finite against old signaling NaN
0x7faabcdeu, // incoming signaling NaN
0x00000000u, // +0.0 against old -0.0
0x80000000u, // -0.0 against old +0.0
0x80000000u, // -0.0 against a negative denorm
0x80000001u, // negative denorm against -0.0
0x00000001u, // positive denorm against +0.0
0x00000000u, // +0.0 against a positive denorm
};
std::vector<u32> code;
for (u32 i = 0; i < static_cast<u32>(std::size(values)); i++) {
AppendVMovU32(&code, 20, i * 4u);
AppendVMovLiteral(&code, i, values[i]);
AppendBufferStoreOpcode(&code, 0x40, i, 20, true);
}
for (u32 i = 0; i < static_cast<u32>(std::size(values)); i++) {
AppendStoreVgpr(&code, i, i + static_cast<u32>(std::size(values)));
}
AppendEnd(&code);
return {"BufferAtomicFMaxSpecialValues",
code,
{0x3f800000u, 0x40800000u, 0xbf800000u, 0x3f800000u, 0x7f800000u,
0xff800000u, 0x7fc12345u, 0x3f800000u, 0x7fa54321u, 0x3f800000u,
0x80000000u, 0x00000000u, 0x80000001u, 0x80000000u, 0x00000000u,
0x00000001u, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0x40000000u, 0x40800000u, 0x7f800000u, 0x3f800000u, 0x7f800000u,
0x3f800000u, 0x7fc12345u, 0x3f800000u, 0x7fa54321u, 0x3f800000u,
0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000001u,
0x00000001u, 0x3f800000u, 0x40800000u, 0xbf800000u, 0x3f800000u,
0x7f800000u, 0xff800000u, 0x7fc12345u, 0x3f800000u, 0x7fa54321u,
0x3f800000u, 0x80000000u, 0x00000000u, 0x80000001u, 0x80000000u,
0x00000000u, 0x00000001u},
{O::VMovB32, O::BufferAtomicFMax, O::BufferStoreDword, O::SEndpgm}};
}
TestCase BufferAtomicFMaxContendedWorkgroup() {
using O = ShaderOpcode;
std::vector<u32> code;
code.push_back(EncodeVop1(0x06, 1, Vgpr(0))); // v_cvt_f32_u32 v1, thread_id.x
AppendVMovU32(&code, 20, 0);
AppendBufferStoreOpcode(&code, 0x40, 1, 20);
AppendEnd(&code);
TestCase test;
test.name = "BufferAtomicFMaxContendedWorkgroup";
test.code = code;
test.initial = {0xc2c80000u}; // -100.0
test.expected = {0x427c0000u}; // max(-100.0, 0.0 .. 63.0)
test.opcodes = {O::VCvtF32U32, O::VMovB32, O::BufferAtomicFMax, O::SEndpgm};
test.compute_info.threads_num[0] = 64;
test.compute_info.threads_num[1] = 1;
test.compute_info.threads_num[2] = 1;
test.compute_info.thread_ids_num = 1;
test.has_compute_info = true;
return test;
}
std::vector<u32> MakeRgbaImage(u32 width, u32 height, u32 value = 0) {
return std::vector<u32>(static_cast<size_t>(width) * height * 4u, value);
}
@@ -14854,6 +15331,9 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorMoves);
AddCase(VectorVop3MoveAppliesFloatSourceModifiers);
AddCase(VectorIntegerOps);
AddCase(Vop2SdwaSubNcExactByte2Destination);
AddCase(Vop2SdwaSubNcPreservesByteAndWordDestinations);
AddCase(Vop2SdwaMinU32PreservesWordDestination);
AddCase(VectorShiftCountsMaskLowBits);
AddCase(VectorVop3IntegerOps);
AddCase(VectorBfeI32ArithmeticShiftMasksField);
@@ -14897,10 +15377,12 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorSpecialF32FlushesDenormalInputs);
AddCase(VectorSinCosMaxFiniteSpecialCases);
AddCase(VectorCompareOps);
AddCase(VectorVop3CompareEqI64OnGpu);
AddCase(VectorVop3CompareNeU64OnGpu);
AddCase(VectorCompareClassF32);
AddCase(VectorCompareF16Ops);
AddCase(Vop2SdwaCndmaskSourceModifier);
AddCase(Vop2SdwaCndmaskFullDestinationWithSubDwordSource);
AddCase(Vop3CndmaskUsesSgprMaskLaneBits);
AddCase(Vop3CndmaskAllowsDataSourceModifier);
AddCase(VectorCompareExecOps);
@@ -14985,6 +15467,12 @@ std::vector<TestCase> MakeCases() {
AddCase(DsSwizzleInvalidSourceLaneZero);
AddCase(BufferAtomicVariants);
AddCase(BufferAtomicGlc0DoesNotReturnOldValue);
AddCase(BufferAtomicFMinExactRawGlcModes);
AddCase(BufferAtomicFMinSpecialValues);
AddCase(BufferAtomicFMinContendedWorkgroup);
AddCase(BufferAtomicFMaxExactRawGlcModes);
AddCase(BufferAtomicFMaxSpecialValues);
AddCase(BufferAtomicFMaxContendedWorkgroup);
AddCase(ImageLoadVariants);
AddCase(ImageLoadR32UintUsesIntegerSampledImage);
AddCase(ImageLoad1DUsesScalarCoordinate);
@@ -15226,6 +15714,15 @@ void CheckEmbeddedFetchVertexOffset() {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
void CheckRenderTargetFormatContract() {
const auto r8_uint =
TextureGetRenderTargetFormat(Prospero::GpuEnumValue(Prospero::ChannelLayout::k8),
Prospero::GpuEnumValue(Prospero::ChannelType::kUInt),
Prospero::GpuEnumValue(Prospero::ChannelOrder::kStandard));
Require("RenderTargetFormat", "R8 uint",
r8_uint.format == vk::Format::eR8Uint && r8_uint.bytes_per_element == 1u &&
r8_uint.export_mapping.IsIdentity(),
"R8 uint render-target tuple was rejected");
const auto rgb565 = TextureGetRenderTargetFormat(16u, 0u, 0u);
Require("RenderTargetFormat", "RGB565 UNorm",
rgb565.format == vk::Format::eB5G6R5UnormPack16 && rgb565.bytes_per_element == 2u &&
@@ -16241,7 +16738,7 @@ ShaderTextureResource AtomicStorageTextureDescriptor() {
} else if (std::strcmp(kind, "type") == 0) {
descriptor.fields[3] = (descriptor.fields[3] & 0x0fffffffu) |
(Prospero::GpuEnumValue(Prospero::ImageType::kColor2D) << 28u);
} else if (std::strcmp(kind, "tile") == 0) {
} else if (std::strcmp(kind, "standard256b-volume") == 0) {
descriptor.fields[3] = (descriptor.fields[3] & ~(0x1fu << 20u)) |
(Prospero::GpuEnumValue(Prospero::TileMode::kStandard256B) << 20u);
} else if (std::strcmp(kind, "mip") == 0) {
@@ -16451,6 +16948,38 @@ void CheckBasicStorageTextureDescriptor() {
"PPSA21268 uint 2D-array storage descriptor fixture is malformed");
ValidateStorageTexture(BasicUintArrayStorageTextureResource(), uint_array, 0x10000);
const ShaderTextureResource standard256b {{
0x204e4900u,
0x43c00000u,
0x00004000u,
0x90100facu,
0x00000000u,
0x00700000u,
0x00000000u,
0x00000000u,
}};
auto standard256b_resource = BasicBgraStorageTextureResource();
standard256b_resource.kind = ShaderRecompiler::IR::ResourceKind::StorageImageUint;
const auto standard256b_pitch = TileGetTexturePitch(
standard256b.Format(), standard256b.Width5() + 1u, 1, standard256b.TileMode());
TileSizeAlign standard256b_size {};
TileGetTextureTotalSize(standard256b.Format(), standard256b.Width5() + 1u,
standard256b.Height5() + 1u, standard256b.Depth() + 1u,
standard256b_pitch, 1, standard256b.TileMode(), false,
standard256b_size);
Require("BasicStorageTexture", "Standard256B uint 2D descriptor",
standard256b.Width5() + 1u == 2 && standard256b.Height5() + 1u == 2 &&
standard256b.Depth() + 1u == 1 &&
standard256b.Type() == Prospero::GpuEnumValue(Prospero::ImageType::kColor2D) &&
standard256b.Format() ==
Prospero::GpuEnumValue(Prospero::BufferFormat::k8_8_8_8UInt) &&
standard256b.TileMode() ==
Prospero::GpuEnumValue(Prospero::TileMode::kStandard256B) &&
standard256b.DstSelXYZW() == DstSel(4, 5, 6, 7) &&
standard256b_size.size == 0x100 && standard256b_size.align == 0x100,
"captured PPSA08511 Standard256B storage descriptor is malformed");
ValidateStorageTexture(standard256b_resource, standard256b, standard256b_size.size);
const auto standard4kb_array = Standard4KBUintArrayStorageTextureDescriptor();
const auto standard4kb_pitch =
TileGetTexturePitch(standard4kb_array.Format(), 1, 1, standard4kb_array.TileMode());
@@ -16587,7 +17116,7 @@ void CheckBasicStorageTextureDescriptor() {
"GetModuleFileName failed");
for (const char* kind: {"resource",
"type",
"tile",
"standard256b-volume",
"mip",
"swizzle",
"linear-rgb1-read",
@@ -17491,6 +18020,136 @@ void CheckPm4StencilInfoValueLane(RenderContext& renderer) {
std::printf("[host] %-32s ok\n", "Pm4StencilInfoValueLane");
}
void CheckPm4ContextStateOperations(RenderContext& renderer) {
GraphicsInitJmpTables();
CommandProcessor processor(renderer);
struct AgcCommandBufferLayout {
using Callback = KYTY_SYSV_ABI bool (*)(Gen5::CommandBuffer*, uint32_t, void*);
uint32_t* bottom;
uint32_t* top;
uint32_t* cursor_up;
uint32_t* cursor_down;
Callback callback;
void* user_data;
uint32_t reserved_dw;
};
static_assert(offsetof(AgcCommandBufferLayout, cursor_up) == 0x10);
static_assert(offsetof(AgcCommandBufferLayout, reserved_dw) == 0x30);
HW::RenderControl original_control {};
original_control.depth_clear_enable = true;
original_control.stencil_clear_enable = true;
processor.GetCtx().SetRenderControl(original_control);
processor.GetCtx().SetRenderTargetMask(0x1234abcd);
processor.GetUcfg().SetPrimitiveType(0x35);
g_hw_sh_indirect_func[Pm4::SPI_SHADER_PGM_LO_PS](processor, Pm4::SPI_SHADER_PGM_LO_PS,
0x00123456);
const auto shader_address = processor.GetShCtx().GetPs().ps_regs.data_addr;
const auto invoke = [&](ContextStateOperation operation) {
std::array<uint32_t, 32> packet {};
std::array<uint32_t, 6> segment_sizes {};
AgcCommandBufferLayout dcb {packet.data(), packet.data() + packet.size(), packet.data(),
packet.data() + packet.size(), nullptr, nullptr, 0};
const auto operation_value = static_cast<uint32_t>(operation);
size_t segment_count = 0;
switch (operation) {
case ContextStateOperation::Clear: segment_sizes = {5}; segment_count = 1; break;
case ContextStateOperation::Push:
segment_sizes = {5, 8, 9, 3, 2};
segment_count = 5;
break;
case ContextStateOperation::Pop:
segment_sizes = {3, 5, 8, 9, 2};
segment_count = 5;
break;
case ContextStateOperation::PushClear:
segment_sizes = {5, 8, 9, 3, 2, 5};
segment_count = 6;
break;
}
const auto size_dw =
static_cast<size_t>(Gen5::GraphicsDcbContextStateOpGetSize(operation_value) / 4);
auto* emitted = Gen5::GraphicsDcbContextStateOp(
reinterpret_cast<Gen5::CommandBuffer*>(&dcb), operation_value);
bool packet_matches = true;
size_t segment_offset = 0;
for (size_t i = 0; i < segment_count; i++) {
const auto segment_size = segment_sizes[i];
const auto selector = (i == 0 ? Pm4::R_CONTEXT_STATE : Pm4::R_ZERO);
packet_matches &=
packet[segment_offset] == KYTY_PM4(segment_size, Pm4::IT_NOP, selector);
for (size_t j = 1; j < segment_size; j++) {
packet_matches &= packet[segment_offset + j] ==
(i == 0 && j == 1 ? operation_value : 0u);
}
segment_offset += segment_size;
}
Require("Pm4ContextState", "HLE packet",
emitted == packet.data() && dcb.cursor_up == packet.data() + size_dw &&
segment_offset == size_dw && packet_matches,
"context-state HLE packet does not match its real allocation size");
Pm4Execution execution;
return processor.Process(execution, packet.data(), size_dw);
};
Require("Pm4ContextState", "push-clear",
invoke(ContextStateOperation::PushClear) == Pm4ProcessResult::Complete &&
!processor.GetCtx().GetRenderControl().depth_clear_enable &&
!processor.GetCtx().GetRenderControl().stencil_clear_enable &&
processor.GetCtx().GetRenderTargetMask() == HW::Context {}.GetRenderTargetMask() &&
processor.GetUcfg().GetPrimType() == 0x35 &&
processor.GetShCtx().GetPs().ps_regs.data_addr == shader_address,
"push-clear did not save and clear only Cx state");
processor.GetCtx().SetRenderTargetMask(0x55aa55aa);
Require("Pm4ContextState", "pop",
invoke(ContextStateOperation::Pop) == Pm4ProcessResult::Complete &&
processor.GetCtx().GetRenderControl().depth_clear_enable &&
processor.GetCtx().GetRenderControl().stencil_clear_enable &&
processor.GetCtx().GetRenderTargetMask() == 0x1234abcd,
"pop did not restore the complete saved Cx state");
Require("Pm4ContextState", "push",
invoke(ContextStateOperation::Push) == Pm4ProcessResult::Complete &&
processor.GetCtx().GetRenderTargetMask() == 0x1234abcd,
"push changed live Cx state");
processor.GetCtx().SetRenderTargetMask(0x01020304);
Require("Pm4ContextState", "push-pop restore",
invoke(ContextStateOperation::Pop) == Pm4ProcessResult::Complete &&
processor.GetCtx().GetRenderTargetMask() == 0x1234abcd,
"ordinary push/pop did not restore Cx state");
Require("Pm4ContextState", "clear",
invoke(ContextStateOperation::Clear) == Pm4ProcessResult::Complete &&
!processor.GetCtx().GetRenderControl().depth_clear_enable &&
processor.GetCtx().GetRenderTargetMask() == HW::Context {}.GetRenderTargetMask() &&
processor.GetUcfg().GetPrimType() == 0x35 &&
processor.GetShCtx().GetPs().ps_regs.data_addr == shader_address,
"clear reset state outside the Cx register domain");
processor.GetCtx().SetRenderTargetMask(0xabcdef01);
Require("Pm4ContextState", "push before processor reset",
invoke(ContextStateOperation::Push) == Pm4ProcessResult::Complete,
"push before reset failed");
processor.Reset();
processor.GetCtx().SetRenderTargetMask(0x76543210);
Require("Pm4ContextState", "processor reset discards push",
invoke(ContextStateOperation::Push) == Pm4ProcessResult::Complete &&
invoke(ContextStateOperation::Pop) == Pm4ProcessResult::Complete &&
processor.GetCtx().GetRenderTargetMask() == 0x76543210,
"processor reset retained an invalid saved Cx state");
Require("Pm4ContextState", "HLE packet size",
Gen5::GraphicsDcbContextStateOpGetSize(0) == 20 &&
Gen5::GraphicsDcbContextStateOpGetSize(1) == 108 &&
Gen5::GraphicsDcbContextStateOpGetSize(2) == 108 &&
Gen5::GraphicsDcbContextStateOpGetSize(3) == 128 &&
Gen5::GraphicsDcbContextStateOpGetSize(4) == 0,
"context-state HLE sizes do not match libSceAgc");
std::printf("[host] %-32s ok\n", "Pm4ContextState");
}
void CheckPm4WaitResume(RenderContext& renderer) {
GraphicsInitJmpTables();
CommandProcessor processor(renderer);
@@ -17627,6 +18286,11 @@ int main(int argc, char** argv) {
vulkan.CheckGpuCommandLane();
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--context-state-only") == 0) {
VulkanHarness vulkan;
CheckPm4ContextStateOperations(vulkan.RuntimeRenderer());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--image-overlap-only") == 0) {
CheckDepthAttachmentWrites();
CheckDynamicRenderingState();
@@ -17761,6 +18425,7 @@ int main(int argc, char** argv) {
CheckVulkan13FeatureRequirements();
CheckPm4AcquireMemNoOp(vulkan.RuntimeRenderer());
CheckPm4StencilInfoValueLane(vulkan.RuntimeRenderer());
CheckPm4ContextStateOperations(vulkan.RuntimeRenderer());
CheckPm4WaitResume(vulkan.RuntimeRenderer());
CheckPm4CeCompletion(vulkan.RuntimeRenderer());
CheckEmbeddedFetchVertexOffset();
+269
View File
@@ -0,0 +1,269 @@
#include "kernel/syncOnAddress.h"
#include "libs/errno.h"
#include <atomic>
#include <chrono>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <thread>
#include <vector>
namespace {
using Libs::LibKernel::SyncOnAddress::Wait32;
using Libs::LibKernel::SyncOnAddress::Wait64;
using Libs::LibKernel::SyncOnAddress::Wake;
std::atomic<int> g_signal_poll_count{0};
void Check(bool value, const char *text) {
if (!value) {
std::fprintf(stderr, "SyncOnAddressTests: failed: %s\n", text);
std::abort();
}
}
template <typename T> void Store(T *address, T value) {
std::atomic_ref<T>(*address).store(value, std::memory_order_release);
}
void CountSignalPoll() {
g_signal_poll_count.fetch_add(1, std::memory_order_relaxed);
}
void TestInvalidAddress() {
uint32_t timeout = 1;
Check(Wait32(nullptr, 0, &timeout) == Libs::LibKernel::KERNEL_ERROR_EINVAL,
"wait32 rejects a null address");
Check(Wait64(nullptr, 0, &timeout) == Libs::LibKernel::KERNEL_ERROR_EINVAL,
"wait64 rejects a null address");
Check(Wake(nullptr, 1) == Libs::LibKernel::KERNEL_ERROR_EINVAL,
"wake rejects a null address");
alignas(uint64_t) uint8_t bytes[16] = {};
auto *misaligned = reinterpret_cast<uint32_t *>(bytes + 1);
Check(Wait32(misaligned, 0, &timeout) == Libs::LibKernel::KERNEL_ERROR_EINVAL,
"wait32 rejects a misaligned address");
Check(Wait64(reinterpret_cast<uint64_t *>(bytes + 4), 0, &timeout) ==
Libs::LibKernel::KERNEL_ERROR_EINVAL,
"wait64 rejects a misaligned address");
Check(Wake(misaligned, 1) == Libs::LibKernel::KERNEL_ERROR_EINVAL,
"wake rejects a misaligned address");
uint64_t aligned = 0;
Check(Wake(&aligned, -1) == Libs::LibKernel::KERNEL_ERROR_EINVAL,
"wake rejects a negative count");
}
void TestMismatchReturnsImmediately() {
uint32_t word = 7;
uint64_t word64 = UINT64_C(0x100000000);
uint32_t timeout = 500000;
const auto start = std::chrono::steady_clock::now();
Check(Wait32(&word, 6, &timeout, CountSignalPoll) == OK,
"mismatch succeeds without parking");
Check(Wait64(&word64, 0, &timeout, CountSignalPoll) == OK,
"wait64 compares all 64 bits");
Check(std::chrono::steady_clock::now() - start <
std::chrono::milliseconds(100),
"mismatched value is a fast path");
Check(g_signal_poll_count.load(std::memory_order_relaxed) >= 2,
"mismatch remains a guest signal safe-point");
}
void TestTimeout() {
uint32_t word = 0;
uint32_t timeout = 20000;
const auto start = std::chrono::steady_clock::now();
Check(Wait32(&word, 0, &timeout) == Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT,
"matching value times out");
const auto elapsed = std::chrono::steady_clock::now() - start;
Check(elapsed >= std::chrono::milliseconds(10),
"timeout does not return too early");
Check(elapsed < std::chrono::milliseconds(500), "timeout remains bounded");
timeout = 0;
Check(Wait32(&word, 0, &timeout) == Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT,
"zero timeout polls a matching value");
word = 1;
Check(Wait32(&word, 0, &timeout) == OK,
"zero timeout succeeds for a mismatched value");
}
void TestValueChangeAndWake() {
uint64_t word = 0;
uint32_t timeout = 1000000;
std::atomic<bool> ready{false};
int result = Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
std::thread waiter([&] {
ready.store(true, std::memory_order_release);
result = Wait64(&word, 0, &timeout);
});
while (!ready.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
Store(&word, UINT64_C(0x100000000));
Check(Wake(&word, 1) == OK, "wake-one succeeds");
waiter.join();
Check(result == OK, "a value change plus wake releases the waiter");
}
void TestWakeOneThenAll() {
constexpr int WAITER_COUNT = 4;
uint32_t word = 0;
uint32_t timeout = 1000000;
std::atomic<int> ready{0};
std::atomic<int> returned{0};
int results[WAITER_COUNT] = {};
std::vector<std::thread> waiters;
for (int i = 0; i < WAITER_COUNT; i++) {
waiters.emplace_back([&, i] {
ready.fetch_add(1, std::memory_order_release);
results[i] = Wait32(&word, 0, &timeout);
returned.fetch_add(1, std::memory_order_release);
});
}
while (ready.load(std::memory_order_acquire) != WAITER_COUNT) {
std::this_thread::yield();
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Check(Wake(&word, 1) == OK, "wake-one succeeds with multiple waiters");
const auto one_deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
while (returned.load(std::memory_order_acquire) == 0 &&
std::chrono::steady_clock::now() < one_deadline) {
std::this_thread::yield();
}
Check(returned.load(std::memory_order_acquire) == 1,
"wake-one releases exactly one waiter");
Check(Wake(&word, 2) == OK, "wake-two succeeds");
const auto two_deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
while (returned.load(std::memory_order_acquire) < 3 &&
std::chrono::steady_clock::now() < two_deadline) {
std::this_thread::yield();
}
Check(returned.load(std::memory_order_acquire) == 3,
"wake-two releases exactly two more waiters");
Check(Wake(&word, INT_MAX) == OK, "wake-all succeeds");
for (auto &waiter : waiters) {
waiter.join();
}
Check(returned.load(std::memory_order_acquire) == WAITER_COUNT,
"wake-all releases the remaining waiters");
for (int result : results) {
Check(result == OK, "explicitly woken waiters return success");
}
}
void TestAddressesAreIsolated() {
uint32_t first = 0;
uint32_t second = 0;
uint32_t first_timeout = 1000000;
uint32_t second_timeout = 1000000;
std::atomic<int> ready{0};
std::atomic<bool> first_returned{false};
std::atomic<bool> second_returned{false};
int first_result = 0;
int second_result = 0;
std::thread first_waiter([&] {
ready.fetch_add(1, std::memory_order_release);
first_result = Wait32(&first, 0, &first_timeout);
first_returned.store(true, std::memory_order_release);
});
std::thread second_waiter([&] {
ready.fetch_add(1, std::memory_order_release);
second_result = Wait32(&second, 0, &second_timeout);
second_returned.store(true, std::memory_order_release);
});
while (ready.load(std::memory_order_acquire) != 2) {
std::this_thread::yield();
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Check(Wake(&first, 1) == OK, "first address wakes");
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Check(first_returned.load(std::memory_order_acquire),
"first address waiter returned");
Check(!second_returned.load(std::memory_order_acquire),
"waking one address does not release another address");
Check(Wake(&second, 1) == OK, "second address wakes");
first_waiter.join();
second_waiter.join();
Check(first_result == OK && second_result == OK,
"isolated waiters return success");
}
void TestCompareRegisterWakeRace() {
for (int i = 0; i < 100; i++) {
uint32_t word = 0;
uint32_t timeout = 500000;
std::atomic<bool> ready{false};
int result = Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
std::thread waiter([&] {
ready.store(true, std::memory_order_release);
result = Wait32(&word, 0, &timeout);
});
while (!ready.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
Store(&word, uint32_t{1});
(void)Wake(&word, 1);
waiter.join();
Check(result == OK, "compare/register/wake race never loses progress");
}
}
void TestWakeZeroIsNoOp() {
constexpr int WAITER_COUNT = 2;
uint32_t word = 0;
uint32_t timeout = 1000000;
std::atomic<int> ready{0};
std::atomic<int> returned{0};
int results[WAITER_COUNT] = {};
std::vector<std::thread> waiters;
for (int i = 0; i < WAITER_COUNT; i++) {
waiters.emplace_back([&, i] {
ready.fetch_add(1, std::memory_order_release);
results[i] = Wait32(&word, 0, &timeout);
returned.fetch_add(1, std::memory_order_release);
});
}
while (ready.load(std::memory_order_acquire) != WAITER_COUNT) {
std::this_thread::yield();
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Check(Wake(&word, 0) == OK, "zero-count wake succeeds");
std::this_thread::sleep_for(std::chrono::milliseconds(20));
Check(returned.load(std::memory_order_acquire) == 0,
"zero-count wake releases no waiters");
Check(Wake(&word, INT_MAX) == OK, "wake-all succeeds after zero-count wake");
for (auto &waiter : waiters) {
waiter.join();
}
for (int result : results) {
Check(result == OK, "wake-all releases waiters after zero-count no-op");
}
}
} // namespace
int main() {
TestInvalidAddress();
TestMismatchReturnsImmediately();
TestTimeout();
TestValueChangeAndWake();
TestWakeOneThenAll();
TestAddressesAreIsolated();
TestCompareRegisterWakeRace();
TestWakeZeroIsNoOp();
std::printf("SyncOnAddressTests: all passed\n");
return 0;
}
+120
View File
@@ -623,6 +623,7 @@ ImageTestUserData(Prospero::ImageType type = Prospero::ImageType::kColor2D) {
std::array<uint32_t, 64> data {};
for (uint32_t start = 0; start + 3u < data.size(); start += 4u) {
data[start] = 0x1000u + start * 0x100u;
data[start + 1u] = Prospero::GpuEnumValue(Prospero::BufferFormat::k8UNorm) << 20u;
data[start + 2u] = UINT32_MAX;
data[start + 3u] = Prospero::GpuEnumValue(type) << 28u;
}
@@ -1169,6 +1170,8 @@ void TestNewShaderRecompilerMoreAluFamilies() {
EncodeVop2Sdwa(5, 6, 0, 4, 5),
0x025e6af9u,
0x16060635u, // v_cndmask_b32 v47, v53, -v53
0x020e02f9u,
0x06040600u, // v_cndmask_b32 v7, v0.lo, v1; full-width destination
0x100490f9u,
0x86860600u, // v_mul_f32 v2, s0, s72 (SDWA full)
EncodeVop2(0x35, 9, 249, 1),
@@ -1395,6 +1398,9 @@ void TestNewShaderRecompilerMoreAluFamilies() {
Check(Common::ContainsStr(result.decoded_dump, "v_cndmask_b32 v47, v53,") &&
Common::ContainsStr(result.decoded_dump, "v53.neg"),
"new decoder did not decode V_CNDMASK_B32 SDWA source modifier");
Check(Common::ContainsStr(result.decoded_dump, "v_cndmask_b32 v7, v0.sdwa(sel=4") &&
Common::ContainsStr(result.decoded_dump, "v1"),
"new decoder did not decode full-destination V_CNDMASK_B32 with SDWA source");
Check(Common::ContainsStr(result.decoded_dump, "v_add_f32 v125, v5.dpp"),
"new decoder did not decode VOP2 DPP source metadata");
Check(!Common::ContainsStr(result.decoded_dump, "VOP2 SDWA/DPP modifiers are not implemented"),
@@ -1699,6 +1705,8 @@ void TestNewShaderRecompilerMoreAluFamilies() {
Check(Common::ContainsStr(result.ir_dump, "SelectMaskF32Bits v47, vcc_lo, v53.neg, v53") &&
Common::ContainsStr(result.ir_dump, "v53.neg"),
"V_CNDMASK_B32 SDWA source modifier did not lower to float-bit select IR");
Check(Common::ContainsStr(result.ir_dump, "SelectMaskU32 v7, vcc_lo, v1, v0.sdwa(sel=4"),
"full-destination V_CNDMASK_B32 with SDWA source did not lower to integer select IR");
Check(Common::ContainsStr(result.ir_dump, "FAddF32 v125.dpp") &&
Common::ContainsStr(result.ir_dump, "v5.dpp"),
"VOP2 DPP source/destination did not lower to IR metadata");
@@ -2416,6 +2424,10 @@ void TestNewShaderRecompilerBootB16PackedAndSdwaOpcodes() {
0x0c860688u, // v_add_nc_u32 v5, 8, sign-extended v4.lo
0x4a0c0cf9u,
0x0d860688u, // v_add_nc_u32 v6, 8, sign-extended v6.hi
0x4c1616f9u,
0x0686128du, // v_sub_nc_u32 v11.byte2, 13, v11; preserve other destination bytes
0x261418f9u,
0x0686149fu, // v_min_u32 v10.word0, 31, v12; preserve upper destination word
0xbf810000u,
};
@@ -2464,6 +2476,10 @@ void TestNewShaderRecompilerBootB16PackedAndSdwaOpcodes() {
Check(Common::ContainsStr(result.decoded_dump, "v_add_nc_u32 v6") &&
Common::ContainsStr(result.decoded_dump, "v6.sdwa(sel=5,sext=1"),
"new decoder did not decode V_ADD_NC_U32 SDWA sign-extended high word");
Check(Common::ContainsStr(result.decoded_dump, "v_sub_nc_u32 v11.sdwa(sel=2"),
"new decoder did not decode V_SUB_NC_U32 SDWA byte-2 destination");
Check(Common::ContainsStr(result.decoded_dump, "v_min_u32 v10.sdwa(sel=4"),
"new decoder did not decode V_MIN_U32 SDWA low-word destination");
Check(!Common::ContainsStr(result.decoded_dump, "unsupported family=VOP2 opcode=0x00"),
"literal/SDWA extension words were decoded as phantom VOP2 instructions");
Check(!Common::ContainsStr(result.decoded_dump,
@@ -2497,6 +2513,10 @@ void TestNewShaderRecompilerBootB16PackedAndSdwaOpcodes() {
Check(Common::ContainsStr(result.ir_dump, "IAddU32 v6") &&
Common::ContainsStr(result.ir_dump, "v6.sdwa(sel=5,sext=1"),
"V_ADD_NC_U32 SDWA sign-extended high word did not lower to IR");
Check(Common::ContainsStr(result.ir_dump, "ISubU32 v11.sdwa(sel=2"),
"V_SUB_NC_U32 SDWA byte-2 destination did not lower to IR");
Check(Common::ContainsStr(result.ir_dump, "UMinU32 v10.sdwa(sel=4"),
"V_MIN_U32 SDWA low-word destination did not lower to IR");
Check(SpirvContainsOpcode(result.spirv, 128),
"SPIR-V binary does not contain OpIAdd for U16 operations");
Check(SpirvContainsOpcode(result.spirv, 202),
@@ -2895,6 +2915,61 @@ void CheckNewDecoderUnsupported(const uint32_t* shader, uint32_t words, const ch
"unsupported lowering error was not explicit");
}
void TestNewShaderRecompilerRejectsDppOn64BitCompares() {
const uint32_t opcodes[] = {0xa2u, 0xe5u}; // v_cmp_eq_i64, v_cmp_ne_u64
for (const auto opcode: opcodes) {
const uint32_t shader[] = {
EncodeVopc(opcode, 250u, 0u), // DPP escape in SRC0
EncodeVop2Dpp(0u),
0xbf810000u,
};
ShaderRecompiler::Decoder::Program program;
std::string error;
Check(ShaderRecompiler::Decoder::DecodeProgram(shader, program, &error), error.c_str());
Check(program.instructions.size() == 2u,
"64-bit VOPC DPP decode did not consume its modifier word");
const auto& compare = program.instructions.front();
Check(compare.opcode == ShaderRecompiler::Decoder::Opcode::Unsupported,
"64-bit VOPC illegally accepted a DPP modifier");
Check(Common::ContainsStr(compare.unsupported_reason,
"VOPC DPP modifier is not supported for opcode"),
"64-bit VOPC DPP rejection reason was not explicit");
}
}
void TestNewShaderRecompilerIrLookupMissFailsExplicitly() {
ShaderRecompiler::Decoder::Program decoded;
ShaderRecompiler::Decoder::Instruction missing;
missing.pc = 0u;
missing.family = ShaderRecompiler::Decoder::Family::VOP1;
missing.opcode = ShaderRecompiler::Decoder::Opcode::Unknown;
decoded.instructions.push_back(missing);
ShaderRecompiler::CFG::Graph cfg;
ShaderRecompiler::CFG::BasicBlock block;
block.inst_end = 1u;
cfg.blocks.push_back(block);
cfg.entry_block = 0u;
ShaderRecompiler::IR::Program ir;
std::string error;
Check(!ShaderRecompiler::IR::LowerProgram(decoded, cfg, ShaderType::Compute, 64u, ir, &error),
"missing decoder-to-IR mapping unexpectedly lowered as an instruction");
Check(Common::ContainsStr(error, "no IR lowering"),
"missing decoder-to-IR mapping did not report an explicit error");
Check(ir.blocks.empty(), "missing decoder-to-IR mapping emitted a fallback IR block");
decoded.instructions.front().family = ShaderRecompiler::Decoder::Family::MUBUF;
decoded.instructions.front().opcode = ShaderRecompiler::Decoder::Opcode::VAddNcU32;
error.clear();
Check(!ShaderRecompiler::IR::LowerProgram(decoded, cfg, ShaderType::Compute, 64u, ir, &error),
"memory-family opcode bypassed specialized memory lowering");
Check(Common::ContainsStr(error, "memory-family opcode") &&
Common::ContainsStr(error, "specialized IR lowering"),
"memory-family lowering bypass did not report an explicit error");
}
void TestNewShaderRecompilerMemoryFamilyLowering() {
const uint32_t shader[] = {
EncodeSmem0(0x00, 0, 4),
@@ -4722,6 +4797,10 @@ void TestNewShaderRecompilerAtomicLowering() {
EncodeMubuf1(5, 0, 1), // buffer_atomic_or
EncodeMubuf0(0x3b, 40, true, true),
EncodeMubuf1(6, 0, 1), // buffer_atomic_xor
0xe0fc0000u,
0x80010000u, // exact buffer_atomic_fmin v0, s[4:7], 0 (GLC=0)
0xe100000cu,
0x80010300u, // exact failing buffer_atomic_fmax v3, s[4:7], 12 (GLC=0)
EncodeDs0(0x00),
EncodeDs1(0, 2, 1), // ds_add_u32
EncodeDs0(0x01),
@@ -4776,6 +4855,10 @@ void TestNewShaderRecompilerAtomicLowering() {
"new decoder did not decode buffer atomic signed max");
Check(Common::ContainsStr(result.decoded_dump, "buffer_atomic_xor"),
"new decoder did not decode buffer atomic xor");
Check(Common::ContainsStr(result.decoded_dump, "buffer_atomic_fmin"),
"new decoder did not decode buffer atomic float min");
Check(Common::ContainsStr(result.decoded_dump, "buffer_atomic_fmax"),
"new decoder did not decode buffer atomic float max");
Check(Common::ContainsStr(result.decoded_dump, "ds_add_u32"),
"new decoder did not decode DS atomic add");
Check(Common::ContainsStr(result.decoded_dump, "ds_sub_u32"),
@@ -4808,6 +4891,10 @@ void TestNewShaderRecompilerAtomicLowering() {
"buffer atomic or did not lower to IR");
Check(Common::ContainsStr(result.ir_dump, "AtomicXorU32 v6"),
"buffer atomic xor did not lower to IR");
Check(Common::ContainsStr(result.ir_dump, "AtomicFMinF32 null, v0"),
"buffer atomic float min did not lower to IR without a GLC return");
Check(Common::ContainsStr(result.ir_dump, "AtomicFMaxF32 null, v3"),
"buffer atomic float max did not lower to IR without a GLC return");
Check(Common::ContainsStr(result.ir_dump, "AtomicAddU32 null, v2"),
"DS no-return atomic add did not lower to IR");
Check(Common::ContainsStr(result.ir_dump, "AtomicSubU32 null, v10"),
@@ -4851,6 +4938,8 @@ void TestNewShaderRecompilerAtomicLowering() {
Check(SpirvContainsOpcode(result.spirv, 240), "SPIR-V binary does not contain OpAtomicAnd");
Check(SpirvContainsOpcode(result.spirv, 241), "SPIR-V binary does not contain OpAtomicOr");
Check(SpirvContainsOpcode(result.spirv, 242), "SPIR-V binary does not contain OpAtomicXor");
Check(SpirvContainsOpcode(result.spirv, 230),
"SPIR-V binary does not contain OpAtomicCompareExchange for float min");
Check(SpirvContainsOpcode(result.spirv, 225),
"buffer atomic SPIR-V binary does not contain OpMemoryBarrier");
CheckSpirvBinaryValidates(result.spirv);
@@ -6638,6 +6727,35 @@ void TestNewShaderRecompilerNativeBindingPlan() {
rejected_spirv == rejected_before && rejected_error.find("atomic") != std::string::npos,
"malformed atomic instruction reached a fatal descriptor path");
auto malformed_float_atomic = result.program;
atomic_inst = FindBufferInstruction(&malformed_float_atomic);
Check(atomic_inst != nullptr, "native validation fixture lacks a float atomic candidate");
atomic_inst->op = ShaderRecompiler::IR::Opcode::AtomicFMaxF32;
atomic_inst->memory.kind = ShaderRecompiler::IR::ResourceKind::Lds;
atomic_inst->memory.resource = 0;
atomic_inst->src_count = 2;
rejected_spirv = rejected_before;
rejected_error.clear();
Check(!ShaderRecompiler::Spirv::EmitProgram(malformed_float_atomic, result.resources, nullptr,
nullptr, nullptr, rejected_spirv,
&rejected_error) &&
rejected_spirv == rejected_before &&
rejected_error.find("floating-point atomic") != std::string::npos,
"buffer-only floating-point atomic accepted a non-buffer resource");
auto invalid_ir_opcode = result.program;
Check(!invalid_ir_opcode.blocks.empty() && !invalid_ir_opcode.blocks.front().instructions.empty(),
"native validation fixture lacks an invalid-opcode candidate");
invalid_ir_opcode.blocks.front().instructions.front().op =
ShaderRecompiler::IR::Opcode::Count;
rejected_spirv = rejected_before;
rejected_error.clear();
Check(!ShaderRecompiler::Spirv::EmitProgram(invalid_ir_opcode, result.resources, nullptr, nullptr,
nullptr, rejected_spirv, &rejected_error) &&
rejected_spirv == rejected_before &&
rejected_error.find("opcode is out of range") != std::string::npos,
"out-of-range IR opcode was not rejected transactionally before emission");
auto overwide_atomic = result.program;
atomic_inst = FindBufferInstruction(&overwide_atomic);
Check(atomic_inst != nullptr, "native validation fixture lacks an overwide atomic candidate");
@@ -7417,6 +7535,8 @@ int main() {
TestNewShaderRecompilerScalarVectorAlu();
TestNewShaderRecompilerVop3LaneReadDestinationEncoding();
TestNewShaderRecompilerMoreAluFamilies();
TestNewShaderRecompilerRejectsDppOn64BitCompares();
TestNewShaderRecompilerIrLookupMissFailsExplicitly();
TestNewShaderRecompilerExpandedAluBatch();
TestNewShaderRecompilerVop3pPackedF16();
TestNewShaderRecompilerStagedShaderOps();