From 57193e794488e788c52a4df71089a17a6d79ca4f Mon Sep 17 00:00:00 2001 From: SpikeHD Date: Sat, 9 Mar 2024 03:45:38 -0800 Subject: [PATCH 01/60] Stub ALL getNsDataIdList functions (#452) * fix: stub ALL getNsDataIdList functions * fix: spaces -> tabs --- src/core/services/boss.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/services/boss.cpp b/src/core/services/boss.cpp index a8f7194b1..cb8fdf818 100644 --- a/src/core/services/boss.cpp +++ b/src/core/services/boss.cpp @@ -15,6 +15,8 @@ namespace BOSSCommands { GetTaskIdList = 0x000E0000, GetNsDataIdList = 0x00100102, GetNsDataIdList1 = 0x00110102, + GetNsDataIdList2 = 0x00120102, + GetNsDataIdList3 = 0x00130102, SendProperty = 0x00140082, ReceiveProperty = 0x00160082, GetTaskServiceStatus = 0x001B0042, @@ -40,7 +42,9 @@ void BOSSService::handleSyncRequest(u32 messagePointer) { case BOSSCommands::GetErrorCode: getErrorCode(messagePointer); break; case BOSSCommands::GetNewArrivalFlag: getNewArrivalFlag(messagePointer); break; case BOSSCommands::GetNsDataIdList: - case BOSSCommands::GetNsDataIdList1: + case BOSSCommands::GetNsDataIdList1: + case BOSSCommands::GetNsDataIdList2: + case BOSSCommands::GetNsDataIdList3: getNsDataIdList(messagePointer, command); break; case BOSSCommands::GetOptoutFlag: getOptoutFlag(messagePointer); break; case BOSSCommands::GetStorageEntryInfo: getStorageEntryInfo(messagePointer); break; From 929019e76bf5477ce5b7f71e7c2cb196d17868bd Mon Sep 17 00:00:00 2001 From: Wunk Date: Mon, 11 Mar 2024 10:51:17 -0700 Subject: [PATCH 02/60] Refactor build targets into `AlberCore` (#455) * Ignore `.cache` folder * Add `AlberCore` build-target Separate the AlberCore from its frontends. Allowing two front-ends to interface with the same core implementation. This also allows for the core to interface better with unit-testing. * Modularize SDL/Qt frontend Separates all QT/SDL build files and options into the frontend-build-target * Fix optional OpenGL enablement Software renderer requires OpenGL, so AlberCore requries OpenGL. The QT frontend currently requires OpenGL due to `ScreenWidget` * Fix Android build * Fix LTO linking * Fix windows build `LoadLibrary` is a preprocessor that will use either `LoadLibraryW` or `LoadLibraryA` depending on if `UNICODE` is defined or not. In this case we are using an ASCII string literal and and can explicitly specify the usage of `LoadLibraryA` with an ASCII literal. * Bonk * Bonk --------- Co-authored-by: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> --- .gitignore | 2 +- CMakeLists.txt | 295 ++++++++++++------------ third_party/duckstation/window_info.cpp | 2 +- 3 files changed, 155 insertions(+), 144 deletions(-) diff --git a/.gitignore b/.gitignore index 7214ef50b..786db9129 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,7 @@ build/ .vs/ .vscode/*.log - +.cache/ ipch/ *.aps *.ncb diff --git a/CMakeLists.txt b/CMakeLists.txt index 4dbe438f2..d8a69acb8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,11 +72,9 @@ endif() if(ENABLE_QT_GUI) find_package(Qt6 REQUIRED COMPONENTS Widgets) - - # We can't use qt_standard_project_setup since it's Qt 6.3+ and we don't need to set the minimum that high - set(CMAKE_AUTOMOC ON) - set(CMAKE_AUTORCC ON) - set(CMAKE_AUTOUIC ON) + if(NOT ENABLE_OPENGL) + message(FATAL_ERROR "Qt frontend requires OpenGL") + endif() endif() set(SDL_STATIC ON CACHE BOOL "" FORCE) @@ -153,9 +151,9 @@ endif() add_subdirectory(third_party/teakra EXCLUDE_FROM_ALL) set(SOURCE_FILES src/emulator.cpp src/io_file.cpp src/config.cpp - src/core/CPU/cpu_dynarmic.cpp src/core/CPU/dynarmic_cycles.cpp - src/core/memory.cpp src/renderer.cpp src/core/renderer_null/renderer_null.cpp - src/http_server.cpp src/stb_image_write.c src/core/cheats.cpp src/core/action_replay.cpp + src/core/CPU/cpu_dynarmic.cpp src/core/CPU/dynarmic_cycles.cpp + src/core/memory.cpp src/renderer.cpp src/core/renderer_null/renderer_null.cpp + src/http_server.cpp src/stb_image_write.c src/core/cheats.cpp src/core/action_replay.cpp src/discord_rpc.cpp src/lua.cpp src/memory_mapped_file.cpp src/miniaudio.cpp ) set(CRYPTO_SOURCE_FILES src/core/crypto/aes_engine.cpp) @@ -199,26 +197,24 @@ set(AUDIO_SOURCE_FILES src/core/audio/dsp_core.cpp src/core/audio/null_core.cpp set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) # Frontend source files -if(NOT ANDROID) - if(ENABLE_QT_GUI) - set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp - src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp - ) - set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp - include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp - ) +if(ENABLE_QT_GUI) + set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp + src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp + ) + set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp + include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp + ) - source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) - source_group("Header Files\\Qt" FILES ${FRONTEND_HEADER_FILES}) - include_directories(${Qt6Gui_PRIVATE_INCLUDE_DIRS}) + source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) + source_group("Header Files\\Qt" FILES ${FRONTEND_HEADER_FILES}) + include_directories(${Qt6Gui_PRIVATE_INCLUDE_DIRS}) - include_directories(third_party/zep/include) # Include zep for text editor usage - configure_file(third_party/zep/cmake/config_app.h.cmake ${CMAKE_BINARY_DIR}/zep_config/config_app.h) - include_directories(${CMAKE_BINARY_DIR}/zep_config) - else() - set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp) - set(FRONTEND_HEADER_FILES "") - endif() + include_directories(third_party/zep/include) # Include zep for text editor usage + configure_file(third_party/zep/cmake/config_app.h.cmake ${CMAKE_BINARY_DIR}/zep_config/config_app.h) + include_directories(${CMAKE_BINARY_DIR}/zep_config) +else() + set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp) + set(FRONTEND_HEADER_FILES "") endif() set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp @@ -257,10 +253,10 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp ) cmrc_add_resource_library( - resources_console_fonts - NAMESPACE ConsoleFonts - WHENCE "src/core/services/fonts/" - "src/core/services/fonts/CitraSharedFontUSRelocated.bin" + resources_console_fonts + NAMESPACE ConsoleFonts + WHENCE "src/core/services/fonts/" + "src/core/services/fonts/CitraSharedFontUSRelocated.bin" ) set(THIRD_PARTY_SOURCE_FILES third_party/imgui/imgui.cpp @@ -315,62 +311,62 @@ set(RENDERER_VK_SOURCE_FILES "") # Empty by default unless we are compiling with if(ENABLE_OPENGL) # This may look weird but opengl.hpp is our header even if it's in the third_party folder - set(RENDERER_GL_INCLUDE_FILES third_party/opengl/opengl.hpp - include/renderer_gl/renderer_gl.hpp include/renderer_gl/textures.hpp - include/renderer_gl/surfaces.hpp include/renderer_gl/surface_cache.hpp - include/renderer_gl/gl_state.hpp - ) + set(RENDERER_GL_INCLUDE_FILES third_party/opengl/opengl.hpp + include/renderer_gl/renderer_gl.hpp include/renderer_gl/textures.hpp + include/renderer_gl/surfaces.hpp include/renderer_gl/surface_cache.hpp + include/renderer_gl/gl_state.hpp + ) - set(RENDERER_GL_SOURCE_FILES src/core/renderer_gl/renderer_gl.cpp + set(RENDERER_GL_SOURCE_FILES src/core/renderer_gl/renderer_gl.cpp src/core/renderer_gl/textures.cpp src/core/renderer_gl/etc1.cpp src/core/renderer_gl/gl_state.cpp src/host_shaders/opengl_display.frag src/host_shaders/opengl_display.vert src/host_shaders/opengl_vertex_shader.vert src/host_shaders/opengl_fragment_shader.frag - ) + ) set(HEADER_FILES ${HEADER_FILES} ${RENDERER_GL_INCLUDE_FILES}) source_group("Source Files\\Core\\OpenGL Renderer" FILES ${RENDERER_GL_SOURCE_FILES}) - cmrc_add_resource_library( - resources_renderer_gl - NAMESPACE RendererGL - WHENCE "src/host_shaders/" - "src/host_shaders/opengl_display.frag" - "src/host_shaders/opengl_display.vert" - "src/host_shaders/opengl_vertex_shader.vert" - "src/host_shaders/opengl_fragment_shader.frag" - ) + cmrc_add_resource_library( + resources_renderer_gl + NAMESPACE RendererGL + WHENCE "src/host_shaders/" + "src/host_shaders/opengl_display.frag" + "src/host_shaders/opengl_display.vert" + "src/host_shaders/opengl_vertex_shader.vert" + "src/host_shaders/opengl_fragment_shader.frag" + ) endif() if(ENABLE_VULKAN) - find_package( - Vulkan 1.3.206 REQUIRED - COMPONENTS glslangValidator - ) - - set(RENDERER_VK_INCLUDE_FILES include/renderer_vk/renderer_vk.hpp - include/renderer_vk/vk_api.hpp include/renderer_vk/vk_debug.hpp - include/renderer_vk/vk_descriptor_heap.hpp - include/renderer_vk/vk_descriptor_update_batch.hpp - include/renderer_vk/vk_sampler_cache.hpp - include/renderer_vk/vk_memory.hpp include/renderer_vk/vk_pica.hpp - ) - - set(RENDERER_VK_SOURCE_FILES src/core/renderer_vk/renderer_vk.cpp - src/core/renderer_vk/vk_api.cpp src/core/renderer_vk/vk_debug.cpp - src/core/renderer_vk/vk_descriptor_heap.cpp - src/core/renderer_vk/vk_descriptor_update_batch.cpp - src/core/renderer_vk/vk_sampler_cache.cpp - src/core/renderer_vk/vk_memory.cpp src/core/renderer_vk/vk_pica.cpp - ) + find_package( + Vulkan 1.3.206 REQUIRED + COMPONENTS glslangValidator + ) + + set(RENDERER_VK_INCLUDE_FILES include/renderer_vk/renderer_vk.hpp + include/renderer_vk/vk_api.hpp include/renderer_vk/vk_debug.hpp + include/renderer_vk/vk_descriptor_heap.hpp + include/renderer_vk/vk_descriptor_update_batch.hpp + include/renderer_vk/vk_sampler_cache.hpp + include/renderer_vk/vk_memory.hpp include/renderer_vk/vk_pica.hpp + ) + + set(RENDERER_VK_SOURCE_FILES src/core/renderer_vk/renderer_vk.cpp + src/core/renderer_vk/vk_api.cpp src/core/renderer_vk/vk_debug.cpp + src/core/renderer_vk/vk_descriptor_heap.cpp + src/core/renderer_vk/vk_descriptor_update_batch.cpp + src/core/renderer_vk/vk_sampler_cache.cpp + src/core/renderer_vk/vk_memory.cpp src/core/renderer_vk/vk_pica.cpp + ) set(HEADER_FILES ${HEADER_FILES} ${RENDERER_VK_INCLUDE_FILES}) source_group("Source Files\\Core\\Vulkan Renderer" FILES ${RENDERER_VK_SOURCE_FILES}) - set(RENDERER_VK_HOST_SHADERS_SOURCE - "src/host_shaders/vulkan_display.frag" - "src/host_shaders/vulkan_display.vert" - ) + set(RENDERER_VK_HOST_SHADERS_SOURCE + "src/host_shaders/vulkan_display.frag" + "src/host_shaders/vulkan_display.vert" + ) set(RENDERER_VK_HOST_SHADERS_FLAGS -e main --target-env vulkan1.1) @@ -383,30 +379,34 @@ if(ENABLE_VULKAN) # Compile each vulkan shader into an .spv file foreach( HOST_SHADER_SOURCE ${RENDERER_VK_HOST_SHADERS_SOURCE} ) - get_filename_component( FILE_NAME ${HOST_SHADER_SOURCE} NAME ) - set( HOST_SHADER_SPIRV "${PROJECT_BINARY_DIR}/host_shaders/${FILE_NAME}.spv" ) - add_custom_command( - OUTPUT ${HOST_SHADER_SPIRV} - COMMAND ${CMAKE_COMMAND} -E make_directory "${PROJECT_BINARY_DIR}/host_shaders/" - COMMAND Vulkan::glslangValidator ${RENDERER_VK_HOST_SHADERS_FLAGS} -V "${PROJECT_SOURCE_DIR}/${HOST_SHADER_SOURCE}" -o ${HOST_SHADER_SPIRV} - DEPENDS ${HOST_SHADER_SOURCE} - ) - list( APPEND RENDERER_VK_HOST_SHADERS_SPIRV ${HOST_SHADER_SPIRV} ) + get_filename_component( FILE_NAME ${HOST_SHADER_SOURCE} NAME ) + set( HOST_SHADER_SPIRV "${PROJECT_BINARY_DIR}/host_shaders/${FILE_NAME}.spv" ) + add_custom_command( + OUTPUT ${HOST_SHADER_SPIRV} + COMMAND ${CMAKE_COMMAND} -E make_directory "${PROJECT_BINARY_DIR}/host_shaders/" + COMMAND Vulkan::glslangValidator ${RENDERER_VK_HOST_SHADERS_FLAGS} -V "${PROJECT_SOURCE_DIR}/${HOST_SHADER_SOURCE}" -o ${HOST_SHADER_SPIRV} + DEPENDS ${HOST_SHADER_SOURCE} + ) + list( APPEND RENDERER_VK_HOST_SHADERS_SPIRV ${HOST_SHADER_SPIRV} ) endforeach() - cmrc_add_resource_library( - resources_renderer_vk - NAMESPACE RendererVK - WHENCE "${PROJECT_BINARY_DIR}/host_shaders/" - ${RENDERER_VK_HOST_SHADERS_SPIRV} - ) + cmrc_add_resource_library( + resources_renderer_vk + NAMESPACE RendererVK + WHENCE "${PROJECT_BINARY_DIR}/host_shaders/" + ${RENDERER_VK_HOST_SHADERS_SPIRV} + ) endif() source_group("Header Files\\Core" FILES ${HEADER_FILES}) -set(ALL_SOURCES ${SOURCE_FILES} ${FRONTEND_SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERNEL_SOURCE_FILES} +set(ALL_SOURCES ${SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERNEL_SOURCE_FILES} ${LOADER_SOURCE_FILES} ${SERVICE_SOURCE_FILES} ${APPLET_SOURCE_FILES} ${RENDERER_SW_SOURCE_FILES} ${PICA_SOURCE_FILES} ${THIRD_PARTY_SOURCE_FILES} ${AUDIO_SOURCE_FILES} ${HEADER_FILES} ${FRONTEND_HEADER_FILES}) +if(ANDROID) + set(ALL_SOURCES ${ALL_SOURCES} src/jni_driver.cpp) +endif() + if(ENABLE_OPENGL) # Add the OpenGL source files to ALL_SOURCES set(ALL_SOURCES ${ALL_SOURCES} ${RENDERER_GL_SOURCE_FILES}) @@ -417,95 +417,106 @@ if(ENABLE_VULKAN) set(ALL_SOURCES ${ALL_SOURCES} ${RENDERER_VK_SOURCE_FILES}) endif() -if(ANDROID) - set(ALL_SOURCES ${ALL_SOURCES} src/jni_driver.cpp) -endif() - -if(BUILD_HYDRA_CORE) - include_directories(third_party/hydra_core/include) - add_library(Alber SHARED ${ALL_SOURCES} src/hydra_core.cpp) - target_compile_definitions(Alber PRIVATE PANDA3DS_HYDRA_CORE=1) -else() - add_executable(Alber ${ALL_SOURCES}) -endif() +add_library(AlberCore STATIC ${ALL_SOURCES}) if(ANDROID) - target_link_libraries(Alber PRIVATE EGL log) + target_link_libraries(AlberCore PRIVATE EGL log) endif() -if(ENABLE_LTO OR ENABLE_USER_BUILD) - set_target_properties(Alber PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE) -endif() - -target_link_libraries(Alber PRIVATE dynarmic cryptopp glad resources_console_fonts teakra) +target_link_libraries(AlberCore PRIVATE dynarmic cryptopp glad resources_console_fonts teakra) +target_link_libraries(AlberCore PUBLIC glad) if(NOT ANDROID) - target_link_libraries(Alber PRIVATE SDL2-static) + target_link_libraries(AlberCore PUBLIC SDL2-static) endif() if(ENABLE_DISCORD_RPC AND NOT ANDROID) - target_compile_definitions(Alber PUBLIC "PANDA3DS_ENABLE_DISCORD_RPC=1") - target_link_libraries(Alber PRIVATE discord-rpc) + target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_DISCORD_RPC=1") + target_link_libraries(AlberCore PRIVATE discord-rpc) endif() if(ENABLE_LUAJIT) - target_compile_definitions(Alber PUBLIC "PANDA3DS_ENABLE_LUA=1") - target_link_libraries(Alber PRIVATE libluajit) + target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_LUA=1") + target_link_libraries(AlberCore PRIVATE libluajit) # If we're not on Android, link libuv too if (NOT ANDROID) - target_link_libraries(Alber PRIVATE uv_a) + target_link_libraries(AlberCore PRIVATE uv_a) endif() endif() if(ENABLE_OPENGL) - target_compile_definitions(Alber PUBLIC "PANDA3DS_ENABLE_OPENGL=1") - target_link_libraries(Alber PRIVATE resources_renderer_gl) + target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_OPENGL=1") + target_link_libraries(AlberCore PRIVATE resources_renderer_gl) endif() if(ENABLE_VULKAN) - target_compile_definitions(Alber PUBLIC "PANDA3DS_ENABLE_VULKAN=1") - target_link_libraries(Alber PRIVATE Vulkan::Vulkan resources_renderer_vk) + target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_VULKAN=1") + target_link_libraries(AlberCore PRIVATE Vulkan::Vulkan resources_renderer_vk) endif() -if(ENABLE_QT_GUI) - target_compile_definitions(Alber PUBLIC "PANDA3DS_FRONTEND_QT=1") - target_compile_definitions(Alber PUBLIC "ZEP_QT=1") - target_compile_definitions(Alber PUBLIC "ZEP_FEATURE_CPP_FILE_SYSTEM=1") +if(GPU_DEBUG_INFO) + target_compile_definitions(AlberCore PRIVATE GPU_DEBUG_INFO=1) +endif() - target_link_libraries(Alber PRIVATE Qt6::Widgets) +if(ENABLE_USER_BUILD) + target_compile_definitions(AlberCore PRIVATE PANDA3DS_USER_BUILD=1) +endif() - if(LINUX OR FREEBSD) - find_package(X11 REQUIRED) - target_link_libraries(Alber PRIVATE ${X11_LIBRARIES}) +if(ENABLE_USER_BUILD OR DISABLE_PANIC_DEV) + target_compile_definitions(AlberCore PRIVATE PANDA3DS_LIMITED_PANICS=1) +endif() - if(ENABLE_OPENGL) - find_package(OpenGL REQUIRED COMPONENTS OpenGL EGL GLX) - target_link_libraries(Alber PRIVATE OpenGL::OpenGL OpenGL::EGL OpenGL::GLX) - endif() - endif() +if(ENABLE_HTTP_SERVER) + target_compile_definitions(AlberCore PRIVATE PANDA3DS_ENABLE_HTTP_SERVER=1) +endif() - qt_add_resources(Alber "app_images" - PREFIX "/" - FILES - docs/img/rsob_icon.png docs/img/rstarstruck_icon.png - ) +# Configure frontend + +if(ENABLE_QT_GUI) + target_compile_definitions(AlberCore PUBLIC "PANDA3DS_FRONTEND_QT=1") else() - target_compile_definitions(Alber PUBLIC "PANDA3DS_FRONTEND_SDL=1") + target_compile_definitions(AlberCore PUBLIC "PANDA3DS_FRONTEND_SDL=1") endif() -if(GPU_DEBUG_INFO) - target_compile_definitions(Alber PRIVATE GPU_DEBUG_INFO=1) -endif() +if(NOT BUILD_HYDRA_CORE) + add_executable(Alber ${FRONTEND_SOURCE_FILES} ${FRONTEND_HEADER_FILES}) + target_link_libraries(Alber PRIVATE AlberCore) -if(ENABLE_USER_BUILD) - target_compile_definitions(Alber PRIVATE PANDA3DS_USER_BUILD=1) -endif() + if(ENABLE_QT_GUI) + target_compile_definitions(Alber PUBLIC "ZEP_QT=1") + target_compile_definitions(Alber PUBLIC "ZEP_FEATURE_CPP_FILE_SYSTEM=1") + target_link_libraries(Alber PRIVATE Qt6::Widgets) + + # We can't use qt_standard_project_setup since it's Qt 6.3+ and we don't need to set the minimum that high + set_target_properties(Alber PROPERTIES AUTOMOC ON) + set_target_properties(Alber PROPERTIES AUTORCC ON) + set_target_properties(Alber PROPERTIES AUTOUIC ON) + + if(LINUX OR FREEBSD) + find_package(X11 REQUIRED) + target_link_libraries(Alber PRIVATE ${X11_LIBRARIES}) + + if(ENABLE_OPENGL) + find_package(OpenGL REQUIRED COMPONENTS OpenGL EGL GLX) + target_link_libraries(Alber PRIVATE OpenGL::OpenGL OpenGL::EGL OpenGL::GLX) + endif() + endif() -if(ENABLE_USER_BUILD OR DISABLE_PANIC_DEV) - target_compile_definitions(Alber PRIVATE PANDA3DS_LIMITED_PANICS=1) + qt_add_resources(AlberCore "app_images" + PREFIX "/" + FILES + docs/img/rsob_icon.png docs/img/rstarstruck_icon.png + ) + else() + endif() +elseif(BUILD_HYDRA_CORE) + target_compile_definitions(AlberCore PRIVATE PANDA3DS_HYDRA_CORE=1) + include_directories(third_party/hydra_core/include) + add_library(Alber SHARED src/hydra_core.cpp) + target_link_libraries(Alber PUBLIC AlberCore) endif() -if(ENABLE_HTTP_SERVER) - target_compile_definitions(Alber PRIVATE PANDA3DS_ENABLE_HTTP_SERVER=1) +if(ENABLE_LTO OR ENABLE_USER_BUILD) + set_target_properties(Alber PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE) endif() diff --git a/third_party/duckstation/window_info.cpp b/third_party/duckstation/window_info.cpp index 5f131826d..4e56ab0cd 100644 --- a/third_party/duckstation/window_info.cpp +++ b/third_party/duckstation/window_info.cpp @@ -16,7 +16,7 @@ static bool GetRefreshRateFromDWM(HWND hwnd, float* refresh_rate) if (!load_tried) { load_tried = true; - dwm_module = LoadLibrary(L"dwmapi.dll"); + dwm_module = LoadLibraryA("dwmapi.dll"); if (dwm_module) { std::atexit([]() { From 18df0664639a905a70a8ab4a5d771d5f8d3224d1 Mon Sep 17 00:00:00 2001 From: Wunk Date: Mon, 11 Mar 2024 15:53:49 -0700 Subject: [PATCH 03/60] Add shader unit-testing (#457) * Initialize catch-2 based unit tests * Add nihstro submodule Enabled only during testing to help with assembling shaders in-code. * Implement `ADD` instruction unit-test * Add arithmetic/logical instruction unit tests * Add embedded catch2 submodule Will use the host catch2 if available. --- .gitmodules | 6 ++ CMakeLists.txt | 26 +++++ tests/dynapica.cpp | 229 ++++++++++++++++++++++++++++++++++++++++++++ third_party/Catch2 | 1 + third_party/nihstro | 1 + 5 files changed, 263 insertions(+) create mode 100644 tests/dynapica.cpp create mode 160000 third_party/Catch2 create mode 160000 third_party/nihstro diff --git a/.gitmodules b/.gitmodules index 6c69fe140..259800549 100644 --- a/.gitmodules +++ b/.gitmodules @@ -61,3 +61,9 @@ [submodule "third_party/dynarmic"] path = third_party/dynarmic url = https://github.com/Panda3DS-emu/dynarmic +[submodule "third_party/nihstro"] + path = third_party/nihstro + url = https://github.com/neobrain/nihstro.git +[submodule "third_party/Catch2"] + path = third_party/Catch2 + url = https://github.com/catchorg/Catch2.git diff --git a/CMakeLists.txt b/CMakeLists.txt index d8a69acb8..629601abf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,7 @@ option(GPU_DEBUG_INFO "Enable additional GPU debugging info" OFF) option(ENABLE_OPENGL "Enable OpenGL rendering backend" ON) option(ENABLE_VULKAN "Enable Vulkan rendering backend" ON) option(ENABLE_LTO "Enable link-time optimization" OFF) +option(ENABLE_TESTS "Compile unit-tests" OFF) option(ENABLE_USER_BUILD "Make a user-facing build. These builds have various assertions disabled, LTO, and more" OFF) option(ENABLE_HTTP_SERVER "Enable HTTP server. Used for Discord bot support" OFF) option(ENABLE_DISCORD_RPC "Compile with Discord RPC support (disabled by default)" ON) @@ -520,3 +521,28 @@ endif() if(ENABLE_LTO OR ENABLE_USER_BUILD) set_target_properties(Alber PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE) endif() + +if(ENABLE_TESTS) + enable_testing() + + find_package(Catch2 3) + if(NOT Catch2_FOUND) + add_subdirectory(third_party/Catch2) + endif() + + add_library(nihstro-headers INTERFACE) + target_include_directories(nihstro-headers SYSTEM INTERFACE ./third_party/nihstro/include) + + add_executable(AlberTests + tests/dynapica.cpp + ) + target_link_libraries( + AlberTests + PRIVATE + Catch2::Catch2WithMain + AlberCore + nihstro-headers + ) + + add_test(AlberTests AlberTests) +endif() \ No newline at end of file diff --git a/tests/dynapica.cpp b/tests/dynapica.cpp new file mode 100644 index 000000000..32e7bdb09 --- /dev/null +++ b/tests/dynapica.cpp @@ -0,0 +1,229 @@ +#include + +#include +#include +#include +#include +#include +#include + +using namespace Floats; +static const nihstro::SourceRegister input0 = nihstro::SourceRegister::MakeInput(0); +static const nihstro::SourceRegister input1 = nihstro::SourceRegister::MakeInput(1); +static const nihstro::DestRegister output0 = nihstro::DestRegister::MakeOutput(0); + +static std::unique_ptr assembleVertexShader(std::initializer_list code) { + const auto shaderBinary = nihstro::InlineAsm::CompileToRawBinary(code); + auto newShader = std::make_unique(ShaderType::Vertex); + newShader->reset(); + + for (const nihstro::Instruction& instruction : shaderBinary.program) { + newShader->uploadWord(instruction.hex); + } + for (const nihstro::SwizzlePattern& swizzle : shaderBinary.swizzle_table) { + newShader->uploadDescriptor(swizzle.hex); + } + newShader->finalize(); + return newShader; +} + +class VertexShaderTest { + private: + std::unique_ptr shader; + + public: + explicit VertexShaderTest(std::initializer_list code) : shader(assembleVertexShader(code)) {} + + // Multiple inputs, singular scalar output + float runScalar(std::initializer_list inputs) { + usize inputIndex = 0; + for (const float& input : inputs) { + const std::array input_vec = std::array{f24::fromFloat32(input), f24::zero(), f24::zero(), f24::zero()}; + shader->inputs[inputIndex++] = input_vec; + } + shader->run(); + return shader->outputs[0][0]; + } + + static std::unique_ptr assembleTest(std::initializer_list code) { + return std::make_unique(code); + } +}; + +TEST_CASE("ADD", "[shader][vertex]") { + const auto shader = VertexShaderTest::assembleTest({ + {nihstro::OpCode::Id::ADD, output0, input0, input1}, + {nihstro::OpCode::Id::END}, + }); + + REQUIRE(shader->runScalar({+1.0f, -1.0f}) == +0.0f); + REQUIRE(shader->runScalar({+0.0f, -0.0f}) == -0.0f); + REQUIRE(std::isnan(shader->runScalar({+INFINITY, -INFINITY}))); + REQUIRE(std::isinf(shader->runScalar({INFINITY, +1.0f}))); + REQUIRE(std::isinf(shader->runScalar({INFINITY, -1.0f}))); +} + +TEST_CASE("MUL", "[shader][vertex]") { + const auto shader = VertexShaderTest::assembleTest({ + {nihstro::OpCode::Id::MUL, output0, input0, input1}, + {nihstro::OpCode::Id::END}, + }); + + REQUIRE(shader->runScalar({+1.0f, -1.0f}) == -1.0f); + REQUIRE(shader->runScalar({-1.0f, +1.0f}) == -1.0f); + REQUIRE(shader->runScalar({INFINITY, 0.0f}) == 0.0f); + REQUIRE(shader->runScalar({+INFINITY, +INFINITY}) == INFINITY); + REQUIRE(shader->runScalar({+INFINITY, -INFINITY}) == -INFINITY); + REQUIRE(std::isnan(shader->runScalar({NAN, 0.0f}))); +} + +TEST_CASE("RCP", "[shader][vertex]") { + const auto shader = VertexShaderTest::assembleTest({ + {nihstro::OpCode::Id::RCP, output0, input0}, + {nihstro::OpCode::Id::END}, + }); + + // REQUIRE(shader->RunScalar({-0.0f}) == INFINITY); // Violates IEEE + REQUIRE(shader->runScalar({0.0f}) == INFINITY); + REQUIRE(shader->runScalar({INFINITY}) == 0.0f); + REQUIRE(std::isnan(shader->runScalar({NAN}))); + + REQUIRE(shader->runScalar({16.0f}) == Catch::Approx(0.0625f).margin(0.001f)); + REQUIRE(shader->runScalar({8.0f}) == Catch::Approx(0.125f).margin(0.001f)); + REQUIRE(shader->runScalar({4.0f}) == Catch::Approx(0.25f).margin(0.001f)); + REQUIRE(shader->runScalar({2.0f}) == Catch::Approx(0.5f).margin(0.001f)); + REQUIRE(shader->runScalar({1.0f}) == Catch::Approx(1.0f).margin(0.001f)); + REQUIRE(shader->runScalar({0.5f}) == Catch::Approx(2.0f).margin(0.001f)); + REQUIRE(shader->runScalar({0.25f}) == Catch::Approx(4.0f).margin(0.001f)); + REQUIRE(shader->runScalar({0.125f}) == Catch::Approx(8.0f).margin(0.002f)); + REQUIRE(shader->runScalar({0.0625f}) == Catch::Approx(16.0f).margin(0.004f)); +} + +TEST_CASE("RSQ", "[shader][vertex]") { + const auto shader = VertexShaderTest::assembleTest({ + {nihstro::OpCode::Id::RSQ, output0, input0}, + {nihstro::OpCode::Id::END}, + }); + + REQUIRE(shader->runScalar({-0.0f}) == INFINITY); + REQUIRE(shader->runScalar({INFINITY}) == 0.0f); + REQUIRE(std::isnan(shader->runScalar({-2.0f}))); + REQUIRE(std::isnan(shader->runScalar({-INFINITY}))); + REQUIRE(std::isnan(shader->runScalar({NAN}))); + REQUIRE(shader->runScalar({16.0f}) == Catch::Approx(0.25f).margin(0.001f)); + REQUIRE(shader->runScalar({8.0f}) == Catch::Approx(1.0f / std::sqrt(8.0f)).margin(0.001f)); + REQUIRE(shader->runScalar({4.0f}) == Catch::Approx(0.5f).margin(0.001f)); + REQUIRE(shader->runScalar({2.0f}) == Catch::Approx(1.0f / std::sqrt(2.0f)).margin(0.001f)); + REQUIRE(shader->runScalar({1.0f}) == Catch::Approx(1.0f).margin(0.001f)); + REQUIRE(shader->runScalar({0.5f}) == Catch::Approx(1.0f / std::sqrt(0.5f)).margin(0.001f)); + REQUIRE(shader->runScalar({0.25f}) == Catch::Approx(2.0f).margin(0.001f)); + REQUIRE(shader->runScalar({0.125f}) == Catch::Approx(1.0 / std::sqrt(0.125)).margin(0.002f)); + REQUIRE(shader->runScalar({0.0625f}) == Catch::Approx(4.0f).margin(0.004f)); +} + +TEST_CASE("LG2", "[shader][vertex]") { + const auto shader = VertexShaderTest::assembleTest({ + {nihstro::OpCode::Id::LG2, output0, input0}, + {nihstro::OpCode::Id::END}, + }); + + REQUIRE(std::isnan(shader->runScalar({NAN}))); + REQUIRE(std::isnan(shader->runScalar({-1.f}))); + REQUIRE(std::isinf(shader->runScalar({0.f}))); + REQUIRE(shader->runScalar({4.f}) == Catch::Approx(2.f)); + REQUIRE(shader->runScalar({64.f}) == Catch::Approx(6.f)); + REQUIRE(shader->runScalar({1.e24f}) == Catch::Approx(79.7262742773f)); +} + +TEST_CASE("EX2", "[shader][vertex]") { + const auto shader = VertexShaderTest::assembleTest({ + {nihstro::OpCode::Id::EX2, output0, input0}, + {nihstro::OpCode::Id::END}, + }); + + REQUIRE(std::isnan(shader->runScalar({NAN}))); + REQUIRE(shader->runScalar({-800.f}) == Catch::Approx(0.f)); + REQUIRE(shader->runScalar({0.f}) == Catch::Approx(1.f)); + REQUIRE(shader->runScalar({2.f}) == Catch::Approx(4.f)); + REQUIRE(shader->runScalar({6.f}) == Catch::Approx(64.f)); + REQUIRE(shader->runScalar({79.7262742773f}) == Catch::Approx(1.e24f)); + REQUIRE(std::isinf(shader->runScalar({800.f}))); +} + +TEST_CASE("MAX", "[shader][vertex]") { + const auto shader = VertexShaderTest::assembleTest({ + {nihstro::OpCode::Id::MAX, output0, input0, input1}, + {nihstro::OpCode::Id::END}, + }); + + REQUIRE(shader->runScalar({1.0f, 0.0f}) == 1.0f); + REQUIRE(shader->runScalar({0.0f, 1.0f}) == 1.0f); + REQUIRE(shader->runScalar({0.0f, +INFINITY}) == +INFINITY); + REQUIRE(shader->runScalar({0.0f, -INFINITY}) == -INFINITY); + REQUIRE(shader->runScalar({NAN, 0.0f}) == 0.0f); + REQUIRE(shader->runScalar({-INFINITY, +INFINITY}) == +INFINITY); + REQUIRE(std::isnan(shader->runScalar({0.0f, NAN}))); +} + +TEST_CASE("MIN", "[shader][vertex]") { + const auto shader = VertexShaderTest::assembleTest({ + {nihstro::OpCode::Id::MIN, output0, input0, input1}, + {nihstro::OpCode::Id::END}, + }); + + REQUIRE(shader->runScalar({1.0f, 0.0f}) == 0.0f); + REQUIRE(shader->runScalar({0.0f, 1.0f}) == 0.0f); + REQUIRE(shader->runScalar({0.0f, +INFINITY}) == 0.0f); + REQUIRE(shader->runScalar({0.0f, -INFINITY}) == -INFINITY); + REQUIRE(shader->runScalar({NAN, 0.0f}) == 0.0f); + REQUIRE(shader->runScalar({-INFINITY, +INFINITY}) == -INFINITY); + REQUIRE(std::isnan(shader->runScalar({0.0f, NAN}))); +} + +TEST_CASE("SGE", "[shader][vertex]") { + const auto shader = VertexShaderTest::assembleTest({ + {nihstro::OpCode::Id::SGE, output0, input0, input1}, + {nihstro::OpCode::Id::END}, + }); + + REQUIRE(shader->runScalar({INFINITY, 0.0f}) == 1.0f); + REQUIRE(shader->runScalar({0.0f, INFINITY}) == 0.0f); + REQUIRE(shader->runScalar({NAN, 0.0f}) == 0.0f); + REQUIRE(shader->runScalar({0.0f, NAN}) == 0.0f); + REQUIRE(shader->runScalar({+INFINITY, +INFINITY}) == 1.0f); + REQUIRE(shader->runScalar({+INFINITY, -INFINITY}) == 1.0f); + REQUIRE(shader->runScalar({-INFINITY, +INFINITY}) == 0.0f); + REQUIRE(shader->runScalar({+1.0f, -1.0f}) == 1.0f); + REQUIRE(shader->runScalar({-1.0f, +1.0f}) == 0.0f); +} + +TEST_CASE("SLT", "[shader][vertex]") { + const auto shader = VertexShaderTest::assembleTest({ + {nihstro::OpCode::Id::SLT, output0, input0, input1}, + {nihstro::OpCode::Id::END}, + }); + + REQUIRE(shader->runScalar({INFINITY, 0.0f}) == 0.0f); + REQUIRE(shader->runScalar({0.0f, INFINITY}) == 1.0f); + REQUIRE(shader->runScalar({NAN, 0.0f}) == 0.0f); + REQUIRE(shader->runScalar({0.0f, NAN}) == 0.0f); + REQUIRE(shader->runScalar({+INFINITY, +INFINITY}) == 0.0f); + REQUIRE(shader->runScalar({+INFINITY, -INFINITY}) == 0.0f); + REQUIRE(shader->runScalar({-INFINITY, +INFINITY}) == 1.0f); + REQUIRE(shader->runScalar({+1.0f, -1.0f}) == 0.0f); + REQUIRE(shader->runScalar({-1.0f, +1.0f}) == 1.0f); +} + +TEST_CASE("FLR", "[shader][vertex]") { + const auto shader = VertexShaderTest::assembleTest({ + {nihstro::OpCode::Id::FLR, output0, input0}, + {nihstro::OpCode::Id::END}, + }); + + REQUIRE(shader->runScalar({0.5}) == 0.0f); + REQUIRE(shader->runScalar({-0.5}) == -1.0f); + REQUIRE(shader->runScalar({1.5}) == 1.0f); + REQUIRE(shader->runScalar({-1.5}) == -2.0f); + REQUIRE(std::isnan(shader->runScalar({NAN}))); + REQUIRE(std::isinf(shader->runScalar({INFINITY}))); +} \ No newline at end of file diff --git a/third_party/Catch2 b/third_party/Catch2 new file mode 160000 index 000000000..4acc51828 --- /dev/null +++ b/third_party/Catch2 @@ -0,0 +1 @@ +Subproject commit 4acc51828f7f93f3b2058a63f54d112af4034503 diff --git a/third_party/nihstro b/third_party/nihstro new file mode 160000 index 000000000..e924e21b1 --- /dev/null +++ b/third_party/nihstro @@ -0,0 +1 @@ +Subproject commit e924e21b1da60170f0f0a4e5a073cb7d579969c0 From fe9939689d2af5dd9032301e5328b265d7c642cc Mon Sep 17 00:00:00 2001 From: Wunk Date: Mon, 11 Mar 2024 17:29:58 -0700 Subject: [PATCH 04/60] Add shader-jit unit-tests (#458) * Rename `dynapica` TU to `shader` These unit-tests in particular only actually test the shader-interpreter and not any of the JITs. * Conditionally test the shader-jit In the case that the host supports the shader-jit, the interpreter and the shader-jit will both be tested with the same unit-tests. Allowing for even more coverage. * Remove weird git submodule --- CMakeLists.txt | 2 +- tests/{dynapica.cpp => shader.cpp} | 84 ++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 28 deletions(-) rename tests/{dynapica.cpp => shader.cpp} (75%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 629601abf..fe9a8584b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -534,7 +534,7 @@ if(ENABLE_TESTS) target_include_directories(nihstro-headers SYSTEM INTERFACE ./third_party/nihstro/include) add_executable(AlberTests - tests/dynapica.cpp + tests/shader.cpp ) target_link_libraries( AlberTests diff --git a/tests/dynapica.cpp b/tests/shader.cpp similarity index 75% rename from tests/dynapica.cpp rename to tests/shader.cpp index 32e7bdb09..2116549dc 100644 --- a/tests/dynapica.cpp +++ b/tests/shader.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -27,12 +28,12 @@ static std::unique_ptr assembleVertexShader(std::initializer_list shader; + std::unique_ptr shader = {}; public: - explicit VertexShaderTest(std::initializer_list code) : shader(assembleVertexShader(code)) {} + explicit ShaderInterpreterTest(std::initializer_list code) : shader(assembleVertexShader(code)) {} // Multiple inputs, singular scalar output float runScalar(std::initializer_list inputs) { @@ -45,13 +46,42 @@ class VertexShaderTest { return shader->outputs[0][0]; } - static std::unique_ptr assembleTest(std::initializer_list code) { - return std::make_unique(code); + static std::unique_ptr assembleTest(std::initializer_list code) { + return std::make_unique(code); } }; -TEST_CASE("ADD", "[shader][vertex]") { - const auto shader = VertexShaderTest::assembleTest({ +#if defined(PANDA3DS_SHADER_JIT_SUPPORTED) +class ShaderJITTest final { + private: + std::unique_ptr shader = {}; + ShaderJIT shaderJit = {}; + + public: + explicit ShaderJITTest(std::initializer_list code) : shader(assembleVertexShader(code)) { shaderJit.prepare(*shader.get()); } + + // Multiple inputs, singular scalar output + float runScalar(std::initializer_list inputs) { + usize inputIndex = 0; + for (const float& input : inputs) { + const std::array input_vec = std::array{f24::fromFloat32(input), f24::zero(), f24::zero(), f24::zero()}; + shader->inputs[inputIndex++] = input_vec; + } + shaderJit.run(*shader.get()); + return shader->outputs[0][0]; + } + + static std::unique_ptr assembleTest(std::initializer_list code) { + return std::make_unique(code); + } +}; +#define SHADER_TEST_CASE(NAME, TAG) TEMPLATE_TEST_CASE(NAME, TAG, ShaderInterpreterTest, ShaderJITTest) +#else +#define SHADER_TEST_CASE(NAME, TAG) TEMPLATE_TEST_CASE(NAME, TAG, ShaderInterpreterTest) +#endif + +SHADER_TEST_CASE("ADD", "[shader][vertex]") { + const auto shader = TestType::assembleTest({ {nihstro::OpCode::Id::ADD, output0, input0, input1}, {nihstro::OpCode::Id::END}, }); @@ -63,8 +93,8 @@ TEST_CASE("ADD", "[shader][vertex]") { REQUIRE(std::isinf(shader->runScalar({INFINITY, -1.0f}))); } -TEST_CASE("MUL", "[shader][vertex]") { - const auto shader = VertexShaderTest::assembleTest({ +SHADER_TEST_CASE("MUL", "[shader][vertex]") { + const auto shader = TestType::assembleTest({ {nihstro::OpCode::Id::MUL, output0, input0, input1}, {nihstro::OpCode::Id::END}, }); @@ -77,8 +107,8 @@ TEST_CASE("MUL", "[shader][vertex]") { REQUIRE(std::isnan(shader->runScalar({NAN, 0.0f}))); } -TEST_CASE("RCP", "[shader][vertex]") { - const auto shader = VertexShaderTest::assembleTest({ +SHADER_TEST_CASE("RCP", "[shader][vertex]") { + const auto shader = TestType::assembleTest({ {nihstro::OpCode::Id::RCP, output0, input0}, {nihstro::OpCode::Id::END}, }); @@ -99,8 +129,8 @@ TEST_CASE("RCP", "[shader][vertex]") { REQUIRE(shader->runScalar({0.0625f}) == Catch::Approx(16.0f).margin(0.004f)); } -TEST_CASE("RSQ", "[shader][vertex]") { - const auto shader = VertexShaderTest::assembleTest({ +SHADER_TEST_CASE("RSQ", "[shader][vertex]") { + const auto shader = TestType::assembleTest({ {nihstro::OpCode::Id::RSQ, output0, input0}, {nihstro::OpCode::Id::END}, }); @@ -121,8 +151,8 @@ TEST_CASE("RSQ", "[shader][vertex]") { REQUIRE(shader->runScalar({0.0625f}) == Catch::Approx(4.0f).margin(0.004f)); } -TEST_CASE("LG2", "[shader][vertex]") { - const auto shader = VertexShaderTest::assembleTest({ +SHADER_TEST_CASE("LG2", "[shader][vertex]") { + const auto shader = TestType::assembleTest({ {nihstro::OpCode::Id::LG2, output0, input0}, {nihstro::OpCode::Id::END}, }); @@ -135,8 +165,8 @@ TEST_CASE("LG2", "[shader][vertex]") { REQUIRE(shader->runScalar({1.e24f}) == Catch::Approx(79.7262742773f)); } -TEST_CASE("EX2", "[shader][vertex]") { - const auto shader = VertexShaderTest::assembleTest({ +SHADER_TEST_CASE("EX2", "[shader][vertex]") { + const auto shader = TestType::assembleTest({ {nihstro::OpCode::Id::EX2, output0, input0}, {nihstro::OpCode::Id::END}, }); @@ -150,8 +180,8 @@ TEST_CASE("EX2", "[shader][vertex]") { REQUIRE(std::isinf(shader->runScalar({800.f}))); } -TEST_CASE("MAX", "[shader][vertex]") { - const auto shader = VertexShaderTest::assembleTest({ +SHADER_TEST_CASE("MAX", "[shader][vertex]") { + const auto shader = TestType::assembleTest({ {nihstro::OpCode::Id::MAX, output0, input0, input1}, {nihstro::OpCode::Id::END}, }); @@ -165,8 +195,8 @@ TEST_CASE("MAX", "[shader][vertex]") { REQUIRE(std::isnan(shader->runScalar({0.0f, NAN}))); } -TEST_CASE("MIN", "[shader][vertex]") { - const auto shader = VertexShaderTest::assembleTest({ +SHADER_TEST_CASE("MIN", "[shader][vertex]") { + const auto shader = TestType::assembleTest({ {nihstro::OpCode::Id::MIN, output0, input0, input1}, {nihstro::OpCode::Id::END}, }); @@ -180,8 +210,8 @@ TEST_CASE("MIN", "[shader][vertex]") { REQUIRE(std::isnan(shader->runScalar({0.0f, NAN}))); } -TEST_CASE("SGE", "[shader][vertex]") { - const auto shader = VertexShaderTest::assembleTest({ +SHADER_TEST_CASE("SGE", "[shader][vertex]") { + const auto shader = TestType::assembleTest({ {nihstro::OpCode::Id::SGE, output0, input0, input1}, {nihstro::OpCode::Id::END}, }); @@ -197,8 +227,8 @@ TEST_CASE("SGE", "[shader][vertex]") { REQUIRE(shader->runScalar({-1.0f, +1.0f}) == 0.0f); } -TEST_CASE("SLT", "[shader][vertex]") { - const auto shader = VertexShaderTest::assembleTest({ +SHADER_TEST_CASE("SLT", "[shader][vertex]") { + const auto shader = TestType::assembleTest({ {nihstro::OpCode::Id::SLT, output0, input0, input1}, {nihstro::OpCode::Id::END}, }); @@ -214,8 +244,8 @@ TEST_CASE("SLT", "[shader][vertex]") { REQUIRE(shader->runScalar({-1.0f, +1.0f}) == 1.0f); } -TEST_CASE("FLR", "[shader][vertex]") { - const auto shader = VertexShaderTest::assembleTest({ +SHADER_TEST_CASE("FLR", "[shader][vertex]") { + const auto shader = TestType::assembleTest({ {nihstro::OpCode::Id::FLR, output0, input0}, {nihstro::OpCode::Id::END}, }); From c89fe05b8ae10794b4df51b840e1d2988c68e500 Mon Sep 17 00:00:00 2001 From: Wunk Date: Mon, 11 Mar 2024 23:34:02 -0700 Subject: [PATCH 05/60] Fix shader-interpreter non-IEEE outputs (#459) * Re-enable non-IEEE shader test * Fix shader-interpreter RCP/RSQ output Handle the `-0.0` special-case * Fix shader-interpreter MIN/MAX output Takes advantage of min/max's properties regarding non-finites to return NaN depending on its input position: ``` max(NaN, 2.f) -> NaN max(2.f, NaN) -> 2 min(NaN, 2.f) -> NaN min(2.f, NaN) -> 2 ``` * Fix shader-interpreter FLR indexing bug `3 - 1` should be `3 - i` --- src/core/PICA/shader_interpreter.cpp | 30 +++++++++++++++++++++------- tests/shader.cpp | 2 +- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/core/PICA/shader_interpreter.cpp b/src/core/PICA/shader_interpreter.cpp index 85ca3c6e4..5ed00b633 100644 --- a/src/core/PICA/shader_interpreter.cpp +++ b/src/core/PICA/shader_interpreter.cpp @@ -223,7 +223,7 @@ void PICAShader::flr(u32 instruction) { u32 componentMask = operandDescriptor & 0xf; for (int i = 0; i < 4; i++) { if (componentMask & (1 << i)) { - destVector[3 - i] = f24::fromFloat32(std::floor(srcVector[3 - 1].toFloat32())); + destVector[3 - i] = f24::fromFloat32(std::floor(srcVector[3 - i].toFloat32())); } } } @@ -244,8 +244,12 @@ void PICAShader::max(u32 instruction) { u32 componentMask = operandDescriptor & 0xf; for (int i = 0; i < 4; i++) { if (componentMask & (1 << i)) { - const auto maximum = srcVec1[3 - i] > srcVec2[3 - i] ? srcVec1[3 - i] : srcVec2[3 - i]; - destVector[3 - i] = maximum; + const float inputA = srcVec1[3 - i].toFloat32(); + const float inputB = srcVec2[3 - i].toFloat32(); + // max(NaN, 2.f) -> NaN + // max(2.f, NaN) -> 2 + const auto& maximum = std::isinf(inputB) ? inputB : std::max(inputB, inputA); + destVector[3 - i] = f24::fromFloat32(maximum); } } } @@ -266,8 +270,12 @@ void PICAShader::min(u32 instruction) { u32 componentMask = operandDescriptor & 0xf; for (int i = 0; i < 4; i++) { if (componentMask & (1 << i)) { - const auto mininum = srcVec1[3 - i] < srcVec2[3 - i] ? srcVec1[3 - i] : srcVec2[3 - i]; - destVector[3 - i] = mininum; + const float inputA = srcVec1[3 - i].toFloat32(); + const float inputB = srcVec2[3 - i].toFloat32(); + // min(NaN, 2.f) -> NaN + // min(2.f, NaN) -> 2 + const auto& mininum = std::min(inputB, inputA); + destVector[3 - i] = f24::fromFloat32(mininum); } } } @@ -382,7 +390,11 @@ void PICAShader::rcp(u32 instruction) { vec4f srcVec1 = getSourceSwizzled<1>(src1, operandDescriptor); vec4f& destVector = getDest(dest); - f24 res = f24::fromFloat32(1.0f) / srcVec1[0]; + float input = srcVec1[0].toFloat32(); + if (input == -0.0f) { + input = 0.0f; + } + const f24 res = f24::fromFloat32(1.0f / input); u32 componentMask = operandDescriptor & 0xf; for (int i = 0; i < 4; i++) { @@ -402,7 +414,11 @@ void PICAShader::rsq(u32 instruction) { vec4f srcVec1 = getSourceSwizzled<1>(src1, operandDescriptor); vec4f& destVector = getDest(dest); - f24 res = f24::fromFloat32(1.0f / std::sqrt(srcVec1[0].toFloat32())); + float input = srcVec1[0].toFloat32(); + if (input == -0.0f) { + input = 0.0f; + } + const f24 res = f24::fromFloat32(1.0f / std::sqrt(input)); u32 componentMask = operandDescriptor & 0xf; for (int i = 0; i < 4; i++) { diff --git a/tests/shader.cpp b/tests/shader.cpp index 2116549dc..edb2743fa 100644 --- a/tests/shader.cpp +++ b/tests/shader.cpp @@ -113,7 +113,7 @@ SHADER_TEST_CASE("RCP", "[shader][vertex]") { {nihstro::OpCode::Id::END}, }); - // REQUIRE(shader->RunScalar({-0.0f}) == INFINITY); // Violates IEEE + REQUIRE(shader->runScalar({-0.0f}) == INFINITY); REQUIRE(shader->runScalar({0.0f}) == INFINITY); REQUIRE(shader->runScalar({INFINITY}) == 0.0f); REQUIRE(std::isnan(shader->runScalar({NAN}))); From e13fe49bbb62b27c561abddfb32095e95ac48776 Mon Sep 17 00:00:00 2001 From: Paris Oplopoios Date: Wed, 13 Mar 2024 22:11:01 +0200 Subject: [PATCH 06/60] Remove duplicate checks in CMakeLists (#461) * Remove duplicate checks in CMakeLists * Bonk --------- Co-authored-by: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> --- CMakeLists.txt | 81 ++++++++++++++++++-------------------------------- 1 file changed, 29 insertions(+), 52 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fe9a8584b..33de5037e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ if (APPLE) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") cmake_minimum_required(VERSION 3.16) else() - cmake_minimum_required(VERSION 3.10) + cmake_minimum_required(VERSION 3.11) endif() set(CMAKE_CXX_STANDARD 20) @@ -41,9 +41,15 @@ option(ENABLE_LUAJIT "Enable scripting with the Lua programming language" ON) option(ENABLE_QT_GUI "Enable the Qt GUI. If not selected then the emulator uses a minimal SDL-based UI instead" OFF) option(BUILD_HYDRA_CORE "Build a Hydra core" OFF) +if(BUILD_HYDRA_CORE) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) +endif() + +add_library(AlberCore STATIC) + include_directories(${PROJECT_SOURCE_DIR}/include/) include_directories(${PROJECT_SOURCE_DIR}/include/kernel) -include_directories (${FMT_INCLUDE_DIR}) +include_directories(${FMT_INCLUDE_DIR}) include_directories(third_party/boost/) include_directories(third_party/elfio/) include_directories(third_party/imgui/) @@ -62,10 +68,6 @@ add_compile_definitions(NOMINMAX) # Make windows.h not define min/ma add_compile_definitions(WIN32_LEAN_AND_MEAN) # Make windows.h not include literally everything add_compile_definitions(SDL_MAIN_HANDLED) -if(BUILD_HYDRA_CORE) - set(CMAKE_POSITION_INDEPENDENT_CODE ON) -endif() - if(ENABLE_DISCORD_RPC AND NOT ANDROID) add_subdirectory(third_party/discord-rpc) include_directories(third_party/discord-rpc/include) @@ -81,7 +83,11 @@ endif() set(SDL_STATIC ON CACHE BOOL "" FORCE) set(SDL_SHARED OFF CACHE BOOL "" FORCE) set(SDL_TEST OFF CACHE BOOL "" FORCE) -add_subdirectory(third_party/SDL2) + +if (NOT ANDROID) + add_subdirectory(third_party/SDL2) + target_link_libraries(AlberCore PUBLIC SDL2-static) +endif() add_subdirectory(third_party/toml11) include_directories(${SDL2_INCLUDE_DIR}) @@ -100,6 +106,8 @@ target_include_directories(boost SYSTEM INTERFACE ${Boost_INCLUDE_DIR}) if(ANDROID) set(CRYPTOPP_OPT_DISABLE_ASM ON CACHE BOOL "" FORCE) + target_sources(AlberCore PRIVATE src/jni_driver.cpp) + target_link_libraries(AlberCore PRIVATE EGL log) endif() set(CRYPTOPP_BUILD_TESTING OFF) @@ -116,6 +124,9 @@ if(ENABLE_LUAJIT) target_compile_definitions(minilua PRIVATE _CRT_SECURE_NO_WARNINGS) target_compile_definitions(buildvm PRIVATE _CRT_SECURE_NO_WARNINGS) endif() + + target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_LUA=1") + target_link_libraries(AlberCore PRIVATE libluajit) endif() # Check for x64 @@ -279,6 +290,7 @@ if(ENABLE_LUAJIT AND NOT ANDROID) set(LIBUV_BUILD_SHARED OFF) add_subdirectory(third_party/libuv) + target_link_libraries(AlberCore PRIVATE uv_a) endif() if(ENABLE_QT_GUI) @@ -337,6 +349,10 @@ if(ENABLE_OPENGL) "src/host_shaders/opengl_vertex_shader.vert" "src/host_shaders/opengl_fragment_shader.frag" ) + + target_sources(AlberCore PRIVATE ${RENDERER_GL_SOURCE_FILES}) + target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_OPENGL=1") + target_link_libraries(AlberCore PRIVATE resources_renderer_gl) endif() if(ENABLE_VULKAN) @@ -397,65 +413,26 @@ if(ENABLE_VULKAN) WHENCE "${PROJECT_BINARY_DIR}/host_shaders/" ${RENDERER_VK_HOST_SHADERS_SPIRV} ) + + target_sources(AlberCore PRIVATE ${RENDERER_VK_SOURCE_FILES}) + target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_VULKAN=1") + target_link_libraries(AlberCore PRIVATE Vulkan::Vulkan resources_renderer_vk) endif() source_group("Header Files\\Core" FILES ${HEADER_FILES}) set(ALL_SOURCES ${SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERNEL_SOURCE_FILES} ${LOADER_SOURCE_FILES} ${SERVICE_SOURCE_FILES} ${APPLET_SOURCE_FILES} ${RENDERER_SW_SOURCE_FILES} ${PICA_SOURCE_FILES} ${THIRD_PARTY_SOURCE_FILES} ${AUDIO_SOURCE_FILES} ${HEADER_FILES} ${FRONTEND_HEADER_FILES}) - -if(ANDROID) - set(ALL_SOURCES ${ALL_SOURCES} src/jni_driver.cpp) -endif() - -if(ENABLE_OPENGL) - # Add the OpenGL source files to ALL_SOURCES - set(ALL_SOURCES ${ALL_SOURCES} ${RENDERER_GL_SOURCE_FILES}) -endif() - -if(ENABLE_VULKAN) - # Add the Vulkan source files to ALL_SOURCES - set(ALL_SOURCES ${ALL_SOURCES} ${RENDERER_VK_SOURCE_FILES}) -endif() - -add_library(AlberCore STATIC ${ALL_SOURCES}) - -if(ANDROID) - target_link_libraries(AlberCore PRIVATE EGL log) -endif() +target_sources(AlberCore PRIVATE ${ALL_SOURCES}) target_link_libraries(AlberCore PRIVATE dynarmic cryptopp glad resources_console_fonts teakra) target_link_libraries(AlberCore PUBLIC glad) -if(NOT ANDROID) - target_link_libraries(AlberCore PUBLIC SDL2-static) -endif() - if(ENABLE_DISCORD_RPC AND NOT ANDROID) target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_DISCORD_RPC=1") target_link_libraries(AlberCore PRIVATE discord-rpc) endif() -if(ENABLE_LUAJIT) - target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_LUA=1") - target_link_libraries(AlberCore PRIVATE libluajit) - - # If we're not on Android, link libuv too - if (NOT ANDROID) - target_link_libraries(AlberCore PRIVATE uv_a) - endif() -endif() - -if(ENABLE_OPENGL) - target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_OPENGL=1") - target_link_libraries(AlberCore PRIVATE resources_renderer_gl) -endif() - -if(ENABLE_VULKAN) - target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_VULKAN=1") - target_link_libraries(AlberCore PRIVATE Vulkan::Vulkan resources_renderer_vk) -endif() - if(GPU_DEBUG_INFO) target_compile_definitions(AlberCore PRIVATE GPU_DEBUG_INFO=1) endif() @@ -545,4 +522,4 @@ if(ENABLE_TESTS) ) add_test(AlberTests AlberTests) -endif() \ No newline at end of file +endif() From cda4aa9ee69adc098b1fcb6c436506693bf8709b Mon Sep 17 00:00:00 2001 From: offtkp Date: Thu, 14 Mar 2024 12:29:36 +0200 Subject: [PATCH 07/60] Merge most ENABLE_QT_GUIs --- CMakeLists.txt | 58 +++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 33de5037e..ca0bcdd2e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -# We need to be able to use enable_language(OBJC) on Mac, so we need CMake 3.16 vs the 3.10 we use otherwise. Blame Apple. +# We need to be able to use enable_language(OBJC) on Mac, so we need CMake 3.16 vs the 3.11 we use otherwise. Blame Apple. if (APPLE) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") cmake_minimum_required(VERSION 3.16) @@ -73,13 +73,6 @@ if(ENABLE_DISCORD_RPC AND NOT ANDROID) include_directories(third_party/discord-rpc/include) endif() -if(ENABLE_QT_GUI) - find_package(Qt6 REQUIRED COMPONENTS Widgets) - if(NOT ENABLE_OPENGL) - message(FATAL_ERROR "Qt frontend requires OpenGL") - endif() -endif() - set(SDL_STATIC ON CACHE BOOL "" FORCE) set(SDL_SHARED OFF CACHE BOOL "" FORCE) set(SDL_TEST OFF CACHE BOOL "" FORCE) @@ -208,27 +201,6 @@ set(AUDIO_SOURCE_FILES src/core/audio/dsp_core.cpp src/core/audio/null_core.cpp ) set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) -# Frontend source files -if(ENABLE_QT_GUI) - set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp - src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp - ) - set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp - include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp - ) - - source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) - source_group("Header Files\\Qt" FILES ${FRONTEND_HEADER_FILES}) - include_directories(${Qt6Gui_PRIVATE_INCLUDE_DIRS}) - - include_directories(third_party/zep/include) # Include zep for text editor usage - configure_file(third_party/zep/cmake/config_app.h.cmake ${CMAKE_BINARY_DIR}/zep_config/config_app.h) - include_directories(${CMAKE_BINARY_DIR}/zep_config) -else() - set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp) - set(FRONTEND_HEADER_FILES "") -endif() - set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/cpu.hpp include/cpu_dynarmic.hpp include/memory.hpp include/renderer.hpp include/kernel/kernel.hpp include/dynarmic_cp15.hpp include/kernel/resource_limits.hpp include/kernel/kernel_types.hpp @@ -458,10 +430,29 @@ else() endif() if(NOT BUILD_HYDRA_CORE) - add_executable(Alber ${FRONTEND_SOURCE_FILES} ${FRONTEND_HEADER_FILES}) - target_link_libraries(Alber PRIVATE AlberCore) + add_executable(Alber) if(ENABLE_QT_GUI) + find_package(Qt6 REQUIRED COMPONENTS Widgets) + if(NOT ENABLE_OPENGL) + message(FATAL_ERROR "Qt frontend requires OpenGL") + endif() + + set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp + src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp + ) + set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp + include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp + ) + + source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) + source_group("Header Files\\Qt" FILES ${FRONTEND_HEADER_FILES}) + include_directories(${Qt6Gui_PRIVATE_INCLUDE_DIRS}) + + include_directories(third_party/zep/include) # Include zep for text editor usage + configure_file(third_party/zep/cmake/config_app.h.cmake ${CMAKE_BINARY_DIR}/zep_config/config_app.h) + include_directories(${CMAKE_BINARY_DIR}/zep_config) + target_compile_definitions(Alber PUBLIC "ZEP_QT=1") target_compile_definitions(Alber PUBLIC "ZEP_FEATURE_CPP_FILE_SYSTEM=1") target_link_libraries(Alber PRIVATE Qt6::Widgets) @@ -487,7 +478,12 @@ if(NOT BUILD_HYDRA_CORE) docs/img/rsob_icon.png docs/img/rstarstruck_icon.png ) else() + set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp) + set(FRONTEND_HEADER_FILES "") endif() + + target_link_libraries(Alber PRIVATE AlberCore) + target_sources(Alber PRIVATE ${FRONTEND_SOURCE_FILES} ${FRONTEND_HEADER_FILES}) elseif(BUILD_HYDRA_CORE) target_compile_definitions(AlberCore PRIVATE PANDA3DS_HYDRA_CORE=1) include_directories(third_party/hydra_core/include) From 2b34ef4a891b106b47aca170134ae6236f68810d Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Thu, 14 Mar 2024 12:16:57 -0700 Subject: [PATCH 08/60] Implement PICA200 compliant arm64 `MUL` Adds `emitSafeMUL` to implement a PICA200 compliant multiplication that handles the special `0 * inf = 0` case. --- .../dynapica/shader_rec_emitter_arm64.hpp | 7 +-- .../dynapica/shader_rec_emitter_arm64.cpp | 50 ++++++++++++++++--- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/include/PICA/dynapica/shader_rec_emitter_arm64.hpp b/include/PICA/dynapica/shader_rec_emitter_arm64.hpp index 7a4a63500..7411c4308 100644 --- a/include/PICA/dynapica/shader_rec_emitter_arm64.hpp +++ b/include/PICA/dynapica/shader_rec_emitter_arm64.hpp @@ -42,6 +42,9 @@ class ShaderEmitter : private oaknut::CodeBlock, public oaknut::CodeGenerator { oaknut::Label emitLog2Func(); oaknut::Label emitExp2Func(); + // Emit a PICA200-compliant multiplication that handles "0 * inf = 0" + void emitSafeMUL(oaknut::QReg src1, oaknut::QReg src2, oaknut::QReg scratch0); + template T getLabelPointer(const oaknut::Label& label) { auto pointer = reinterpret_cast(oaknut::CodeBlock::ptr()) + label.offset(); @@ -123,9 +126,7 @@ class ShaderEmitter : private oaknut::CodeBlock, public oaknut::CodeGenerator { ShaderEmitter() : oaknut::CodeBlock(allocSize), oaknut::CodeGenerator(oaknut::CodeBlock::ptr()) {} // PC must be a valid entrypoint here. It doesn't have that much overhead in this case, so we use std::array<>::at() to assert it does - InstructionCallback getInstructionCallback(u32 pc) { - return getLabelPointer(instructionLabels.at(pc)); - } + InstructionCallback getInstructionCallback(u32 pc) { return getLabelPointer(instructionLabels.at(pc)); } PrologueCallback getPrologueCallback() { return prologueCb; } void compile(const PICAShader& shaderUnit); diff --git a/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp b/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp index 6a3fbfee8..7d8960f90 100644 --- a/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp +++ b/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp @@ -7,6 +7,9 @@ using namespace Helpers; using namespace oaknut; using namespace oaknut::util; +// TODO: Expose safe/unsafe optimizations to the user +constexpr bool useSafeMUL = true; + // Similar to the x64 recompiler, we use an odd internal ABI, which abuses the fact that we'll very rarely be calling C++ functions // So to avoid pushing and popping, we'll be making use of volatile registers as much as possible static constexpr QReg scratch1 = Q0; @@ -474,14 +477,18 @@ void ShaderEmitter::recDP3(const PICAShader& shader, u32 instruction) { const u32 dest = getBits<21, 5>(instruction); const u32 writeMask = getBits<0, 4>(operandDescriptor); - // TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA) loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); // Set W component of src1 to 0.0, so that the w factor of the following dp4 will become 0, making it equivalent to a dp3 INS(src1_vec.Selem()[3], WZR); // Now do a full DP4 - FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); // Do a piecewise multiplication of the vectors first + // Do a piecewise multiplication of the vectors first + if constexpr (useSafeMUL) { + emitSafeMUL(src1_vec, src2_vec, scratch1); + } else { + FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); + } FADDP(src1_vec.S4(), src1_vec.S4(), src1_vec.S4()); // Now add the adjacent components together FADDP(src1_vec.toS(), src1_vec.toD().S2()); // Again for the bottom 2 lanes. Now the bottom lane contains the dot product @@ -500,11 +507,15 @@ void ShaderEmitter::recDP4(const PICAShader& shader, u32 instruction) { const u32 dest = getBits<21, 5>(instruction); const u32 writeMask = getBits<0, 4>(operandDescriptor); - // TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA) loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); - FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); // Do a piecewise multiplication of the vectors first + // Do a piecewise multiplication of the vectors first + if constexpr (useSafeMUL) { + emitSafeMUL(src1_vec, src2_vec, scratch1); + } else { + FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); + } FADDP(src1_vec.S4(), src1_vec.S4(), src1_vec.S4()); // Now add the adjacent components together FADDP(src1_vec.toS(), src1_vec.toD().S2()); // Again for the bottom 2 lanes. Now the bottom lane contains the dot product @@ -515,6 +526,20 @@ void ShaderEmitter::recDP4(const PICAShader& shader, u32 instruction) { storeRegister(src1_vec, shader, dest, operandDescriptor); } +void ShaderEmitter::emitSafeMUL(oaknut::QReg src1, oaknut::QReg src2, oaknut::QReg scratch0) { + // 0 * inf and inf * 0 in the PICA should return 0 instead of NaN + // This can be done by checking for NaNs before and after a multiplication + + // FMULX returns 2.0 in the case of 0.0 * inf or inf * 0.0 + // Both a FMUL and FMULX are done and the results are compared to each other + // In the case that the results are diferent(a 0.0*inf happened), then + // a 0.0 is written + FMULX(scratch1.S4(), src1.S4(), src2.S4()); + FMUL(src1.S4(), src1.S4(), src2.S4()); + CMEQ(scratch1.S4(), scratch1.S4(), src1.S4()); + AND(src1.B16(), src1.B16(), scratch1.B16()); +} + void ShaderEmitter::recADD(const PICAShader& shader, u32 instruction) { const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; const u32 src1 = getBits<12, 7>(instruction); @@ -561,10 +586,15 @@ void ShaderEmitter::recMUL(const PICAShader& shader, u32 instruction) { const u32 idx = getBits<19, 2>(instruction); const u32 dest = getBits<21, 5>(instruction); - // TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA) loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); - FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); + + if constexpr (useSafeMUL) { + emitSafeMUL(src1_vec, src2_vec, scratch1); + } else { + FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); + } + storeRegister(src1_vec, shader, dest, operandDescriptor); } @@ -632,8 +662,12 @@ void ShaderEmitter::recMAD(const PICAShader& shader, u32 instruction) { loadRegister<2>(src2_vec, shader, src2, isMADI ? 0 : idx, operandDescriptor); loadRegister<3>(src3_vec, shader, src3, isMADI ? idx : 0, operandDescriptor); - // TODO: Safe PICA multiplication - FMLA(src3_vec.S4(), src1_vec.S4(), src2_vec.S4()); + if constexpr (useSafeMUL) { + emitSafeMUL(src1_vec, src2_vec, scratch1); + FADD(src3_vec.S4(), src3_vec.S4(), src1_vec.S4()); + } else { + FMLA(src3_vec.S4(), src1_vec.S4(), src2_vec.S4()); + } storeRegister(src3_vec, shader, dest, operandDescriptor); } From 27ad7b01f3971c059d35e63c416191ea7aef4f52 Mon Sep 17 00:00:00 2001 From: Paris Oplopoios Date: Sat, 16 Mar 2024 23:20:37 +0200 Subject: [PATCH 09/60] Rename Emulator::run to FrontendSDL::run (#466) * Rename Emulator::run to FrontendSDL::run * Update frontend_sdl.cpp --------- Co-authored-by: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> --- include/emulator.hpp | 12 ------- include/panda_sdl/frontend_sdl.hpp | 11 ++++++ src/emulator.cpp | 2 +- src/panda_sdl/frontend_sdl.cpp | 57 +++++++++++++++--------------- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/include/emulator.hpp b/include/emulator.hpp index 47fbc839f..de04648ea 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -51,22 +51,11 @@ class Emulator { MiniAudioDevice audioDevice; Cheats cheats; - // Variables to keep track of whether the user is controlling the 3DS analog stick with their keyboard - // This is done so when a gamepad is connected, we won't automatically override the 3DS analog stick settings with the gamepad's state - // And so the user can still use the keyboard to control the analog - bool keyboardAnalogX = false; - bool keyboardAnalogY = false; - - // For tracking whether to update gyroscope - // We bind gyro to right click + mouse movement - bool holdingRightClick = false; - public: static constexpr u32 width = 400; static constexpr u32 height = 240 * 2; // * 2 because 2 screens ROMType romType = ROMType::None; bool running = false; // Is the emulator running a game? - bool programRunning = false; // Is the emulator program itself running? private: #ifdef PANDA3DS_ENABLE_HTTP_SERVER @@ -103,7 +92,6 @@ class Emulator { void step(); void render(); void reset(ReloadOption reload); - void run(void* frontend = nullptr); void runFrame(); // Poll the scheduler for events void pollScheduler(); diff --git a/include/panda_sdl/frontend_sdl.hpp b/include/panda_sdl/frontend_sdl.hpp index 8e58c7684..2d206175c 100644 --- a/include/panda_sdl/frontend_sdl.hpp +++ b/include/panda_sdl/frontend_sdl.hpp @@ -20,4 +20,15 @@ class FrontendSDL { SDL_Window* window = nullptr; SDL_GameController* gameController = nullptr; int gameControllerID; + bool programRunning = true; + + // For tracking whether to update gyroscope + // We bind gyro to right click + mouse movement + bool holdingRightClick = false; + + // Variables to keep track of whether the user is controlling the 3DS analog stick with their keyboard + // This is done so when a gamepad is connected, we won't automatically override the 3DS analog stick settings with the gamepad's state + // And so the user can still use the keyboard to control the analog + bool keyboardAnalogX = false; + bool keyboardAnalogY = false; }; \ No newline at end of file diff --git a/src/emulator.cpp b/src/emulator.cpp index a02ead482..16c3bffd9 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -18,7 +18,7 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1; Emulator::Emulator() : config(getConfigPath()), kernel(cpu, memory, gpu, config), cpu(memory, kernel, *this), gpu(memory, config), memory(cpu.getTicksRef(), config), - cheats(memory, kernel.getServiceManager().getHID()), lua(*this), running(false), programRunning(false) + cheats(memory, kernel.getServiceManager().getHID()), lua(*this), running(false) #ifdef PANDA3DS_ENABLE_HTTP_SERVER , httpServer(this) diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index 04b582e1c..b6486b45c 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -67,19 +67,20 @@ FrontendSDL::FrontendSDL() { } bool FrontendSDL::loadROM(const std::filesystem::path& path) { return emu.loadROM(path); } -void FrontendSDL::run() { emu.run(this); } -void Emulator::run(void* frontend) { - FrontendSDL* frontendSDL = reinterpret_cast(frontend); +void FrontendSDL::run() { programRunning = true; + keyboardAnalogX = false; + keyboardAnalogY = false; + holdingRightClick = false; while (programRunning) { #ifdef PANDA3DS_ENABLE_HTTP_SERVER httpServer.processActions(); #endif - runFrame(); - HIDService& hid = kernel.getServiceManager().getHID(); + emu.runFrame(); + HIDService& hid = emu.getServiceManager().getHID(); SDL_Event event; while (SDL_PollEvent(&event)) { @@ -92,7 +93,7 @@ void Emulator::run(void* frontend) { return; case SDL_KEYDOWN: - if (romType == ROMType::None) break; + if (emu.romType == ROMType::None) break; switch (event.key.keysym.sym) { case SDLK_l: hid.pressKey(Keys::A); break; @@ -134,20 +135,20 @@ void Emulator::run(void* frontend) { // Use the F4 button as a hot-key to pause or resume the emulator // We can't use the audio play/pause buttons because it's annoying case SDLK_F4: { - togglePause(); + emu.togglePause(); break; } // Use F5 as a reset button case SDLK_F5: { - reset(ReloadOption::Reload); + emu.reset(Emulator::ReloadOption::Reload); break; } } break; case SDL_KEYUP: - if (romType == ROMType::None) break; + if (emu.romType == ROMType::None) break; switch (event.key.keysym.sym) { case SDLK_l: hid.releaseKey(Keys::A); break; @@ -182,7 +183,7 @@ void Emulator::run(void* frontend) { break; case SDL_MOUSEBUTTONDOWN: - if (romType == ROMType::None) break; + if (emu.romType == ROMType::None) break; if (event.button.button == SDL_BUTTON_LEFT) { const s32 x = event.button.x; @@ -205,7 +206,7 @@ void Emulator::run(void* frontend) { break; case SDL_MOUSEBUTTONUP: - if (romType == ROMType::None) break; + if (emu.romType == ROMType::None) break; if (event.button.button == SDL_BUTTON_LEFT) { hid.releaseTouchScreen(); @@ -215,23 +216,23 @@ void Emulator::run(void* frontend) { break; case SDL_CONTROLLERDEVICEADDED: - if (frontendSDL->gameController == nullptr) { - frontendSDL->gameController = SDL_GameControllerOpen(event.cdevice.which); - frontendSDL->gameControllerID = event.cdevice.which; + if (gameController == nullptr) { + gameController = SDL_GameControllerOpen(event.cdevice.which); + gameControllerID = event.cdevice.which; } break; case SDL_CONTROLLERDEVICEREMOVED: - if (event.cdevice.which == frontendSDL->gameControllerID) { - SDL_GameControllerClose(frontendSDL->gameController); - frontendSDL->gameController = nullptr; - frontendSDL->gameControllerID = 0; + if (event.cdevice.which == gameControllerID) { + SDL_GameControllerClose(gameController); + gameController = nullptr; + gameControllerID = 0; } break; case SDL_CONTROLLERBUTTONUP: case SDL_CONTROLLERBUTTONDOWN: { - if (romType == ROMType::None) break; + if (emu.romType == ROMType::None) break; u32 key = 0; switch (event.cbutton.button) { @@ -261,7 +262,7 @@ void Emulator::run(void* frontend) { // Detect mouse motion events for gyroscope emulation case SDL_MOUSEMOTION: { - if (romType == ROMType::None) break; + if (emu.romType == ROMType::None) break; // Handle "dragging" across the touchscreen if (hid.isTouchScreenPressed()) { @@ -301,9 +302,9 @@ void Emulator::run(void* frontend) { const std::filesystem::path path(droppedDir); if (path.extension() == ".amiibo") { - loadAmiibo(path); + emu.loadAmiibo(path); } else if (path.extension() == ".lua") { - lua.loadFile(droppedDir); + emu.getLua().loadFile(droppedDir); } else { loadROM(path); } @@ -316,10 +317,10 @@ void Emulator::run(void* frontend) { } // Update controller analog sticks and HID service - if (romType != ROMType::None) { - if (frontendSDL->gameController != nullptr) { - const s16 stickX = SDL_GameControllerGetAxis(frontendSDL->gameController, SDL_CONTROLLER_AXIS_LEFTX); - const s16 stickY = SDL_GameControllerGetAxis(frontendSDL->gameController, SDL_CONTROLLER_AXIS_LEFTY); + if (emu.romType != ROMType::None) { + if (gameController != nullptr) { + const s16 stickX = SDL_GameControllerGetAxis(gameController, SDL_CONTROLLER_AXIS_LEFTX); + const s16 stickY = SDL_GameControllerGetAxis(gameController, SDL_CONTROLLER_AXIS_LEFTY); constexpr s16 deadzone = 3276; constexpr s16 maxValue = 0x9C; constexpr s16 div = 0x8000 / maxValue; @@ -338,11 +339,11 @@ void Emulator::run(void* frontend) { } } - hid.updateInputs(cpu.getTicks()); + hid.updateInputs(emu.getTicks()); } // TODO: Should this be uncommented? // kernel.evalReschedule(); - SDL_GL_SwapWindow(frontendSDL->window); + SDL_GL_SwapWindow(window); } } From dcd64802a354db828085c4d2190951011ab245cc Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Tue, 19 Mar 2024 10:13:04 -0700 Subject: [PATCH 10/60] Refactor `ShaderJITTest` into a specialization of `ShaderInterpreterTest` --- tests/shader.cpp | 58 +++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/tests/shader.cpp b/tests/shader.cpp index edb2743fa..29c07481c 100644 --- a/tests/shader.cpp +++ b/tests/shader.cpp @@ -7,6 +7,7 @@ #include #include #include +#include using namespace Floats; static const nihstro::SourceRegister input0 = nihstro::SourceRegister::MakeInput(0); @@ -28,48 +29,59 @@ static std::unique_ptr assembleVertexShader(std::initializer_list shader = {}; + virtual void runShader() { shader->run(); } + public: explicit ShaderInterpreterTest(std::initializer_list code) : shader(assembleVertexShader(code)) {} - // Multiple inputs, singular scalar output - float runScalar(std::initializer_list inputs) { - usize inputIndex = 0; + std::span> runTest(std::span> inputs) { + std::copy(inputs.begin(), inputs.end(), shader->inputs.begin()); + runShader(); + return shader->outputs; + } + + // Each input is written to the x component of sequential input registers + // The first output vector is returned + const std::array& runVector(std::initializer_list inputs) { + std::vector> inputsVec; for (const float& input : inputs) { - const std::array input_vec = std::array{f24::fromFloat32(input), f24::zero(), f24::zero(), f24::zero()}; - shader->inputs[inputIndex++] = input_vec; + const std::array inputVec = { + f24::fromFloat32(input), + f24::zero(), + f24::zero(), + f24::zero(), + }; + inputsVec.emplace_back(inputVec); } - shader->run(); - return shader->outputs[0][0]; + return runTest(inputsVec)[0]; } + // Each input is written to the x component of sequential input registers + // The x component of the first output + float runScalar(std::initializer_list inputs) { return runVector(inputs)[0].toFloat32(); } + + [[nodiscard]] std::array, 96>& floatUniforms() const { return shader->floatUniforms; } + [[nodiscard]] std::array, 4>& intUniforms() const { return shader->intUniforms; } + [[nodiscard]] u32& boolUniform() const { return shader->boolUniform; } + static std::unique_ptr assembleTest(std::initializer_list code) { return std::make_unique(code); } }; #if defined(PANDA3DS_SHADER_JIT_SUPPORTED) -class ShaderJITTest final { +class ShaderJITTest final : public ShaderInterpreterTest { private: - std::unique_ptr shader = {}; ShaderJIT shaderJit = {}; - public: - explicit ShaderJITTest(std::initializer_list code) : shader(assembleVertexShader(code)) { shaderJit.prepare(*shader.get()); } + void runShader() override { shaderJit.run(*shader); } - // Multiple inputs, singular scalar output - float runScalar(std::initializer_list inputs) { - usize inputIndex = 0; - for (const float& input : inputs) { - const std::array input_vec = std::array{f24::fromFloat32(input), f24::zero(), f24::zero(), f24::zero()}; - shader->inputs[inputIndex++] = input_vec; - } - shaderJit.run(*shader.get()); - return shader->outputs[0][0]; - } + public: + explicit ShaderJITTest(std::initializer_list code) : ShaderInterpreterTest(code) { shaderJit.prepare(*shader); } static std::unique_ptr assembleTest(std::initializer_list code) { return std::make_unique(code); From 40e2774b7f0c20bacee24ebf84572c383573f896 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Tue, 19 Mar 2024 13:17:07 -0700 Subject: [PATCH 11/60] Implement arm64 `LG2`/`EX2` --- .../dynapica/shader_rec_emitter_arm64.cpp | 462 +++++++++++++----- 1 file changed, 350 insertions(+), 112 deletions(-) diff --git a/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp b/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp index 7d8960f90..d63580701 100644 --- a/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp +++ b/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp @@ -12,16 +12,19 @@ constexpr bool useSafeMUL = true; // Similar to the x64 recompiler, we use an odd internal ABI, which abuses the fact that we'll very rarely be calling C++ functions // So to avoid pushing and popping, we'll be making use of volatile registers as much as possible -static constexpr QReg scratch1 = Q0; -static constexpr QReg scratch2 = Q1; -static constexpr QReg src1_vec = Q2; -static constexpr QReg src2_vec = Q3; -static constexpr QReg src3_vec = Q4; -static constexpr QReg onesVector = Q5; +static constexpr QReg src1Vec = Q1; +static constexpr QReg src2Vec = Q2; +static constexpr QReg src3Vec = Q3; +static constexpr QReg scratch1Vec = Q16; +static constexpr QReg scratch2Vec = Q17; +static constexpr QReg scratch3Vec = Q18; +static constexpr QReg onesVector = Q31; static constexpr XReg arg1 = X0; static constexpr XReg arg2 = X1; -static constexpr XReg statePointer = X9; +static constexpr XReg scratch1 = X9; +static constexpr XReg scratch2 = X10; +static constexpr XReg statePointer = X15; void ShaderEmitter::compile(const PICAShader& shaderUnit) { oaknut::CodeBlock::unprotect(); // Unprotect the memory before writing to it @@ -62,8 +65,12 @@ void ShaderEmitter::compile(const PICAShader& shaderUnit) { // Scan the code for call, exp2, log2, etc instructions which need some special care // After that, emit exp2 and log2 functions if the corresponding instructions are present scanCode(shaderUnit); - if (codeHasExp2) Helpers::panic("arm64 shader JIT: Code has exp2"); - if (codeHasLog2) Helpers::panic("arm64 shader JIT: Code has log2"); + if (codeHasExp2) { + exp2Func = emitExp2Func(); + } + if (codeHasLog2) { + log2Func = emitLog2Func(); + } align(16); // Compile every instruction in the shader @@ -140,13 +147,13 @@ void ShaderEmitter::compileInstruction(const PICAShader& shaderUnit) { // case ShaderOpcodes::DPH: // case ShaderOpcodes::DPHI: recDPH(shaderUnit, instruction); break; case ShaderOpcodes::END: recEND(shaderUnit, instruction); break; - // case ShaderOpcodes::EX2: recEX2(shaderUnit, instruction); break; + case ShaderOpcodes::EX2: recEX2(shaderUnit, instruction); break; case ShaderOpcodes::FLR: recFLR(shaderUnit, instruction); break; case ShaderOpcodes::IFC: recIFC(shaderUnit, instruction); break; case ShaderOpcodes::IFU: recIFU(shaderUnit, instruction); break; case ShaderOpcodes::JMPC: recJMPC(shaderUnit, instruction); break; case ShaderOpcodes::JMPU: recJMPU(shaderUnit, instruction); break; - // case ShaderOpcodes::LG2: recLG2(shaderUnit, instruction); break; + case ShaderOpcodes::LG2: recLG2(shaderUnit, instruction); break; case ShaderOpcodes::LOOP: recLOOP(shaderUnit, instruction); break; case ShaderOpcodes::MOV: recMOV(shaderUnit, instruction); break; case ShaderOpcodes::MOVA: recMOVA(shaderUnit, instruction); break; @@ -221,7 +228,7 @@ void ShaderEmitter::loadRegister(QReg dest, const PICAShader& shader, u32 src, u u32 compSwizzle; // Component swizzle pattern for the register bool negate; // If true, negate all lanes of the register - if constexpr (sourceIndex == 1) { // SRC1 + if constexpr (sourceIndex == 1) { // src1Vec negate = (getBit<4>(operandDescriptor)) != 0; compSwizzle = getBits<5, 8>(operandDescriptor); } else if constexpr (sourceIndex == 2) { // SRC2 @@ -252,7 +259,7 @@ void ShaderEmitter::loadRegister(QReg dest, const PICAShader& shader, u32 src, u // Some of these cases may still be optimizable default: { - MOV(scratch1.B16(), dest.B16()); // Make a copy of the register + MOV(scratch1Vec.B16(), dest.B16()); // Make a copy of the register const auto newX = getBits<6, 2>(compSwizzle); const auto newY = getBits<4, 2>(compSwizzle); @@ -262,19 +269,19 @@ void ShaderEmitter::loadRegister(QReg dest, const PICAShader& shader, u32 src, u // If the lane swizzled into the new x component is NOT the current x component, swizzle the correct lane with a mov // Repeat for each component of the vector if (newX != 0) { - MOV(dest.Selem()[0], scratch1.Selem()[newX]); + MOV(dest.Selem()[0], scratch1Vec.Selem()[newX]); } if (newY != 1) { - MOV(dest.Selem()[1], scratch1.Selem()[newY]); + MOV(dest.Selem()[1], scratch1Vec.Selem()[newY]); } if (newZ != 2) { - MOV(dest.Selem()[2], scratch1.Selem()[newZ]); + MOV(dest.Selem()[2], scratch1Vec.Selem()[newZ]); } if (newW != 3) { - MOV(dest.Selem()[3], scratch1.Selem()[newW]); + MOV(dest.Selem()[3], scratch1Vec.Selem()[newW]); } break; @@ -326,7 +333,7 @@ void ShaderEmitter::loadRegister(QReg dest, const PICAShader& shader, u32 src, u // Some of these cases may still be optimizable default: { - MOV(scratch1.B16(), dest.B16()); // Make a copy of the register + MOV(scratch1Vec.B16(), dest.B16()); // Make a copy of the register const auto newX = getBits<6, 2>(compSwizzle); const auto newY = getBits<4, 2>(compSwizzle); @@ -336,19 +343,19 @@ void ShaderEmitter::loadRegister(QReg dest, const PICAShader& shader, u32 src, u // If the lane swizzled into the new x component is NOT the current x component, swizzle the correct lane with a mov // Repeat for each component of the vector if (newX != 0) { - MOV(dest.Selem()[0], scratch1.Selem()[newX]); + MOV(dest.Selem()[0], scratch1Vec.Selem()[newX]); } if (newY != 1) { - MOV(dest.Selem()[1], scratch1.Selem()[newY]); + MOV(dest.Selem()[1], scratch1Vec.Selem()[newY]); } if (newZ != 2) { - MOV(dest.Selem()[2], scratch1.Selem()[newZ]); + MOV(dest.Selem()[2], scratch1Vec.Selem()[newZ]); } if (newW != 3) { - MOV(dest.Selem()[3], scratch1.Selem()[newW]); + MOV(dest.Selem()[3], scratch1Vec.Selem()[newW]); } break; @@ -411,11 +418,11 @@ void ShaderEmitter::storeRegister(QReg source, const PICAShader& shader, u32 des STR(source, statePointer, offset); } else { u8* blendMaskPointer = getLabelPointer(blendMasks); - LDR(scratch1, statePointer, offset); // Load current value - LDR(scratch2, blendMaskPointer + writeMask * 16); // Load write mask for blending + LDR(scratch1Vec, statePointer, offset); // Load current value + LDR(scratch2Vec, blendMaskPointer + writeMask * 16); // Load write mask for blending - BSL(scratch2.B16(), source.B16(), scratch1.B16()); // Scratch2 = (Source & mask) | (original & ~mask) - STR(scratch2, statePointer, offset); // Write it back + BSL(scratch2Vec.B16(), source.B16(), scratch1Vec.B16()); // Scratch2 = (Source & mask) | (original & ~mask) + STR(scratch2Vec, statePointer, offset); // Write it back } } @@ -425,8 +432,8 @@ void ShaderEmitter::recMOV(const PICAShader& shader, u32 instruction) { const u32 idx = getBits<19, 2>(instruction); const u32 dest = getBits<21, 5>(instruction); - loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1 - storeRegister(src1_vec, shader, dest, operandDescriptor); + loadRegister<1>(src1Vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1Vec + storeRegister(src1Vec, shader, dest, operandDescriptor); } void ShaderEmitter::recFLR(const PICAShader& shader, u32 instruction) { @@ -435,9 +442,9 @@ void ShaderEmitter::recFLR(const PICAShader& shader, u32 instruction) { const u32 idx = getBits<19, 2>(instruction); const u32 dest = getBits<21, 5>(instruction); - loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1 - FRINTM(src1_vec.S4(), src1_vec.S4()); // Floor it and store into dest - storeRegister(src1_vec, shader, dest, operandDescriptor); + loadRegister<1>(src1Vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1Vec + FRINTM(src1Vec.S4(), src1Vec.S4()); // Floor it and store into dest + storeRegister(src1Vec, shader, dest, operandDescriptor); } void ShaderEmitter::recMOVA(const PICAShader& shader, u32 instruction) { @@ -455,16 +462,16 @@ void ShaderEmitter::recMOVA(const PICAShader& shader, u32 instruction) { // If no register is being written to then it is a nop. Probably not common but whatever if (!writeX && !writeY) return; - loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor); - FCVTZS(src1_vec.S4(), src1_vec.S4()); // Convert src1 from floats to s32s with truncation + loadRegister<1>(src1Vec, shader, src, idx, operandDescriptor); + FCVTZS(src1Vec.S4(), src1Vec.S4()); // Convert src1 from floats to s32s with truncation // Write both together if (writeX && writeY) { - STR(src1_vec.toD(), statePointer, addrRegisterOffset); + STR(src1Vec.toD(), statePointer, addrRegisterOffset); } else if (writeX) { - STR(src1_vec.toS(), statePointer, addrRegisterOffset); + STR(src1Vec.toS(), statePointer, addrRegisterOffset); } else if (writeY) { - MOV(W0, src1_vec.Selem()[1]); // W0 = Y component + MOV(W0, src1Vec.Selem()[1]); // W0 = Y component STR(W0, statePointer, addrRegisterYOffset); } } @@ -477,26 +484,26 @@ void ShaderEmitter::recDP3(const PICAShader& shader, u32 instruction) { const u32 dest = getBits<21, 5>(instruction); const u32 writeMask = getBits<0, 4>(operandDescriptor); - loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); - loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); + loadRegister<1>(src1Vec, shader, src1, idx, operandDescriptor); + loadRegister<2>(src2Vec, shader, src2, 0, operandDescriptor); // Set W component of src1 to 0.0, so that the w factor of the following dp4 will become 0, making it equivalent to a dp3 - INS(src1_vec.Selem()[3], WZR); + INS(src1Vec.Selem()[3], WZR); // Now do a full DP4 // Do a piecewise multiplication of the vectors first if constexpr (useSafeMUL) { - emitSafeMUL(src1_vec, src2_vec, scratch1); + emitSafeMUL(src1Vec, src2Vec, scratch1Vec); } else { - FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); + FMUL(src1Vec.S4(), src1Vec.S4(), src2Vec.S4()); } - FADDP(src1_vec.S4(), src1_vec.S4(), src1_vec.S4()); // Now add the adjacent components together - FADDP(src1_vec.toS(), src1_vec.toD().S2()); // Again for the bottom 2 lanes. Now the bottom lane contains the dot product + FADDP(src1Vec.S4(), src1Vec.S4(), src1Vec.S4()); // Now add the adjacent components together + FADDP(src1Vec.toS(), src1Vec.toD().S2()); // Again for the bottom 2 lanes. Now the bottom lane contains the dot product - if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x - DUP(src1_vec.S4(), src1_vec.Selem()[0]); // src1_vec = src1_vec.xxxx + if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x + DUP(src1Vec.S4(), src1Vec.Selem()[0]); // src1Vec = src1Vec.xxxx } - storeRegister(src1_vec, shader, dest, operandDescriptor); + storeRegister(src1Vec, shader, dest, operandDescriptor); } void ShaderEmitter::recDP4(const PICAShader& shader, u32 instruction) { @@ -507,23 +514,228 @@ void ShaderEmitter::recDP4(const PICAShader& shader, u32 instruction) { const u32 dest = getBits<21, 5>(instruction); const u32 writeMask = getBits<0, 4>(operandDescriptor); - loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); - loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); + loadRegister<1>(src1Vec, shader, src1, idx, operandDescriptor); + loadRegister<2>(src2Vec, shader, src2, 0, operandDescriptor); // Do a piecewise multiplication of the vectors first if constexpr (useSafeMUL) { - emitSafeMUL(src1_vec, src2_vec, scratch1); + emitSafeMUL(src1Vec, src2Vec, scratch1Vec); } else { - FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); + FMUL(src1Vec.S4(), src1Vec.S4(), src2Vec.S4()); } - FADDP(src1_vec.S4(), src1_vec.S4(), src1_vec.S4()); // Now add the adjacent components together - FADDP(src1_vec.toS(), src1_vec.toD().S2()); // Again for the bottom 2 lanes. Now the bottom lane contains the dot product + FADDP(src1Vec.S4(), src1Vec.S4(), src1Vec.S4()); // Now add the adjacent components together + FADDP(src1Vec.toS(), src1Vec.toD().S2()); // Again for the bottom 2 lanes. Now the bottom lane contains the dot product - if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x - DUP(src1_vec.S4(), src1_vec.Selem()[0]); // src1_vec = src1_vec.xxxx + if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x + DUP(src1Vec.S4(), src1Vec.Selem()[0]); // src1Vec = src1Vec.xxxx } - storeRegister(src1_vec, shader, dest, operandDescriptor); + storeRegister(src1Vec, shader, dest, operandDescriptor); +} + +oaknut::Label ShaderEmitter::emitLog2Func() { + oaknut::Label funcStart; + + // We perform this approximation by first performing a range reduction into the range + // [1.0, 2.0). A minimax polynomial which was fit for the function log2(x) / (x - 1) is then + // evaluated. We multiply the result by (x - 1) then restore the result into the appropriate + // range. Coefficients for the minimax polynomial. + // f(x) computes approximately log2(x) / (x - 1). + // f(x) = c4 + x * (c3 + x * (c2 + x * (c1 + x * c0)). + oaknut::Label c0; + l(c0); + dw(0x3d74552f); + + oaknut::Label c14; + l(c14); + dw(0xbeee7397); + dw(0x3fbd96dd); + dw(0xc02153f6); + dw(0x4038d96c); + + oaknut::Label negativeInfinityVec; + l(negativeInfinityVec); + dw(0xff800000); + dw(0xff800000); + dw(0xff800000); + dw(0xff800000); + + oaknut::Label defaultQnanVec; + l(defaultQnanVec); + dw(0x7fc00000); + dw(0x7fc00000); + dw(0x7fc00000); + dw(0x7fc00000); + + oaknut::Label exit; + oaknut::Label inputIsZero; + oaknut::Label inputOutOfRange; + + l(inputOutOfRange); + B(Cond::EQ, inputIsZero); + ADR(scratch1, defaultQnanVec); + LDR(src1Vec, scratch1); + RET(); + + l(inputIsZero); + ADR(scratch1, negativeInfinityVec); + LDR(src1Vec, scratch1); + RET(); + + l(funcStart); + + // Here we handle edge cases: input in {NaN, 0, -Inf, Negative} + // Ordinal(n) ? 0xFFFFFFFF : 0x0 + FCMEQ(scratch1Vec.toS(), src1Vec.toS(), src1Vec.toS()); + MOV(scratch1.toW(), scratch1Vec.Selem()[0]); + + // src1Vec == NaN + CMP(scratch1.toW(), 0); + B(Cond::EQ, exit); + + // (0.0 >= n) ? 0xFFFFFFFF : 0x0 + MOV(scratch1.toW(), src1Vec.Selem()[0]); + + // src1Vec <= 0.0 + CMP(scratch1.toW(), 0); + B(Cond::LE, inputOutOfRange); + + // Split input: + // src1Vec = MANT[1,2) + // scratch2Vec = Exponent + MOV(scratch1.toW(), src1Vec.Selem()[0]); + MOV(scratch2.toW(), scratch1.toW()); + AND(scratch2.toW(), scratch2.toW(), 0x007fffff); + ORR(scratch2.toW(), scratch2.toW(), 0x3f800000); + MOV(src1Vec.Selem()[0], scratch2.toW()); + // src1Vec now contains the mantissa of the input + UBFX(scratch1.toW(), scratch1.toW(), 23, 8); + SUB(scratch1.toW(), scratch1.toW(), 0x7F); + MOV(scratch2Vec.Selem()[0], scratch1.toW()); + UCVTF(scratch2Vec.toS(), scratch2Vec.toS()); + // scratch2Vec now contains the exponent of the input + + ADR(scratch1, c0); + LDR(scratch1.toW(), scratch1); + MOV(scratch1Vec.Selem()[0], scratch1.toW()); + + // Complete computation of polynomial + // Load C1, C2, C3, C4 into a single scratch register + const QReg C14 = src2Vec; + ADR(scratch1, c14); + LDR(C14, scratch1); + FMUL(scratch1Vec.toS(), scratch1Vec.toS(), src1Vec.toS()); + FMLA(scratch1Vec.toS(), onesVector.toS(), C14.Selem()[0]); + FMUL(scratch1Vec.toS(), scratch1Vec.toS(), src1Vec.toS()); + FMLA(scratch1Vec.toS(), onesVector.toS(), C14.Selem()[1]); + FMUL(scratch1Vec.toS(), scratch1Vec.toS(), src1Vec.toS()); + FMLA(scratch1Vec.toS(), onesVector.toS(), C14.Selem()[2]); + FMUL(scratch1Vec.toS(), scratch1Vec.toS(), src1Vec.toS()); + + FSUB(src1Vec.toS(), src1Vec.toS(), onesVector.toS()); + FMLA(scratch1Vec.toS(), onesVector.toS(), C14.Selem()[3]); + + FMUL(scratch1Vec.toS(), scratch1Vec.toS(), src1Vec.toS()); + FADD(scratch2Vec.toS(), scratch1Vec.toS(), scratch2Vec.toS()); + + // Duplicate result across vector + MOV(src1Vec.Selem()[0], scratch2Vec.Selem()[0]); + l(exit); + DUP(src1Vec.S4(), src1Vec.Selem()[0]); + + RET(); + + return funcStart; +} + +oaknut::Label ShaderEmitter::emitExp2Func() { + oaknut::Label funcStart; + + // This performs a range reduction into the range [-0.5, 0.5) + // A minmax polynomial which was fit for the function exp2(x) is then evaluated + // Then restore the result into the appropriate range + + oaknut::Label inputMax; + l(inputMax); + dw(0x43010000); + oaknut::Label inputMin; + l(inputMin); + dw(0xc2fdffff); + oaknut::Label half; + l(half); + dw(0x3f000000); + oaknut::Label c0; + l(c0); + dw(0x3c5dbe69); + dw(0x3d5509f9); + dw(0x3e773cc5); + dw(0x3f3168b3); + dw(0x3f800016); + + oaknut::Label exit; + + l(funcStart); + + FCMP(src1Vec.toS(), src1Vec.toS()); + // Branch if NaN + B(Cond::NE, exit); + + // Decompose input: + // scratch1Vec = 2^round(input) + // src1Vec = input-round(input) [-0.5, 0.5) + // Clamp to maximum range since we shift the value directly into the exponent + ADR(scratch1, inputMax); + LDR(scratch1Vec.toS(), scratch1, 0); + FMIN(src1Vec.toS(), src1Vec.toS(), scratch1Vec.toS()); + + LDR(scratch1Vec.toS(), scratch1, 4); + FMAX(src1Vec.toS(), src1Vec.toS(), scratch1Vec.toS()); + + ADR(scratch1, half); + LDR(scratch1Vec.toS(), scratch1); + FSUB(scratch1Vec.toS(), src1Vec.toS(), scratch1Vec.toS()); + + FCVTNS(scratch1Vec.toS(), scratch1Vec.toS()); + MOV(scratch1.toW(), scratch1Vec.Selem()[0]); + SCVTF(scratch1Vec.toS(), scratch1.toW()); + + // scratch1Vec now contains input rounded to the nearest integer + ADD(scratch1.toW(), scratch1.toW(), 0x7F); + FSUB(src1Vec.toS(), src1Vec.toS(), scratch1Vec.toS()); + // src1Vec contains input - round(input), which is in [-0.5, 0.5) + LSL(scratch1.toW(), scratch1.toW(), 23); + MOV(scratch1Vec.Selem()[0], scratch1.toW()); + // scratch1Vec contains 2^(round(input)) + + // Complete computation of polynomial + ADR(scratch2, c0); + LDR(scratch2Vec.toS(), scratch2, 0); + FMUL(scratch2Vec.toS(), src1Vec.toS(), scratch2Vec.toS()); + + LDR(scratch3Vec.toS(), scratch2, 4); + FADD(scratch2Vec.toS(), scratch2Vec.toS(), scratch3Vec.toS()); + FMUL(scratch2Vec.toS(), scratch2Vec.toS(), src1Vec.toS()); + + LDR(scratch3Vec.toS(), scratch2, 8); + FADD(scratch2Vec.toS(), scratch2Vec.toS(), scratch3Vec.toS()); + FMUL(scratch2Vec.toS(), scratch2Vec.toS(), src1Vec.toS()); + + LDR(scratch3Vec.toS(), scratch2, 12); + FADD(scratch2Vec.toS(), scratch2Vec.toS(), scratch3Vec.toS()); + FMUL(src1Vec.toS(), scratch2Vec.toS(), src1Vec.toS()); + + LDR(scratch3Vec.toS(), scratch2, 16); + FADD(src1Vec.toS(), scratch3Vec.toS(), src1Vec.toS()); + + FMUL(src1Vec.toS(), src1Vec.toS(), scratch1Vec.toS()); + + // Duplicate result across vector + l(exit); + DUP(src1Vec.S4(), src1Vec.Selem()[0]); + + RET(); + + return funcStart; } void ShaderEmitter::emitSafeMUL(oaknut::QReg src1, oaknut::QReg src2, oaknut::QReg scratch0) { @@ -534,10 +746,10 @@ void ShaderEmitter::emitSafeMUL(oaknut::QReg src1, oaknut::QReg src2, oaknut::QR // Both a FMUL and FMULX are done and the results are compared to each other // In the case that the results are diferent(a 0.0*inf happened), then // a 0.0 is written - FMULX(scratch1.S4(), src1.S4(), src2.S4()); + FMULX(scratch1Vec.S4(), src1.S4(), src2.S4()); FMUL(src1.S4(), src1.S4(), src2.S4()); - CMEQ(scratch1.S4(), scratch1.S4(), src1.S4()); - AND(src1.B16(), src1.B16(), scratch1.B16()); + CMEQ(scratch1Vec.S4(), scratch1Vec.S4(), src1.S4()); + AND(src1.B16(), src1.B16(), scratch1Vec.B16()); } void ShaderEmitter::recADD(const PICAShader& shader, u32 instruction) { @@ -547,10 +759,10 @@ void ShaderEmitter::recADD(const PICAShader& shader, u32 instruction) { const u32 idx = getBits<19, 2>(instruction); const u32 dest = getBits<21, 5>(instruction); - loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); - loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); - FADD(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); - storeRegister(src1_vec, shader, dest, operandDescriptor); + loadRegister<1>(src1Vec, shader, src1, idx, operandDescriptor); + loadRegister<2>(src2Vec, shader, src2, 0, operandDescriptor); + FADD(src1Vec.S4(), src1Vec.S4(), src2Vec.S4()); + storeRegister(src1Vec, shader, dest, operandDescriptor); } void ShaderEmitter::recMAX(const PICAShader& shader, u32 instruction) { @@ -560,10 +772,10 @@ void ShaderEmitter::recMAX(const PICAShader& shader, u32 instruction) { const u32 idx = getBits<19, 2>(instruction); const u32 dest = getBits<21, 5>(instruction); - loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); - loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); - FMAX(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); - storeRegister(src1_vec, shader, dest, operandDescriptor); + loadRegister<1>(src1Vec, shader, src1, idx, operandDescriptor); + loadRegister<2>(src2Vec, shader, src2, 0, operandDescriptor); + FMAX(src1Vec.S4(), src1Vec.S4(), src2Vec.S4()); + storeRegister(src1Vec, shader, dest, operandDescriptor); } void ShaderEmitter::recMIN(const PICAShader& shader, u32 instruction) { @@ -573,10 +785,10 @@ void ShaderEmitter::recMIN(const PICAShader& shader, u32 instruction) { const u32 idx = getBits<19, 2>(instruction); const u32 dest = getBits<21, 5>(instruction); - loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); - loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); - FMIN(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); - storeRegister(src1_vec, shader, dest, operandDescriptor); + loadRegister<1>(src1Vec, shader, src1, idx, operandDescriptor); + loadRegister<2>(src2Vec, shader, src2, 0, operandDescriptor); + FMIN(src1Vec.S4(), src1Vec.S4(), src2Vec.S4()); + storeRegister(src1Vec, shader, dest, operandDescriptor); } void ShaderEmitter::recMUL(const PICAShader& shader, u32 instruction) { @@ -586,16 +798,16 @@ void ShaderEmitter::recMUL(const PICAShader& shader, u32 instruction) { const u32 idx = getBits<19, 2>(instruction); const u32 dest = getBits<21, 5>(instruction); - loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); - loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); + loadRegister<1>(src1Vec, shader, src1, idx, operandDescriptor); + loadRegister<2>(src2Vec, shader, src2, 0, operandDescriptor); if constexpr (useSafeMUL) { - emitSafeMUL(src1_vec, src2_vec, scratch1); + emitSafeMUL(src1Vec, src2Vec, scratch1Vec); } else { - FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); + FMUL(src1Vec.S4(), src1Vec.S4(), src2Vec.S4()); } - storeRegister(src1_vec, shader, dest, operandDescriptor); + storeRegister(src1Vec, shader, dest, operandDescriptor); } void ShaderEmitter::recRCP(const PICAShader& shader, u32 instruction) { @@ -605,16 +817,16 @@ void ShaderEmitter::recRCP(const PICAShader& shader, u32 instruction) { const u32 dest = getBits<21, 5>(instruction); const u32 writeMask = operandDescriptor & 0xf; - loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1 - FDIV(src1_vec.toS(), onesVector.toS(), src1_vec.toS()); // src1 = 1.0 / src1 + loadRegister<1>(src1Vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1Vec + FDIV(src1Vec.toS(), onesVector.toS(), src1Vec.toS()); // src1 = 1.0 / src1 // If we only write back the x component to the result, we needn't perform a shuffle to do res = res.xxxx // Otherwise we do - if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x - DUP(src1_vec.S4(), src1_vec.Selem()[0]); // src1_vec = src1_vec.xxxx + if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x + DUP(src1Vec.S4(), src1Vec.Selem()[0]); // src1Vec = src1Vec.xxxx } - storeRegister(src1_vec, shader, dest, operandDescriptor); + storeRegister(src1Vec, shader, dest, operandDescriptor); } void ShaderEmitter::recRSQ(const PICAShader& shader, u32 instruction) { @@ -625,7 +837,7 @@ void ShaderEmitter::recRSQ(const PICAShader& shader, u32 instruction) { const u32 writeMask = operandDescriptor & 0xf; constexpr bool useAccurateRSQ = true; - loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1 + loadRegister<1>(src1Vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1Vec // Compute reciprocal square root approximation // TODO: Should this use frsqte or fsqrt+div? The former is faster but less accurate @@ -633,19 +845,19 @@ void ShaderEmitter::recRSQ(const PICAShader& shader, u32 instruction) { // It doesn't have regular sqrt/div instructions. // For now, we default to accurate inverse square root if constexpr (useAccurateRSQ) { - FSQRT(src1_vec.toS(), src1_vec.toS()); // src1 = sqrt(src1), scalar - FDIV(src1_vec.toS(), onesVector.toS(), src1_vec.toS()); // Now invert src1 + FSQRT(src1Vec.toS(), src1Vec.toS()); // src1 = sqrt(src1), scalar + FDIV(src1Vec.toS(), onesVector.toS(), src1Vec.toS()); // Now invert src1 } else { - FRSQRTE(src1_vec.toS(), src1_vec.toS()); // Much nicer + FRSQRTE(src1Vec.toS(), src1Vec.toS()); // Much nicer } // If we only write back the x component to the result, we needn't perform a shuffle to do res = res.xxxx // Otherwise we do - if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x - DUP(src1_vec.S4(), src1_vec.Selem()[0]); // src1_vec = src1_vec.xxxx + if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x + DUP(src1Vec.S4(), src1Vec.Selem()[0]); // src1Vec = src1Vec.xxxx } - storeRegister(src1_vec, shader, dest, operandDescriptor); + storeRegister(src1Vec, shader, dest, operandDescriptor); } void ShaderEmitter::recMAD(const PICAShader& shader, u32 instruction) { @@ -658,17 +870,17 @@ void ShaderEmitter::recMAD(const PICAShader& shader, u32 instruction) { const u32 idx = getBits<22, 2>(instruction); const u32 dest = getBits<24, 5>(instruction); - loadRegister<1>(src1_vec, shader, src1, 0, operandDescriptor); - loadRegister<2>(src2_vec, shader, src2, isMADI ? 0 : idx, operandDescriptor); - loadRegister<3>(src3_vec, shader, src3, isMADI ? idx : 0, operandDescriptor); + loadRegister<1>(src1Vec, shader, src1, 0, operandDescriptor); + loadRegister<2>(src2Vec, shader, src2, isMADI ? 0 : idx, operandDescriptor); + loadRegister<3>(src3Vec, shader, src3, isMADI ? idx : 0, operandDescriptor); if constexpr (useSafeMUL) { - emitSafeMUL(src1_vec, src2_vec, scratch1); - FADD(src3_vec.S4(), src3_vec.S4(), src1_vec.S4()); + emitSafeMUL(src1Vec, src2Vec, scratch1Vec); + FADD(src3Vec.S4(), src3Vec.S4(), src1Vec.S4()); } else { - FMLA(src3_vec.S4(), src1_vec.S4(), src2_vec.S4()); + FMLA(src3Vec.S4(), src1Vec.S4(), src2Vec.S4()); } - storeRegister(src3_vec, shader, dest, operandDescriptor); + storeRegister(src3Vec, shader, dest, operandDescriptor); } void ShaderEmitter::recSLT(const PICAShader& shader, u32 instruction) { @@ -680,13 +892,13 @@ void ShaderEmitter::recSLT(const PICAShader& shader, u32 instruction) { const u32 idx = getBits<19, 2>(instruction); const u32 dest = getBits<21, 5>(instruction); - loadRegister<1>(src1_vec, shader, src1, isSLTI ? 0 : idx, operandDescriptor); - loadRegister<2>(src2_vec, shader, src2, isSLTI ? idx : 0, operandDescriptor); - // Set each lane of SRC1 to FFFFFFFF if src2 > src1, else to 0. NEON does not have FCMLT so we use FCMGT with inverted operands + loadRegister<1>(src1Vec, shader, src1, isSLTI ? 0 : idx, operandDescriptor); + loadRegister<2>(src2Vec, shader, src2, isSLTI ? idx : 0, operandDescriptor); + // Set each lane of src1Vec to FFFFFFFF if src2 > src1, else to 0. NEON does not have FCMLT so we use FCMGT with inverted operands // This is more or less a direct port of the relevant x64 JIT code - FCMGT(src1_vec.S4(), src2_vec.S4(), src1_vec.S4()); - AND(src1_vec.B16(), src1_vec.B16(), onesVector.B16()); // AND with vec4(1.0) to convert the FFFFFFFF lanes into 1.0 - storeRegister(src1_vec, shader, dest, operandDescriptor); + FCMGT(src1Vec.S4(), src2Vec.S4(), src1Vec.S4()); + AND(src1Vec.B16(), src1Vec.B16(), onesVector.B16()); // AND with vec4(1.0) to convert the FFFFFFFF lanes into 1.0 + storeRegister(src1Vec, shader, dest, operandDescriptor); } void ShaderEmitter::recSGE(const PICAShader& shader, u32 instruction) { @@ -698,13 +910,13 @@ void ShaderEmitter::recSGE(const PICAShader& shader, u32 instruction) { const u32 idx = getBits<19, 2>(instruction); const u32 dest = getBits<21, 5>(instruction); - loadRegister<1>(src1_vec, shader, src1, isSGEI ? 0 : idx, operandDescriptor); - loadRegister<2>(src2_vec, shader, src2, isSGEI ? idx : 0, operandDescriptor); - // Set each lane of SRC1 to FFFFFFFF if src1 >= src2, else to 0. + loadRegister<1>(src1Vec, shader, src1, isSGEI ? 0 : idx, operandDescriptor); + loadRegister<2>(src2Vec, shader, src2, isSGEI ? idx : 0, operandDescriptor); + // Set each lane of src1Vec to FFFFFFFF if src1 >= src2, else to 0. // This is more or less a direct port of the relevant x64 JIT code - FCMGE(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); - AND(src1_vec.B16(), src1_vec.B16(), onesVector.B16()); // AND with vec4(1.0) to convert the FFFFFFFF lanes into 1.0 - storeRegister(src1_vec, shader, dest, operandDescriptor); + FCMGE(src1Vec.S4(), src1Vec.S4(), src2Vec.S4()); + AND(src1Vec.B16(), src1Vec.B16(), onesVector.B16()); // AND with vec4(1.0) to convert the FFFFFFFF lanes into 1.0 + storeRegister(src1Vec, shader, dest, operandDescriptor); } void ShaderEmitter::recCMP(const PICAShader& shader, u32 instruction) { @@ -715,8 +927,8 @@ void ShaderEmitter::recCMP(const PICAShader& shader, u32 instruction) { const u32 cmpY = getBits<21, 3>(instruction); const u32 cmpX = getBits<24, 3>(instruction); - loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); - loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); + loadRegister<1>(src1Vec, shader, src1, idx, operandDescriptor); + loadRegister<2>(src2Vec, shader, src2, 0, operandDescriptor); // Map from PICA condition codes (used as index) to x86 condition codes // We treat invalid condition codes as "always" as suggested by 3DBrew @@ -729,13 +941,13 @@ void ShaderEmitter::recCMP(const PICAShader& shader, u32 instruction) { const size_t cmpRegXOffset = uintptr_t(&shader.cmpRegister[0]) - uintptr_t(&shader); // NEON doesn't have SIMD comparisons to do fun stuff with like on x64 - FCMP(src1_vec.toS(), src2_vec.toS()); + FCMP(src1Vec.toS(), src2Vec.toS()); CSET(W0, conditionCodes[cmpX]); // Compare Y components, which annoyingly enough can't be done without moving - MOV(scratch1.toS(), src1_vec.Selem()[1]); - MOV(scratch2.toS(), src2_vec.Selem()[1]); - FCMP(scratch1.toS(), scratch2.toS()); + MOV(scratch1Vec.toS(), src1Vec.Selem()[1]); + MOV(scratch2Vec.toS(), src2Vec.Selem()[1]); + FCMP(scratch1Vec.toS(), scratch2Vec.toS()); CSET(W1, conditionCodes[cmpY]); // Merge the booleans and write them back in one STRh @@ -915,6 +1127,19 @@ void ShaderEmitter::recJMPU(const PICAShader& shader, u32 instruction) { } } +void ShaderEmitter::recLG2(const PICAShader& shader, u32 instruction) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src = getBits<12, 7>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + + loadRegister<1>(src1Vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1Vec + STR(X30, SP, POST_INDEXED, -16); + BL(log2Func); + LDR(X30, SP, PRE_INDEXED, 16); + storeRegister(src1Vec, shader, dest, operandDescriptor); +} + void ShaderEmitter::recLOOP(const PICAShader& shader, u32 instruction) { const u32 dest = getBits<10, 12>(instruction); const u32 uniformIndex = getBits<22, 2>(instruction); @@ -979,4 +1204,17 @@ void ShaderEmitter::recEND(const PICAShader& shader, u32 instruction) { RET(); } +void ShaderEmitter::recEX2(const PICAShader& shader, u32 instruction) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src = getBits<12, 7>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + + loadRegister<1>(src1Vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1Vec + STR(X30, SP, POST_INDEXED, -16); + BL(exp2Func); + LDR(X30, SP, PRE_INDEXED, 16); + storeRegister(src1Vec, shader, dest, operandDescriptor); +} + #endif From 3270cfe602dd0cb1ec47a777c49ac2ea239f88bf Mon Sep 17 00:00:00 2001 From: Paris Oplopoios Date: Thu, 21 Mar 2024 15:54:18 +0200 Subject: [PATCH 12/60] First step towards configurable keyboard mappings (#464) * Configurable keyboard mappings * Cleanup * Cleanup * Biggest mistake of my career * format * Fix naming convention --------- Co-authored-by: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> --- CMakeLists.txt | 6 +- include/input_mappings.hpp | 22 +++++ include/panda_qt/main_window.hpp | 4 +- include/panda_sdl/frontend_sdl.hpp | 4 + include/services/hid.hpp | 1 + src/panda_qt/main_window.cpp | 71 ++++++--------- src/panda_qt/mappings.cpp | 25 ++++++ src/panda_sdl/frontend_sdl.cpp | 134 ++++++++++++----------------- src/panda_sdl/mappings.cpp | 25 ++++++ 9 files changed, 164 insertions(+), 128 deletions(-) create mode 100644 include/input_mappings.hpp create mode 100644 src/panda_qt/mappings.cpp create mode 100644 src/panda_sdl/mappings.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ca0bcdd2e..dc230bf66 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -201,7 +201,7 @@ set(AUDIO_SOURCE_FILES src/core/audio/dsp_core.cpp src/core/audio/null_core.cpp ) set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) -set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp +set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/input_mappings.hpp include/cpu.hpp include/cpu_dynarmic.hpp include/memory.hpp include/renderer.hpp include/kernel/kernel.hpp include/dynarmic_cp15.hpp include/kernel/resource_limits.hpp include/kernel/kernel_types.hpp include/kernel/config_mem.hpp include/services/service_manager.hpp include/services/apt.hpp @@ -439,7 +439,7 @@ if(NOT BUILD_HYDRA_CORE) endif() set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp - src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp + src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp src/panda_qt/mappings.cpp ) set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp @@ -478,7 +478,7 @@ if(NOT BUILD_HYDRA_CORE) docs/img/rsob_icon.png docs/img/rstarstruck_icon.png ) else() - set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp) + set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp src/panda_sdl/mappings.cpp) set(FRONTEND_HEADER_FILES "") endif() diff --git a/include/input_mappings.hpp b/include/input_mappings.hpp new file mode 100644 index 000000000..177f1d51a --- /dev/null +++ b/include/input_mappings.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "helpers.hpp" +#include "services/hid.hpp" + +struct InputMappings { + using Scancode = u32; + using Container = std::unordered_map; + + u32 getMapping(Scancode scancode) const { + auto it = container.find(scancode); + return it != container.end() ? it->second : HID::Keys::Null; + } + + void setMapping(Scancode scancode, u32 key) { container[scancode] = key; } + static InputMappings defaultKeyboardMappings(); + + private: + Container container; +}; diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index c2db9ac1a..c3f99c291 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -11,6 +11,7 @@ #include #include "emulator.hpp" +#include "input_mappings.hpp" #include "panda_qt/about_window.hpp" #include "panda_qt/config_window.hpp" #include "panda_qt/cheats_window.hpp" @@ -87,6 +88,7 @@ class MainWindow : public QMainWindow { std::mutex messageQueueMutex; std::vector messageQueue; + InputMappings keyboardMappings; ScreenWidget screen; AboutWindow* aboutWindow; ConfigWindow* configWindow; @@ -120,4 +122,4 @@ class MainWindow : public QMainWindow { void loadLuaScript(const std::string& code); void editCheat(u32 handle, const std::vector& cheat, const std::function& callback); -}; \ No newline at end of file +}; diff --git a/include/panda_sdl/frontend_sdl.hpp b/include/panda_sdl/frontend_sdl.hpp index 2d206175c..dd6ab6c05 100644 --- a/include/panda_sdl/frontend_sdl.hpp +++ b/include/panda_sdl/frontend_sdl.hpp @@ -5,6 +5,7 @@ #include #include "emulator.hpp" +#include "input_mappings.hpp" class FrontendSDL { Emulator emu; @@ -16,9 +17,12 @@ class FrontendSDL { FrontendSDL(); bool loadROM(const std::filesystem::path& path); void run(); + u32 getMapping(InputMappings::Scancode scancode) { return keyboardMappings.getMapping(scancode); } SDL_Window* window = nullptr; SDL_GameController* gameController = nullptr; + InputMappings keyboardMappings; + int gameControllerID; bool programRunning = true; diff --git a/include/services/hid.hpp b/include/services/hid.hpp index febd7bd68..d9018a4fd 100644 --- a/include/services/hid.hpp +++ b/include/services/hid.hpp @@ -10,6 +10,7 @@ namespace HID::Keys { enum : u32 { + Null = 0, A = 1 << 0, B = 1 << 1, Select = 1 << 2, diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index dff4c1713..17f9ff262 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -8,8 +8,9 @@ #include #include "cheats.hpp" +#include "input_mappings.hpp" -MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), screen(this) { +MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), keyboardMappings(InputMappings::defaultKeyboardMappings()), screen(this) { setWindowTitle("Alber"); // Enable drop events for loading ROMs setAcceptDrops(true); @@ -298,29 +299,21 @@ void MainWindow::keyPressEvent(QKeyEvent* event) { sendMessage(message); }; - switch (event->key()) { - case Qt::Key_L: pressKey(HID::Keys::A); break; - case Qt::Key_K: pressKey(HID::Keys::B); break; - case Qt::Key_O: pressKey(HID::Keys::X); break; - case Qt::Key_I: pressKey(HID::Keys::Y); break; - - case Qt::Key_Q: pressKey(HID::Keys::L); break; - case Qt::Key_P: pressKey(HID::Keys::R); break; - - case Qt::Key_W: setCirclePad(MessageType::SetCirclePadY, 0x9C); break; - case Qt::Key_A: setCirclePad(MessageType::SetCirclePadX, -0x9C); break; - case Qt::Key_S: setCirclePad(MessageType::SetCirclePadY, -0x9C); break; - case Qt::Key_D: setCirclePad(MessageType::SetCirclePadX, 0x9C); break; - - case Qt::Key_Right: pressKey(HID::Keys::Right); break; - case Qt::Key_Left: pressKey(HID::Keys::Left); break; - case Qt::Key_Up: pressKey(HID::Keys::Up); break; - case Qt::Key_Down: pressKey(HID::Keys::Down); break; - - case Qt::Key_Return: pressKey(HID::Keys::Start); break; - case Qt::Key_Backspace: pressKey(HID::Keys::Select); break; - case Qt::Key_F4: sendMessage(EmulatorMessage{.type = MessageType::TogglePause}); break; - case Qt::Key_F5: sendMessage(EmulatorMessage{.type = MessageType::Reset}); break; + u32 key = keyboardMappings.getMapping(event->key()); + if (key != HID::Keys::Null) { + switch (key) { + case HID::Keys::CirclePadUp: setCirclePad(MessageType::SetCirclePadY, 0x9C); break; + case HID::Keys::CirclePadDown: setCirclePad(MessageType::SetCirclePadY, -0x9C); break; + case HID::Keys::CirclePadLeft: setCirclePad(MessageType::SetCirclePadX, -0x9C); break; + case HID::Keys::CirclePadRight: setCirclePad(MessageType::SetCirclePadX, 0x9C); break; + + default: pressKey(key); break; + } + } else { + switch (event->key()) { + case Qt::Key_F4: sendMessage(EmulatorMessage{.type = MessageType::TogglePause}); break; + case Qt::Key_F5: sendMessage(EmulatorMessage{.type = MessageType::Reset}); break; + } } } @@ -337,28 +330,16 @@ void MainWindow::keyReleaseEvent(QKeyEvent* event) { sendMessage(message); }; - switch (event->key()) { - case Qt::Key_L: releaseKey(HID::Keys::A); break; - case Qt::Key_K: releaseKey(HID::Keys::B); break; - case Qt::Key_O: releaseKey(HID::Keys::X); break; - case Qt::Key_I: releaseKey(HID::Keys::Y); break; - - case Qt::Key_Q: releaseKey(HID::Keys::L); break; - case Qt::Key_P: releaseKey(HID::Keys::R); break; + u32 key = keyboardMappings.getMapping(event->key()); + if (key != HID::Keys::Null) { + switch (key) { + case HID::Keys::CirclePadUp: releaseCirclePad(MessageType::SetCirclePadY); break; + case HID::Keys::CirclePadDown: releaseCirclePad(MessageType::SetCirclePadY); break; + case HID::Keys::CirclePadLeft: releaseCirclePad(MessageType::SetCirclePadX); break; + case HID::Keys::CirclePadRight: releaseCirclePad(MessageType::SetCirclePadX); break; - case Qt::Key_W: - case Qt::Key_S: releaseCirclePad(MessageType::SetCirclePadY); break; - - case Qt::Key_A: - case Qt::Key_D: releaseCirclePad(MessageType::SetCirclePadX); break; - - case Qt::Key_Right: releaseKey(HID::Keys::Right); break; - case Qt::Key_Left: releaseKey(HID::Keys::Left); break; - case Qt::Key_Up: releaseKey(HID::Keys::Up); break; - case Qt::Key_Down: releaseKey(HID::Keys::Down); break; - - case Qt::Key_Return: releaseKey(HID::Keys::Start); break; - case Qt::Key_Backspace: releaseKey(HID::Keys::Select); break; + default: releaseKey(key); break; + } } } diff --git a/src/panda_qt/mappings.cpp b/src/panda_qt/mappings.cpp new file mode 100644 index 000000000..22741a739 --- /dev/null +++ b/src/panda_qt/mappings.cpp @@ -0,0 +1,25 @@ +#include "input_mappings.hpp" + +#include + +InputMappings InputMappings::defaultKeyboardMappings() { + InputMappings mappings; + mappings.setMapping(Qt::Key_L, HID::Keys::A); + mappings.setMapping(Qt::Key_K, HID::Keys::B); + mappings.setMapping(Qt::Key_O, HID::Keys::X); + mappings.setMapping(Qt::Key_I, HID::Keys::Y); + mappings.setMapping(Qt::Key_Q, HID::Keys::L); + mappings.setMapping(Qt::Key_P, HID::Keys::R); + mappings.setMapping(Qt::Key_Up, HID::Keys::Up); + mappings.setMapping(Qt::Key_Down, HID::Keys::Down); + mappings.setMapping(Qt::Key_Right, HID::Keys::Right); + mappings.setMapping(Qt::Key_Left, HID::Keys::Left); + mappings.setMapping(Qt::Key_Return, HID::Keys::Start); + mappings.setMapping(Qt::Key_Backspace, HID::Keys::Select); + mappings.setMapping(Qt::Key_W, HID::Keys::CirclePadUp); + mappings.setMapping(Qt::Key_S, HID::Keys::CirclePadDown); + mappings.setMapping(Qt::Key_D, HID::Keys::CirclePadRight); + mappings.setMapping(Qt::Key_A, HID::Keys::CirclePadLeft); + + return mappings; +} \ No newline at end of file diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index b6486b45c..f94f98f44 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -2,7 +2,7 @@ #include -FrontendSDL::FrontendSDL() { +FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMappings()) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) { Helpers::panic("Failed to initialize SDL2"); } @@ -92,95 +92,71 @@ void FrontendSDL::run() { programRunning = false; return; - case SDL_KEYDOWN: + case SDL_KEYDOWN: { if (emu.romType == ROMType::None) break; - switch (event.key.keysym.sym) { - case SDLK_l: hid.pressKey(Keys::A); break; - case SDLK_k: hid.pressKey(Keys::B); break; - case SDLK_o: hid.pressKey(Keys::X); break; - case SDLK_i: hid.pressKey(Keys::Y); break; - - case SDLK_q: hid.pressKey(Keys::L); break; - case SDLK_p: hid.pressKey(Keys::R); break; - - case SDLK_RIGHT: hid.pressKey(Keys::Right); break; - case SDLK_LEFT: hid.pressKey(Keys::Left); break; - case SDLK_UP: hid.pressKey(Keys::Up); break; - case SDLK_DOWN: hid.pressKey(Keys::Down); break; - - case SDLK_w: - hid.setCirclepadY(0x9C); - keyboardAnalogY = true; - break; - - case SDLK_a: - hid.setCirclepadX(-0x9C); - keyboardAnalogX = true; - break; - - case SDLK_s: - hid.setCirclepadY(-0x9C); - keyboardAnalogY = true; - break; - - case SDLK_d: - hid.setCirclepadX(0x9C); - keyboardAnalogX = true; - break; - - case SDLK_RETURN: hid.pressKey(Keys::Start); break; - case SDLK_BACKSPACE: hid.pressKey(Keys::Select); break; - - // Use the F4 button as a hot-key to pause or resume the emulator - // We can't use the audio play/pause buttons because it's annoying - case SDLK_F4: { - emu.togglePause(); - break; + u32 key = getMapping(event.key.keysym.scancode); + if (key != HID::Keys::Null) { + switch (key) { + case HID::Keys::CirclePadRight: + hid.setCirclepadX(0x9C); + keyboardAnalogX = true; + break; + case HID::Keys::CirclePadLeft: + hid.setCirclepadX(-0x9C); + keyboardAnalogX = true; + break; + case HID::Keys::CirclePadUp: + hid.setCirclepadY(0x9C); + keyboardAnalogY = true; + break; + case HID::Keys::CirclePadDown: + hid.setCirclepadY(-0x9C); + keyboardAnalogY = true; + break; + default: hid.pressKey(key); break; } - - // Use F5 as a reset button - case SDLK_F5: { - emu.reset(Emulator::ReloadOption::Reload); - break; + } else { + switch (event.key.keysym.sym) { + // Use the F4 button as a hot-key to pause or resume the emulator + // We can't use the audio play/pause buttons because it's annoying + case SDLK_F4: { + emu.togglePause(); + break; + } + + // Use F5 as a reset button + case SDLK_F5: { + emu.reset(Emulator::ReloadOption::Reload); + break; + } } } break; + } - case SDL_KEYUP: + case SDL_KEYUP: { if (emu.romType == ROMType::None) break; - switch (event.key.keysym.sym) { - case SDLK_l: hid.releaseKey(Keys::A); break; - case SDLK_k: hid.releaseKey(Keys::B); break; - case SDLK_o: hid.releaseKey(Keys::X); break; - case SDLK_i: hid.releaseKey(Keys::Y); break; - - case SDLK_q: hid.releaseKey(Keys::L); break; - case SDLK_p: hid.releaseKey(Keys::R); break; - - case SDLK_RIGHT: hid.releaseKey(Keys::Right); break; - case SDLK_LEFT: hid.releaseKey(Keys::Left); break; - case SDLK_UP: hid.releaseKey(Keys::Up); break; - case SDLK_DOWN: hid.releaseKey(Keys::Down); break; - - // Err this is probably not ideal - case SDLK_w: - case SDLK_s: - hid.setCirclepadY(0); - keyboardAnalogY = false; - break; - - case SDLK_a: - case SDLK_d: - hid.setCirclepadX(0); - keyboardAnalogX = false; - break; - - case SDLK_RETURN: hid.releaseKey(Keys::Start); break; - case SDLK_BACKSPACE: hid.releaseKey(Keys::Select); break; + u32 key = getMapping(event.key.keysym.scancode); + if (key != HID::Keys::Null) { + switch (key) { + // Err this is probably not ideal + case HID::Keys::CirclePadRight: + case HID::Keys::CirclePadLeft: + hid.setCirclepadX(0); + keyboardAnalogX = false; + break; + case HID::Keys::CirclePadUp: + case HID::Keys::CirclePadDown: + hid.setCirclepadY(0); + keyboardAnalogY = false; + break; + default: hid.releaseKey(key); break; + } } break; + } case SDL_MOUSEBUTTONDOWN: if (emu.romType == ROMType::None) break; diff --git a/src/panda_sdl/mappings.cpp b/src/panda_sdl/mappings.cpp new file mode 100644 index 000000000..0c09b8527 --- /dev/null +++ b/src/panda_sdl/mappings.cpp @@ -0,0 +1,25 @@ +#include "input_mappings.hpp" + +#include + +InputMappings InputMappings::defaultKeyboardMappings() { + InputMappings mappings; + mappings.setMapping(SDLK_l, HID::Keys::A); + mappings.setMapping(SDLK_k, HID::Keys::B); + mappings.setMapping(SDLK_o, HID::Keys::X); + mappings.setMapping(SDLK_i, HID::Keys::Y); + mappings.setMapping(SDLK_q, HID::Keys::L); + mappings.setMapping(SDLK_p, HID::Keys::R); + mappings.setMapping(SDLK_UP, HID::Keys::Up); + mappings.setMapping(SDLK_DOWN, HID::Keys::Down); + mappings.setMapping(SDLK_RIGHT, HID::Keys::Right); + mappings.setMapping(SDLK_LEFT, HID::Keys::Left); + mappings.setMapping(SDLK_RETURN, HID::Keys::Start); + mappings.setMapping(SDLK_BACKSPACE, HID::Keys::Select); + mappings.setMapping(SDLK_w, HID::Keys::CirclePadUp); + mappings.setMapping(SDLK_s, HID::Keys::CirclePadDown); + mappings.setMapping(SDLK_d, HID::Keys::CirclePadRight); + mappings.setMapping(SDLK_a, HID::Keys::CirclePadLeft); + + return mappings; +} \ No newline at end of file From 5284109fd4f0ba32e21d29fa89ba1333735e6e17 Mon Sep 17 00:00:00 2001 From: Wunk Date: Fri, 22 Mar 2024 09:48:03 -0700 Subject: [PATCH 13/60] Implement shader-interpreter relative `MOVA` addressing (#471) * Add shader uniform-read unit test * Add unit test f24 vector formatter * Add Address Register Offset shader unit test * Implement float-uniform out-of-bound return value In the case that the resulting float-uniform index is greater than the 96 slots that it has, a result of `{1,1,1,1}` is to be returned. * Implement shader relative addressing Fails on the negative unit tests at the moment but passes all of the others. * Fix `MOVA` source register indexing --- src/core/PICA/shader_interpreter.cpp | 33 ++++++---- tests/shader.cpp | 98 +++++++++++++++++++++++++++- 2 files changed, 118 insertions(+), 13 deletions(-) diff --git a/src/core/PICA/shader_interpreter.cpp b/src/core/PICA/shader_interpreter.cpp index 5ed00b633..003ef97ac 100644 --- a/src/core/PICA/shader_interpreter.cpp +++ b/src/core/PICA/shader_interpreter.cpp @@ -119,9 +119,17 @@ u8 PICAShader::getIndexedSource(u32 source, u32 index) { return source; switch (index) { - case 0: [[likely]] return u8(source); // No offset applied - case 1: return u8(source + addrRegister[0]); - case 2: return u8(source + addrRegister[1]); + // No offset applied + case 0: [[likely]] return u8(source); + // Address register + case 1: + case 2: { + const s32 offset = addrRegister[index - 1]; + if (offset < -128 || offset > 127) [[unlikely]] { + return u8(source); + } + return u8(source + offset); + } case 3: return u8(source + loopCounter); } @@ -130,15 +138,16 @@ u8 PICAShader::getIndexedSource(u32 source, u32 index) { } PICAShader::vec4f PICAShader::getSource(u32 source) { - if (source < 0x10) + if (source < 0x10) { return inputs[source]; - else if (source < 0x20) + } else if (source < 0x20) { return tempRegisters[source - 0x10]; - else if (source <= 0x7f) - return floatUniforms[source - 0x20]; - else { - Helpers::warn("[PICA] Unimplemented source value: %X\n", source); - return vec4f({f24::zero(), f24::zero(), f24::zero(), f24::zero()}); + } else { + const usize floatIndex = (source - 0x20) & 0x7f; + if (floatIndex >= 96) [[unlikely]] { + return vec4f({f24::fromFloat32(1.0f), f24::fromFloat32(1.0f), f24::fromFloat32(1.0f), f24::fromFloat32(1.0f)}); + } + return floatUniforms[floatIndex]; } } @@ -300,10 +309,10 @@ void PICAShader::mov(u32 instruction) { void PICAShader::mova(u32 instruction) { const u32 operandDescriptor = operandDescriptors[instruction & 0x7f]; - const u32 src = getBits<12, 7>(instruction); + u32 src = getBits<12, 7>(instruction); const u32 idx = getBits<19, 2>(instruction); - if (idx) Helpers::panic("[PICA] MOVA: idx != 0"); + src = getIndexedSource(src, idx); vec4f srcVector = getSourceSwizzled<1>(src, operandDescriptor); u32 componentMask = operandDescriptor & 0xf; diff --git a/tests/shader.cpp b/tests/shader.cpp index 29c07481c..6b0dece85 100644 --- a/tests/shader.cpp +++ b/tests/shader.cpp @@ -14,6 +14,13 @@ static const nihstro::SourceRegister input0 = nihstro::SourceRegister::MakeInput static const nihstro::SourceRegister input1 = nihstro::SourceRegister::MakeInput(1); static const nihstro::DestRegister output0 = nihstro::DestRegister::MakeOutput(0); +static const std::array vectorOnes = { + Floats::f24::fromFloat32(1.0f), + Floats::f24::fromFloat32(1.0f), + Floats::f24::fromFloat32(1.0f), + Floats::f24::fromFloat32(1.0f), +}; + static std::unique_ptr assembleVertexShader(std::initializer_list code) { const auto shaderBinary = nihstro::InlineAsm::CompileToRawBinary(code); auto newShader = std::make_unique(ShaderType::Vertex); @@ -66,7 +73,7 @@ class ShaderInterpreterTest { [[nodiscard]] std::array, 96>& floatUniforms() const { return shader->floatUniforms; } [[nodiscard]] std::array, 4>& intUniforms() const { return shader->intUniforms; } - [[nodiscard]] u32& boolUniform() const { return shader->boolUniform; } + [[nodiscard]] u32& boolUniforms() const { return shader->boolUniform; } static std::unique_ptr assembleTest(std::initializer_list code) { return std::make_unique(code); @@ -92,6 +99,15 @@ class ShaderJITTest final : public ShaderInterpreterTest { #define SHADER_TEST_CASE(NAME, TAG) TEMPLATE_TEST_CASE(NAME, TAG, ShaderInterpreterTest) #endif +namespace Catch { + template <> + struct StringMaker> { + static std::string convert(std::array value) { + return std::format("({}, {}, {}, {})", value[0].toFloat32(), value[1].toFloat32(), value[2].toFloat32(), value[3].toFloat32()); + } + }; +} // namespace Catch + SHADER_TEST_CASE("ADD", "[shader][vertex]") { const auto shader = TestType::assembleTest({ {nihstro::OpCode::Id::ADD, output0, input0, input1}, @@ -268,4 +284,84 @@ SHADER_TEST_CASE("FLR", "[shader][vertex]") { REQUIRE(shader->runScalar({-1.5}) == -2.0f); REQUIRE(std::isnan(shader->runScalar({NAN}))); REQUIRE(std::isinf(shader->runScalar({INFINITY}))); +} + +SHADER_TEST_CASE("Uniform Read", "[shader][vertex][uniform]") { + const auto constant0 = nihstro::SourceRegister::MakeFloat(0); + auto shader = TestType::assembleTest({ + {nihstro::OpCode::Id::MOVA, nihstro::DestRegister{}, "x", input0, "x", nihstro::SourceRegister{}, "", nihstro::InlineAsm::RelativeAddress::A1 + }, + {nihstro::OpCode::Id::MOV, output0, "xyzw", constant0, "xyzw", nihstro::SourceRegister{}, "", nihstro::InlineAsm::RelativeAddress::A1}, + {nihstro::OpCode::Id::END}, + }); + + // Generate float uniforms + std::array, 96> floatUniforms = {}; + for (u32 i = 0; i < 96; ++i) { + const float color = (i * 2.0f) / 255.0f; + const Floats::f24 color24 = Floats::f24::fromFloat32(color); + const std::array testValue = {color24, color24, color24, Floats::f24::fromFloat32(1.0f)}; + shader->floatUniforms()[i] = testValue; + floatUniforms[i] = testValue; + } + + for (u32 i = 0; i < 96; ++i) { + const float index = static_cast(i); + // Intentionally use some fractional values to verify float->integer + // truncation during address translation + const float fractional = (i % 17) / 17.0f; + + REQUIRE(shader->runVector({index + fractional}) == floatUniforms[i]); + } +} + +SHADER_TEST_CASE("Address Register Offset", "[video_core][shader][shader_jit]") { + const auto constant40 = nihstro::SourceRegister::MakeFloat(40); + auto shader = TestType::assembleTest({ + // mova a0.x, sh_input.x + {nihstro::OpCode::Id::MOVA, nihstro::DestRegister{}, "x", input0, "x", nihstro::SourceRegister{}, "", nihstro::InlineAsm::RelativeAddress::A1 + }, + // mov sh_output.xyzw, c40[a0.x].xyzw + {nihstro::OpCode::Id::MOV, output0, "xyzw", constant40, "xyzw", nihstro::SourceRegister{}, "", nihstro::InlineAsm::RelativeAddress::A1}, + {nihstro::OpCode::Id::END}, + }); + + // Generate uniforms + const bool inverted = true; + std::array, 96> floatUniforms = {}; + for (u8 i = 0; i < 0x80; i++) { + // Float uniforms + if (i >= 0x00 && i < 0x60) { + const u32 base = inverted ? (0x60 - i) : i; + const auto color = (base * 2.f) / 255.0f; + const auto color24 = Floats::f24::fromFloat32(color); + const std::array testValue = {color24, color24, color24, Floats::f24::fromFloat32(1.0f)}; + shader->floatUniforms()[i] = testValue; + floatUniforms[i] = testValue; + } + // Integer uniforms + else if (i >= 0x60 && i < 0x64) { + const u8 color = static_cast((i - 0x60) * 0x10); + shader->intUniforms()[i - 0x60] = {color, color, color, 255}; + } + // Bool uniforms(bools packed into an integer) + else if (i >= 0x70 && i < 0x80) { + shader->boolUniforms() |= (i >= 0x78) << (i - 0x70); + } + } + + REQUIRE(shader->runVector({0.f}) == floatUniforms[40]); + REQUIRE(shader->runVector({13.f}) == floatUniforms[53]); + REQUIRE(shader->runVector({50.f}) == floatUniforms[90]); + REQUIRE(shader->runVector({60.f}) == vectorOnes); + REQUIRE(shader->runVector({74.f}) == vectorOnes); + REQUIRE(shader->runVector({87.f}) == vectorOnes); + REQUIRE(shader->runVector({88.f}) == floatUniforms[0]); + REQUIRE(shader->runVector({128.f}) == floatUniforms[40]); + REQUIRE(shader->runVector({-40.f}) == floatUniforms[0]); + REQUIRE(shader->runVector({-42.f}) == vectorOnes); + REQUIRE(shader->runVector({-70.f}) == vectorOnes); + REQUIRE(shader->runVector({-73.f}) == floatUniforms[95]); + REQUIRE(shader->runVector({-127.f}) == floatUniforms[41]); + REQUIRE(shader->runVector({-129.f}) == floatUniforms[40]); } \ No newline at end of file From 429dc2a94474eb15304c45938dcfb3ad72db4b51 Mon Sep 17 00:00:00 2001 From: Auxy6858 <71662994+Auxy6858@users.noreply.github.com> Date: Tue, 26 Mar 2024 16:40:10 +0000 Subject: [PATCH 14/60] Added rom path (#474) * Added app icon to the window * Added Roms path Added an option to the config to set a folder that opens when selecting a game instead of having to navigate to the folder manually every time. * Clear up PR * Clear up PR --------- Co-authored-by: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> --- include/config.hpp | 4 +++- src/config.cpp | 2 ++ src/panda_qt/main_window.cpp | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/include/config.hpp b/include/config.hpp index 8c0d2e123..2333c6825 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -6,7 +6,7 @@ // Remember to initialize every field here to its default value otherwise bad things will happen struct EmulatorConfig { - // Only enable the shader JIT by default on platforms where it's completely tested + // Only enable the shader JIT by default on platforms where it's completely tested #ifdef PANDA3DS_X64_HOST static constexpr bool shaderJitDefault = true; #else @@ -29,6 +29,8 @@ struct EmulatorConfig { // Default to 3% battery to make users suffer int batteryPercentage = 3; + // Default ROM path to open in Qt and misc frontends + std::filesystem::path defaultRomPath = ""; std::filesystem::path filePath; EmulatorConfig(const std::filesystem::path& path); diff --git a/src/config.cpp b/src/config.cpp index 299ef5946..2f9b7e00c 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -40,6 +40,7 @@ void EmulatorConfig::load() { discordRpcEnabled = toml::find_or(general, "EnableDiscordRPC", false); usePortableBuild = toml::find_or(general, "UsePortableBuild", false); + defaultRomPath = toml::find_or(general, "DefaultRomPath", ""); } } @@ -120,6 +121,7 @@ void EmulatorConfig::save() { data["General"]["EnableDiscordRPC"] = discordRpcEnabled; data["General"]["UsePortableBuild"] = usePortableBuild; + data["General"]["DefaultRomPath"] = defaultRomPath.string(); data["GPU"]["EnableShaderJIT"] = shaderJitEnabled; data["GPU"]["Renderer"] = std::string(Renderer::typeToString(rendererType)); data["GPU"]["EnableVSync"] = vsyncEnabled; diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 17f9ff262..3ff1049cd 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -139,8 +139,10 @@ void MainWindow::swapEmuBuffer() { } void MainWindow::selectROM() { - auto path = - QFileDialog::getOpenFileName(this, tr("Select 3DS ROM to load"), "", tr("Nintendo 3DS ROMs (*.3ds *.cci *.cxi *.app *.3dsx *.elf *.axf)")); + auto path = QFileDialog::getOpenFileName( + this, tr("Select 3DS ROM to load"), QString::fromStdU16String(emu->getConfig().defaultRomPath.u16string()), + tr("Nintendo 3DS ROMs (*.3ds *.cci *.cxi *.app *.3dsx *.elf *.axf)") + ); if (!path.isEmpty()) { std::filesystem::path* p = new std::filesystem::path(path.toStdU16String()); From 3b9490e633782177355d3c73662c4eab18e607eb Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 27 Mar 2024 19:11:47 +0000 Subject: [PATCH 15/60] Add controller support to Qt (#475) * Add controllers to Qt Co-Authored-By: Nadia Holmquist Pedersen <893884+nadiaholmquist@users.noreply.github.com> * Remove debug logs * Bonk --------- Co-authored-by: Nadia Holmquist Pedersen <893884+nadiaholmquist@users.noreply.github.com> --- include/panda_qt/main_window.hpp | 16 ++++- src/panda_qt/main_window.cpp | 117 ++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index c3f99c291..208da2c37 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -13,8 +15,8 @@ #include "emulator.hpp" #include "input_mappings.hpp" #include "panda_qt/about_window.hpp" -#include "panda_qt/config_window.hpp" #include "panda_qt/cheats_window.hpp" +#include "panda_qt/config_window.hpp" #include "panda_qt/screen.hpp" #include "panda_qt/text_editor.hpp" #include "services/hid.hpp" @@ -96,6 +98,10 @@ class MainWindow : public QMainWindow { TextEditorWindow* luaEditor; QMenuBar* menuBar = nullptr; + // We use SDL's game controller API since it's the sanest API that supports as many controllers as possible + SDL_GameController* gameController = nullptr; + int gameControllerID = 0; + void swapEmuBuffer(); void emuThreadMainLoop(); void selectLuaFile(); @@ -104,6 +110,8 @@ class MainWindow : public QMainWindow { void openLuaEditor(); void openCheatsEditor(); void showAboutMenu(); + void initControllers(); + void pollControllers(); void sendMessage(const EmulatorMessage& message); void dispatchMessage(const EmulatorMessage& message); @@ -111,6 +119,12 @@ class MainWindow : public QMainWindow { bool usingGL = false; bool usingVk = false; + // Variables to keep track of whether the user is controlling the 3DS analog stick with their keyboard + // This is done so when a gamepad is connected, we won't automatically override the 3DS analog stick settings with the gamepad's state + // And so the user can still use the keyboard to control the analog + bool keyboardAnalogX = false; + bool keyboardAnalogY = false; + public: MainWindow(QApplication* app, QWidget* parent = nullptr); ~MainWindow(); diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 3ff1049cd..a4fc20f02 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -97,6 +97,8 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) Helpers::panic("Unsupported graphics backend for Qt frontend!"); } + // We have to initialize controllers on the same thread they'll be polled in + initControllers(); emuThreadMainLoop(); }); } @@ -117,6 +119,8 @@ void MainWindow::emuThreadMainLoop() { } emu->runFrame(); + pollControllers(); + if (emu->romType != ROMType::None) { emu->getServiceManager().getHID().updateInputs(emu->getTicks()); } @@ -279,8 +283,21 @@ void MainWindow::dispatchMessage(const EmulatorMessage& message) { case MessageType::Reset: emu->reset(Emulator::ReloadOption::Reload); break; case MessageType::PressKey: emu->getServiceManager().getHID().pressKey(message.key.key); break; case MessageType::ReleaseKey: emu->getServiceManager().getHID().releaseKey(message.key.key); break; - case MessageType::SetCirclePadX: emu->getServiceManager().getHID().setCirclepadX(message.circlepad.value); break; - case MessageType::SetCirclePadY: emu->getServiceManager().getHID().setCirclepadY(message.circlepad.value); break; + + // Track whether we're controlling the analog stick with our controller and update the CirclePad X/Y values in HID + // Controllers are polled on the emulator thread, so this message type is only used when the circlepad is changed via keyboard input + case MessageType::SetCirclePadX: { + keyboardAnalogX = message.circlepad.value != 0; + emu->getServiceManager().getHID().setCirclepadX(message.circlepad.value); + break; + } + + case MessageType::SetCirclePadY: { + keyboardAnalogY = message.circlepad.value != 0; + emu->getServiceManager().getHID().setCirclepadY(message.circlepad.value); + break; + } + case MessageType::PressTouchscreen: emu->getServiceManager().getHID().setTouchScreenPress(message.touchscreen.x, message.touchscreen.y); break; @@ -397,4 +414,100 @@ void MainWindow::editCheat(u32 handle, const std::vector& cheat, const message.cheat.c = c; sendMessage(message); +} + +void MainWindow::initControllers() { + // Make SDL use consistent positional button mapping + SDL_SetHint(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0"); + if (SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0) { + Helpers::warn("Failed to initialize SDL2 GameController: %s", SDL_GetError()); + return; + } + + if (SDL_WasInit(SDL_INIT_GAMECONTROLLER)) { + gameController = SDL_GameControllerOpen(0); + + if (gameController != nullptr) { + SDL_Joystick* stick = SDL_GameControllerGetJoystick(gameController); + gameControllerID = SDL_JoystickInstanceID(stick); + } + } +} + +void MainWindow::pollControllers() { + // Update circlepad if a controller is plugged in + if (gameController != nullptr) { + HIDService& hid = emu->getServiceManager().getHID(); + const s16 stickX = SDL_GameControllerGetAxis(gameController, SDL_CONTROLLER_AXIS_LEFTX); + const s16 stickY = SDL_GameControllerGetAxis(gameController, SDL_CONTROLLER_AXIS_LEFTY); + constexpr s16 deadzone = 3276; + constexpr s16 maxValue = 0x9C; + constexpr s16 div = 0x8000 / maxValue; + + // Avoid overriding the keyboard's circlepad input + if (std::abs(stickX) < deadzone && !keyboardAnalogX) { + hid.setCirclepadX(0); + } else { + hid.setCirclepadX(stickX / div); + } + + if (std::abs(stickY) < deadzone && !keyboardAnalogY) { + hid.setCirclepadY(0); + } else { + hid.setCirclepadY(-(stickY / div)); + } + } + + SDL_Event event; + while (SDL_PollEvent(&event)) { + HIDService& hid = emu->getServiceManager().getHID(); + using namespace HID; + + switch (event.type) { + case SDL_CONTROLLERDEVICEADDED: + if (gameController == nullptr) { + gameController = SDL_GameControllerOpen(event.cdevice.which); + gameControllerID = event.cdevice.which; + } + break; + + case SDL_CONTROLLERDEVICEREMOVED: + if (event.cdevice.which == gameControllerID) { + SDL_GameControllerClose(gameController); + gameController = nullptr; + gameControllerID = 0; + } + break; + + case SDL_CONTROLLERBUTTONUP: + case SDL_CONTROLLERBUTTONDOWN: { + if (emu->romType == ROMType::None) break; + u32 key = 0; + + switch (event.cbutton.button) { + case SDL_CONTROLLER_BUTTON_A: key = Keys::B; break; + case SDL_CONTROLLER_BUTTON_B: key = Keys::A; break; + case SDL_CONTROLLER_BUTTON_X: key = Keys::Y; break; + case SDL_CONTROLLER_BUTTON_Y: key = Keys::X; break; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: key = Keys::L; break; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: key = Keys::R; break; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: key = Keys::Left; break; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: key = Keys::Right; break; + case SDL_CONTROLLER_BUTTON_DPAD_UP: key = Keys::Up; break; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: key = Keys::Down; break; + case SDL_CONTROLLER_BUTTON_BACK: key = Keys::Select; break; + case SDL_CONTROLLER_BUTTON_START: key = Keys::Start; break; + } + + if (key != 0) { + if (event.cbutton.state == SDL_PRESSED) { + hid.pressKey(key); + } else { + hid.releaseKey(key); + } + } + break; + } + } + } } \ No newline at end of file From 35b15fdd485106195dcc4c994a3f17adf3c0ad6e Mon Sep 17 00:00:00 2001 From: Auxy6858 <71662994+Auxy6858@users.noreply.github.com> Date: Mon, 1 Apr 2024 23:21:36 +0100 Subject: [PATCH 16/60] Jelly + ice cream theme (#486) * Update config_window.cpp * Update config_window.hpp * Rename theme to Cream * Rename theme to cream harder --------- Co-authored-by: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> --- include/panda_qt/config_window.hpp | 3 ++- src/panda_qt/config_window.cpp | 25 ++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/include/panda_qt/config_window.hpp b/include/panda_qt/config_window.hpp index e91936c43..4a5238794 100644 --- a/include/panda_qt/config_window.hpp +++ b/include/panda_qt/config_window.hpp @@ -16,6 +16,7 @@ class ConfigWindow : public QDialog { Light = 1, Dark = 2, GreetingsCat = 3, + Cream = 4, }; Theme currentTheme; @@ -26,4 +27,4 @@ class ConfigWindow : public QDialog { public: ConfigWindow(QWidget* parent = nullptr); ~ConfigWindow(); -}; \ No newline at end of file +}; diff --git a/src/panda_qt/config_window.cpp b/src/panda_qt/config_window.cpp index 44debc324..75293742e 100644 --- a/src/panda_qt/config_window.cpp +++ b/src/panda_qt/config_window.cpp @@ -10,6 +10,7 @@ ConfigWindow::ConfigWindow(QWidget* parent) : QDialog(parent) { themeSelect->addItem(tr("Light")); themeSelect->addItem(tr("Dark")); themeSelect->addItem(tr("Greetings Cat")); + themeSelect->addItem(tr("Cream")); themeSelect->setCurrentIndex(static_cast(currentTheme)); themeSelect->setGeometry(40, 40, 100, 50); @@ -87,6 +88,28 @@ void ConfigWindow::setTheme(Theme theme) { break; } + case Theme::Cream: { + QApplication::setStyle(QStyleFactory::create("Fusion")); + + QPalette p; + p.setColor(QPalette::Window, QColor(255, 229, 180)); + p.setColor(QPalette::WindowText, QColor(33, 37, 41)); + p.setColor(QPalette::Base, QColor(255, 229, 180)); + p.setColor(QPalette::AlternateBase, QColor(255, 229, 180)); + p.setColor(QPalette::ToolTipBase, QColor(33, 37, 41)); + p.setColor(QPalette::ToolTipText, QColor(33, 37, 41)); + p.setColor(QPalette::Text, QColor(33, 37, 41)); + p.setColor(QPalette::Button, QColor(255, 229, 180)); + p.setColor(QPalette::ButtonText, QColor(33, 37, 41)); + p.setColor(QPalette::BrightText, QColor(217, 113, 103)); + p.setColor(QPalette::Link, QColor(248, 148, 150)); + + p.setColor(QPalette::Highlight, QColor(217, 113, 103)); + p.setColor(QPalette::HighlightedText, QColor(63, 33, 29)); + qApp->setPalette(p); + break; + } + case Theme::System: { qApp->setPalette(this->style()->standardPalette()); qApp->setStyle(QStyleFactory::create("WindowsVista")); @@ -96,4 +119,4 @@ void ConfigWindow::setTheme(Theme theme) { } } -ConfigWindow::~ConfigWindow() { delete themeSelect; } \ No newline at end of file +ConfigWindow::~ConfigWindow() { delete themeSelect; } From df3200a4652ba98b29442210b64ec99e94877c5b Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:54:26 +0000 Subject: [PATCH 17/60] Add Dolphin bitfield class (#487) * Add Dolphin bitfield class * Remove bitfield test --- CMakeLists.txt | 2 +- include/bitfield.hpp | 413 ++++++++++++++++++++++++++++++++++++++++++ src/panda_qt/main.cpp | 2 +- 3 files changed, 415 insertions(+), 2 deletions(-) create mode 100644 include/bitfield.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index dc230bf66..b7fc8518b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -233,7 +233,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/fs/archive_system_save_data.hpp include/lua_manager.hpp include/memory_mapped_file.hpp include/hydra_icon.hpp include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/scheduler.hpp include/applets/error_applet.hpp include/audio/dsp_core.hpp include/audio/null_core.hpp include/audio/teakra_core.hpp - include/audio/miniaudio_device.hpp include/ring_buffer.hpp + include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp ) cmrc_add_resource_library( diff --git a/include/bitfield.hpp b/include/bitfield.hpp new file mode 100644 index 000000000..d98b4e533 --- /dev/null +++ b/include/bitfield.hpp @@ -0,0 +1,413 @@ +// Copyright 2014 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// Copyright 2014 Tony Wasserka +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of the owner nor the names of its contributors may +// be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include +#include + +#include "compiler_builtins.hpp" +/* + * Abstract bitfield class + * + * Allows endianness-independent access to individual bitfields within some raw + * integer value. The assembly generated by this class is identical to the + * usage of raw bitfields, so it's a perfectly fine replacement. + * + * For BitField, X is the distance of the bitfield to the LSB of the + * raw value, Y is the length in bits of the bitfield. Z is an integer type + * which determines the sign of the bitfield. Z must have the same size as the + * raw integer. + * + * + * General usage: + * + * Create a new union with the raw integer value as a member. + * Then for each bitfield you want to expose, add a BitField member + * in the union. The template parameters are the bit offset and the number + * of desired bits. + * + * Changes in the bitfield members will then get reflected in the raw integer + * value and vice-versa. + * + * + * Sample usage: + * + * union SomeRegister + * { + * u32 hex; + * + * BitField<0,7,u32> first_seven_bits; // unsigned + * BitField<7,8,u32> next_eight_bits; // unsigned + * BitField<3,15,s32> some_signed_fields; // signed + * }; + * + * This is equivalent to the little-endian specific code: + * + * union SomeRegister + * { + * u32 hex; + * + * struct + * { + * u32 first_seven_bits : 7; + * u32 next_eight_bits : 8; + * }; + * struct + * { + * u32 : 3; // padding + * s32 some_signed_fields : 15; + * }; + * }; + * + * + * Caveats: + * + * 1) + * BitField provides automatic casting from and to the storage type where + * appropriate. However, when using non-typesafe functions like printf, an + * explicit cast must be performed on the BitField object to make sure it gets + * passed correctly, e.g.: + * printf("Value: %d", (s32)some_register.some_signed_fields); + * Note that this does not apply when using fmt, as a formatter is provided that + * handles this conversion automatically. + * + * 2) + * Not really a caveat, but potentially irritating: This class is used in some + * packed structures that do not guarantee proper alignment. Therefore we have + * to use #pragma pack here not to pack the members of the class, but instead + * to break GCC's assumption that the members of the class are aligned on + * sizeof(StorageType). + * TODO(neobrain): Confirm that this is a proper fix and not just masking + * symptoms. + */ +#pragma pack(1) +template < + std::size_t position, std::size_t bits, typename T, + // StorageType is T for non-enum types and the underlying type of T if + // T is an enumeration. Note that T is wrapped within an enable_if in the + // former case to workaround compile errors which arise when using + // std::underlying_type::type directly. + typename StorageType = typename std::conditional_t::value, std::underlying_type, std::enable_if>::type> +struct BitField { + private: + // This constructor might be considered ambiguous: + // Would it initialize the storage or just the bitfield? + // Hence, delete it. Use the assignment operator to set bitfield values! + BitField(T val) = delete; + + public: + // Force default constructor to be created + // so that we can use this within unions + constexpr BitField() = default; + + // We explicitly delete the copy assignment operator here, because the + // default copy assignment would copy the full storage value, rather than + // just the bits relevant to this particular bit field. + // Ideally, we would just implement the copy assignment to copy only the + // relevant bits, but we're prevented from doing that because the savestate + // code expects that this class is trivially copyable. + BitField& operator=(const BitField&) = delete; + + ALWAYS_INLINE BitField& operator=(T val) { + storage = (storage & ~GetMask()) | ((static_cast(val) << position) & GetMask()); + return *this; + } + + constexpr T Value() const { return Value(std::is_signed()); } + constexpr operator T() const { return Value(); } + static constexpr bool IsSigned() { return std::is_signed(); } + static constexpr std::size_t StartBit() { return position; } + static constexpr std::size_t NumBits() { return bits; } + + private: + // Unsigned version of StorageType + using StorageTypeU = std::make_unsigned_t; + + constexpr T Value(std::true_type) const { + const size_t shift_amount = 8 * sizeof(StorageType) - bits; + return static_cast((storage << (shift_amount - position)) >> shift_amount); + } + + constexpr T Value(std::false_type) const { return static_cast((storage & GetMask()) >> position); } + + static constexpr StorageType GetMask() { return (std::numeric_limits::max() >> (8 * sizeof(StorageType) - bits)) << position; } + + StorageType storage; + + static_assert(bits + position <= 8 * sizeof(StorageType), "Bitfield out of range"); + static_assert(sizeof(T) <= sizeof(StorageType), "T must fit in StorageType"); + + // And, you know, just in case people specify something stupid like bits=position=0x80000000 + static_assert(position < 8 * sizeof(StorageType), "Invalid position"); + static_assert(bits <= 8 * sizeof(T), "Invalid number of bits"); + static_assert(bits > 0, "Invalid number of bits"); +}; +#pragma pack() + +// Language limitations require the following to make these formattable +// (formatter::Ref> is not legal) +template +class BitFieldArrayConstRef; +template +class BitFieldArrayRef; +template +class BitFieldArrayConstIterator; +template +class BitFieldArrayIterator; + +#pragma pack(1) +template < + std::size_t position, std::size_t bits, std::size_t size, typename T, + // StorageType is T for non-enum types and the underlying type of T if + // T is an enumeration. Note that T is wrapped within an enable_if in the + // former case to workaround compile errors which arise when using + // std::underlying_type::type directly. + typename StorageType = typename std::conditional_t::value, std::underlying_type, std::enable_if>::type> +struct BitFieldArray { + using Ref = BitFieldArrayRef; + using ConstRef = BitFieldArrayConstRef; + using Iterator = BitFieldArrayIterator; + using ConstIterator = BitFieldArrayConstIterator; + + private: + // This constructor might be considered ambiguous: + // Would it initialize the storage or just the bitfield? + // Hence, delete it. Use the assignment operator to set bitfield values! + BitFieldArray(T val) = delete; + + public: + // Force default constructor to be created + // so that we can use this within unions + constexpr BitFieldArray() = default; + + // We explicitly delete the copy assignment operator here, because the + // default copy assignment would copy the full storage value, rather than + // just the bits relevant to this particular bit field. + // Ideally, we would just implement the copy assignment to copy only the + // relevant bits, but we're prevented from doing that because the savestate + // code expects that this class is trivially copyable. + BitFieldArray& operator=(const BitFieldArray&) = delete; + + public: + constexpr bool IsSigned() const { return std::is_signed(); } + constexpr std::size_t StartBit() const { return position; } + constexpr std::size_t NumBits() const { return bits; } + constexpr std::size_t Size() const { return size; } + constexpr std::size_t TotalNumBits() const { return bits * size; } + + constexpr T Value(size_t index) const { return Value(std::is_signed(), index); } + void SetValue(size_t index, T value) { + const size_t pos = position + bits * index; + storage = (storage & ~GetElementMask(index)) | ((static_cast(value) << pos) & GetElementMask(index)); + } + Ref operator[](size_t index) { return Ref(this, index); } + constexpr const ConstRef operator[](size_t index) const { return ConstRef(this, index); } + + constexpr Iterator begin() { return Iterator(this, 0); } + constexpr Iterator end() { return Iterator(this, size); } + constexpr ConstIterator begin() const { return ConstIterator(this, 0); } + constexpr ConstIterator end() const { return ConstIterator(this, size); } + constexpr ConstIterator cbegin() const { return begin(); } + constexpr ConstIterator cend() const { return end(); } + + private: + // Unsigned version of StorageType + using StorageTypeU = std::make_unsigned_t; + + constexpr T Value(std::true_type, size_t index) const { + const size_t pos = position + bits * index; + const size_t shift_amount = 8 * sizeof(StorageType) - bits; + return static_cast((storage << (shift_amount - pos)) >> shift_amount); + } + + constexpr T Value(std::false_type, size_t index) const { + const size_t pos = position + bits * index; + return static_cast((storage & GetElementMask(index)) >> pos); + } + + static constexpr StorageType GetElementMask(size_t index) { + const size_t pos = position + bits * index; + return (std::numeric_limits::max() >> (8 * sizeof(StorageType) - bits)) << pos; + } + + StorageType storage; + + static_assert(bits * size + position <= 8 * sizeof(StorageType), "Bitfield array out of range"); + static_assert(sizeof(T) <= sizeof(StorageType), "T must fit in StorageType"); + + // And, you know, just in case people specify something stupid like bits=position=0x80000000 + static_assert(position < 8 * sizeof(StorageType), "Invalid position"); + static_assert(bits <= 8 * sizeof(T), "Invalid number of bits"); + static_assert(bits > 0, "Invalid number of bits"); + static_assert(size <= 8 * sizeof(StorageType), "Invalid size"); + static_assert(size > 0, "Invalid size"); +}; +#pragma pack() + +template +class BitFieldArrayConstRef { + friend struct BitFieldArray; + friend class BitFieldArrayConstIterator; + + public: + constexpr T Value() const { return m_array->Value(m_index); }; + constexpr operator T() const { return Value(); } + + private: + constexpr BitFieldArrayConstRef(const BitFieldArray* array, size_t index) : m_array(array), m_index(index) {} + + const BitFieldArray* const m_array; + const size_t m_index; +}; + +template +class BitFieldArrayRef { + friend struct BitFieldArray; + friend class BitFieldArrayIterator; + + public: + constexpr T Value() const { return m_array->Value(m_index); }; + constexpr operator T() const { return Value(); } + T operator=(const BitFieldArrayRef& value) const { + m_array->SetValue(m_index, value); + return value; + } + T operator=(T value) const { + m_array->SetValue(m_index, value); + return value; + } + + private: + constexpr BitFieldArrayRef(BitFieldArray* array, size_t index) : m_array(array), m_index(index) {} + + BitFieldArray* const m_array; + const size_t m_index; +}; + +// Satisfies LegacyOutputIterator / std::output_iterator. +// Does not satisfy LegacyInputIterator / std::input_iterator as std::output_iterator_tag does not +// extend std::input_iterator_tag. +// Does not satisfy LegacyForwardIterator / std::forward_iterator, as that requires use of real +// references instead of proxy objects. +// This iterator allows use of BitFieldArray in range-based for loops, and with fmt::join. +template +class BitFieldArrayIterator { + friend struct BitFieldArray; + + public: + using iterator_category = std::output_iterator_tag; + using value_type = T; + using difference_type = ptrdiff_t; + using pointer = void; + using reference = BitFieldArrayRef; + + private: + constexpr BitFieldArrayIterator(BitFieldArray* array, size_t index) : m_array(array), m_index(index) {} + + public: + // Required by std::input_or_output_iterator + constexpr BitFieldArrayIterator() = default; + // Required by LegacyIterator + constexpr BitFieldArrayIterator(const BitFieldArrayIterator& other) = default; + // Required by LegacyIterator + BitFieldArrayIterator& operator=(const BitFieldArrayIterator& other) = default; + // Move constructor and assignment operators, explicitly defined for completeness + constexpr BitFieldArrayIterator(BitFieldArrayIterator&& other) = default; + BitFieldArrayIterator& operator=(BitFieldArrayIterator&& other) = default; + + public: + BitFieldArrayIterator& operator++() { + m_index++; + return *this; + } + BitFieldArrayIterator operator++(int) { + BitFieldArrayIterator other(*this); + ++*this; + return other; + } + constexpr reference operator*() const { return reference(m_array, m_index); } + constexpr bool operator==(BitFieldArrayIterator other) const { return m_index == other.m_index; } + constexpr bool operator!=(BitFieldArrayIterator other) const { return m_index != other.m_index; } + + private: + BitFieldArray* m_array; + size_t m_index; +}; + +// Satisfies LegacyInputIterator / std::input_iterator. +// Does not satisfy LegacyForwardIterator / std::forward_iterator, as that requires use of real +// references instead of proxy objects. +// This iterator allows use of BitFieldArray in range-based for loops, and with fmt::join. +template +class BitFieldArrayConstIterator { + friend struct BitFieldArray; + + public: + using iterator_category = std::input_iterator_tag; + using value_type = T; + using difference_type = ptrdiff_t; + using pointer = void; + using reference = BitFieldArrayConstRef; + + private: + constexpr BitFieldArrayConstIterator(const BitFieldArray* array, size_t index) : m_array(array), m_index(index) {} + + public: + // Required by std::input_or_output_iterator + constexpr BitFieldArrayConstIterator() = default; + // Required by LegacyIterator + constexpr BitFieldArrayConstIterator(const BitFieldArrayConstIterator& other) = default; + // Required by LegacyIterator + BitFieldArrayConstIterator& operator=(const BitFieldArrayConstIterator& other) = default; + // Move constructor and assignment operators, explicitly defined for completeness + constexpr BitFieldArrayConstIterator(BitFieldArrayConstIterator&& other) = default; + BitFieldArrayConstIterator& operator=(BitFieldArrayConstIterator&& other) = default; + + public: + BitFieldArrayConstIterator& operator++() { + m_index++; + return *this; + } + BitFieldArrayConstIterator operator++(int) { + BitFieldArrayConstIterator other(*this); + ++*this; + return other; + } + constexpr reference operator*() const { return reference(m_array, m_index); } + constexpr bool operator==(BitFieldArrayConstIterator other) const { return m_index == other.m_index; } + constexpr bool operator!=(BitFieldArrayConstIterator other) const { return m_index != other.m_index; } + + private: + const BitFieldArray* m_array; + size_t m_index; +}; diff --git a/src/panda_qt/main.cpp b/src/panda_qt/main.cpp index 56391e659..a7a6216c5 100644 --- a/src/panda_qt/main.cpp +++ b/src/panda_qt/main.cpp @@ -9,4 +9,4 @@ int main(int argc, char *argv[]) { window.show(); return app.exec(); -} \ No newline at end of file +} From 2b76f89b7b1425a03041b7d8bdd11f0a3e6b6b13 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 3 Apr 2024 21:47:13 +0300 Subject: [PATCH 18/60] Add DSP shared memory definitions --- CMakeLists.txt | 2 +- include/audio/dsp_shared_mem.hpp | 534 +++++++++++++++++++++++++++++++ 2 files changed, 535 insertions(+), 1 deletion(-) create mode 100644 include/audio/dsp_shared_mem.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b7fc8518b..4b763dfff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -233,7 +233,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/fs/archive_system_save_data.hpp include/lua_manager.hpp include/memory_mapped_file.hpp include/hydra_icon.hpp include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/scheduler.hpp include/applets/error_applet.hpp include/audio/dsp_core.hpp include/audio/null_core.hpp include/audio/teakra_core.hpp - include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp + include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp ) cmrc_add_resource_library( diff --git a/include/audio/dsp_shared_mem.hpp b/include/audio/dsp_shared_mem.hpp new file mode 100644 index 000000000..148986f9d --- /dev/null +++ b/include/audio/dsp_shared_mem.hpp @@ -0,0 +1,534 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include + +#include "bitfield.hpp" +#include "helpers.hpp" +#include "swap.hpp" + +namespace Audio::HLE { + // The application-accessible region of DSP memory consists of two parts. Both are marked as IO and + // have Read/Write permissions. + + // First Region: 0x1FF50000 (Size: 0x8000) + // Second Region: 0x1FF70000 (Size: 0x8000) + + // The DSP reads from each region alternately based on the frame counter for each region much like a + // double-buffer. The frame counter is located as the very last u16 of each region and is + // incremented each audio tick. + + constexpr u32 region0Offset = 0x50000; + constexpr u32 region1Offset = 0x70000; + + // Number of DSP voices + constexpr u32 sourceCount = 24; + // There are 160 stereo samples in 1 audio frame, so 320 samples total + static constexpr u64 samplesInFrame = 160; + + /** + * The DSP is native 16-bit. The DSP also appears to be big-endian. When reading 32-bit numbers from + * its memory regions, the higher and lower 16-bit halves are swapped compared to the little-endian + * layout of the ARM11. Hence from the ARM11's point of view the memory space appears to be + * middle-endian. + * + * Unusually this does not appear to be an issue for floating point numbers. The DSP makes the more + * sensible choice of keeping that little-endian. There are also some exceptions such as the + * IntermediateMixSamples structure, which is little-endian. + * + * This struct implements the conversion to and from this middle-endianness. + */ + struct u32_dsp { + u32_dsp() = default; + operator u32() const { return Convert(storage); } + void operator=(u32 newValue) { storage = Convert(newValue); } + + private: + static constexpr u32 Convert(u32 value) { return (value << 16) | (value >> 16); } + u32_le storage; + }; + static_assert(std::is_trivially_copyable::value, "u32_dsp isn't trivially copyable"); + + // There are 15 structures in each memory region. A table of them in the order they appear in memory + // is presented below: + // # First Region DSP Address Purpose Control + // 5 0x8400 DSP Status DSP + // 9 0x8410 DSP Debug Info DSP + // 6 0x8540 Final Mix Samples DSP + // 2 0x8680 Source Status [24] DSP + // 8 0x8710 Compressor Table Application + // 4 0x9430 DSP Configuration Application + // 7 0x9492 Intermediate Mix Samples DSP + App + // 1 0x9E92 Source Configuration [24] Application + // 3 0xA792 Source ADPCM Coefficients [24] Application + // 10 0xA912 Surround Sound Related + // 11 0xAA12 Surround Sound Related + // 12 0xAAD2 Surround Sound Related + // 13 0xAC52 Surround Sound Related + // 14 0xAC5C Surround Sound Related + // 0 0xBFFF Frame Counter Application + // + // #: This refers to the order in which they appear in the DspPipe::Audio DSP pipe. + // See also: HLE::PipeRead. + // + // Note that the above addresses do vary slightly between audio firmwares observed; the addresses + // are not fixed in stone. The addresses above are only an examplar; they're what this + // implementation does and provides to applications. + // + // Application requests the DSP service to convert DSP addresses into ARM11 virtual addresses using + // the ConvertProcessAddressFromDspDram service call. Applications seem to derive the addresses for + // the second region via: + // second_region_dsp_addr = first_region_dsp_addr | 0x10000 + // + // Applications maintain most of its own audio state, the memory region is used mainly for + // communication and not storage of state. + // + // In the documentation below, filter and effect transfer functions are specified in the z domain. + // (If you are more familiar with the Laplace transform, z = exp(sT). The z domain is the digital + // frequency domain, just like how the s domain is the analog frequency domain.) + +#define ASSERT_DSP_STRUCT(name, size) \ + static_assert(std::is_standard_layout::value, "DSP structure " #name " doesn't use standard layout"); \ + static_assert(std::is_trivially_copyable::value, "DSP structure " #name " isn't trivially copyable"); \ + static_assert(sizeof(name) == (size), "Unexpected struct size for DSP structure " #name) + + struct SourceConfiguration { + struct Configuration { + /// These dirty flags are set by the application when it updates the fields in this struct. + /// The DSP clears these each audio frame. + union { + u32_le dirtyRaw; + + BitField<0, 1, u32> formatDirty; + BitField<1, 1, u32> monoOrStereoDirty; + BitField<2, 1, u32> adpcmCoefficientsDirty; + /// Tends to be set when a looped buffer is queued. + BitField<3, 1, u32> partialEmbeddedBufferDirty; + BitField<4, 1, u32> partialResetFlag; + + BitField<16, 1, u32> enableDirty; + BitField<17, 1, u32> interpolationDirty; + BitField<18, 1, u32> rateMultiplierDirty; + BitField<19, 1, u32> bufferQueueDirty; + BitField<20, 1, u32> loopRelatedDirty; + /// Tends to also be set when embedded buffer is updated. + BitField<21, 1, u32> playPositionDirty; + BitField<22, 1, u32> filtersEnabledDirty; + BitField<23, 1, u32> simpleFilterDirty; + BitField<24, 1, u32> biquadFilterDirty; + BitField<25, 1, u32> gain0Dirty; + BitField<26, 1, u32> gain1Dirty; + BitField<27, 1, u32> gain2Dirty; + BitField<28, 1, u32> syncCountDirty; + BitField<29, 1, u32> resetFlag; + BitField<30, 1, u32> embeddedBufferDirty; + }; + + // Gain control + + /** + * Gain is between 0.0-1.0. This determines how much will this source appear on each of the + * 12 channels that feed into the intermediate mixers. Each of the three intermediate mixers + * is fed two left and two right channels. + */ + float_le gain[3][4]; + + // Interpolation + + /// Multiplier for sample rate. Resampling occurs with the selected interpolation method. + float_le rateMultiplier; + + enum class InterpolationMode : u8 { + Polyphase = 0, + Linear = 1, + None = 2, + }; + + InterpolationMode interpolationMode; + u8 pad; ///< Interpolation related + + // Filters + + /** + * This is the simplest normalized first-order digital recursive filter. + * The transfer function of this filter is: + * H(z) = b0 / (1 - a1 z^-1) + * Note the feedbackward coefficient is negated. + * Values are signed fixed point with 15 fractional bits. + */ + struct SimpleFilter { + s16_le b0; + s16_le a1; + }; + + /** + * This is a normalised biquad filter (second-order). + * The transfer function of this filter is: + * H(z) = (b0 + b1 z^-1 + b2 z^-2) / (1 - a1 z^-1 - a2 z^-2) + * Nintendo chose to negate the feedbackward coefficients. This differs from standard + * notation as in: https://ccrma.stanford.edu/~jos/filters/Direct_Form_I.html + * Values are signed fixed point with 14 fractional bits.simple_filter_enabled + */ + struct BiquadFilter { + s16_le a2; + s16_le a1; + s16_le b2; + s16_le b1; + s16_le b0; + }; + + union { + u16_le filters_enabled; + BitField<0, 1, u16> simpleFilterEnabled; + BitField<1, 1, u16> biquadFilterEnabled; + }; + + SimpleFilter simpleFilter; + BiquadFilter biquadFilter; + + // Buffer Queue + + /// A buffer of audio data from the application, along with metadata about it. + struct Buffer { + /// Physical memory address of the start of the buffer + u32_dsp physicalAddress; + + /// This is length in terms of samples. + /// Note that in different buffer formats a sample takes up different number of bytes. + u32_dsp length; + + /// ADPCM Predictor (4 bits) and Scale (4 bits) + union { + u16_le adpcm_ps; + BitField<0, 4, u16> adpcmScale; + BitField<4, 4, u16> adpcmPredictor; + }; + + /// ADPCM Historical Samples (y[n-1] and y[n-2]) + u16_le adpcm_yn[2]; + + /// This is non-zero when the ADPCM values above are to be updated. + u8 adpcmDirty; + + /// Is a looping buffer. + u8 isLooping; + + /// This value is shown in SourceStatus::previous_buffer_id when this buffer has + /// finished. This allows the emulated application to tell what buffer is currently + /// playing. + u16_le bufferID; + + u16 pad; + }; + + u16_le buffersDirty; ///< Bitmap indicating which buffers are dirty (bit i -> buffers[i]) + Buffer buffers[4]; ///< Queued Buffers + + // Playback controls + u32_dsp loopRelated; + u8 enable; + u8 pad1; + u16_le syncCount; ///< Application-side sync count (See also: SourceStatus::sync_count) + u32_dsp playPosition; ///< Position. (Units: number of samples) + u16 pad2[2]; + + // Embedded Buffer + // This buffer is often the first buffer to be used when initiating audio playback, + // after which the buffer queue is used. + + u32_dsp physicalAddress; + + /// This is length in terms of samples. + /// Note a sample takes up different number of bytes in different buffer formats. + u32_dsp length; + + enum class MonoOrStereo : u16_le { + Mono = 1, + Stereo = 2, + }; + + enum class Format : u16_le { + PCM8 = 0, + PCM16 = 1, + ADPCM = 2, + }; + + union { + u16_le flags1Raw; + BitField<0, 2, MonoOrStereo> monoOrStereo; + BitField<2, 2, Format> format; + BitField<5, 1, u16> fadeIn; + }; + + /// ADPCM Predictor (4 bit) and Scale (4 bit) + union { + u16_le adpcm_ps; + BitField<0, 4, u16> adpcmScale; + BitField<4, 4, u16> adpcmPredictor; + }; + + /// ADPCM Historical Samples (y[n-1] and y[n-2]) + u16_le adpcm_yn[2]; + + union { + u16_le flags2Raw; + BitField<0, 1, u16> adpcmDirty; ///< Has the ADPCM info above been changed? + BitField<1, 1, u16> isLooping; ///< Is this a looping buffer? + }; + + /// Buffer id of embedded buffer (used as a buffer id in SourceStatus to reference this + /// buffer). + u16_le bufferID; + }; + + Configuration config[sourceCount]; + }; + ASSERT_DSP_STRUCT(SourceConfiguration::Configuration, 192); + ASSERT_DSP_STRUCT(SourceConfiguration::Configuration::Buffer, 20); + + struct SourceStatus { + struct Status { + u8 isEnabled; ///< Is this channel enabled? (Doesn't have to be playing anything.) + u8 currentBufferIDDirty; ///< Non-zero when current_buffer_id changes + u16_le syncCount; ///< Is set by the DSP to the value of SourceConfiguration::sync_count + u32_dsp bufferPosition; ///< Number of samples into the current buffer + u16_le currentBufferID; ///< Updated when a buffer finishes playing + u16_le lastBufferID; ///< Updated when all buffers in the queue finish playing + }; + + Status status[sourceCount]; + }; + ASSERT_DSP_STRUCT(SourceStatus::Status, 12); + + struct DspConfiguration { + /// These dirty flags are set by the application when it updates the fields in this struct. + /// The DSP clears these each audio frame. + union { + u32_le dirtyRaw; + + BitField<6, 1, u32> auxFrontBypass0Dirty; + BitField<7, 1, u32> auxFrontBypass1Dirty; + BitField<8, 1, u32> auxBusEnable0Dirty; + BitField<9, 1, u32> auxBusEnable1Dirty; + BitField<10, 1, u32> delayEffect0Dirty; + BitField<11, 1, u32> delayEffect1Dirty; + BitField<12, 1, u32> reverbEffect0Dirty; + BitField<13, 1, u32> reverbEffect1Dirty; + + BitField<15, 1, u32> outputBufferCountDirty; + BitField<16, 1, u32> masterVolumeDirty; + + BitField<24, 1, u32> auxReturnVolume0Dirty; + BitField<25, 1, u32> auxReturnVolume1Dirty; + BitField<26, 1, u32> outputFormatDirty; + BitField<27, 1, u32> clippingModeDirty; + BitField<28, 1, u32> headphonesConnectedDirty; + BitField<29, 1, u32> surroundDepthDirty; + BitField<30, 1, u32> surroundSpeakerPositionDirty; + BitField<31, 1, u32> rearRatioDirty; + }; + + /// The DSP has three intermediate audio mixers. This controls the volume level (0.0-1.0) for + /// each at the final mixer. + float_le masterVolume; + std::array auxReturnVolume; + + u16_le outputBufferCount; + u16 pad1[2]; + + enum class OutputFormat : u16_le { + Mono = 0, + Stereo = 1, + Surround = 2, + }; + + OutputFormat outputFormat; + + u16_le clippingMode; ///< Not sure of the exact gain equation for the limiter. + u16_le headphonesConnected; ///< Application updates the DSP on headphone status. + + u16_le surroundDepth; + u16_le surroundSpeakerPosition; + u16 pad2; ///< TODO: Surround sound related + u16_le rearRatio; + std::array auxFrontBypass; + std::array auxBusEnable; + + /** + * This is delay with feedback. + * Transfer function: + * H(z) = a z^-N / (1 - b z^-1 + a g z^-N) + * where + * N = frameCount * samplesInFrame + * g, a and b are fixed point with 7 fractional bits + */ + struct DelayEffect { + /// These dirty flags are set by the application when it updates the fields in this struct. + /// The DSP clears these each audio frame. + union { + u16_le dirtyRaw; + BitField<0, 1, u16> enableDirty; + BitField<1, 1, u16> workBufferAddressDirty; + BitField<2, 1, u16> otherDirty; ///< Set when anything else has been changed + }; + + u16_le enable; + u16 pad3; + u16_le outputs; + /// The application allocates a block of memory for the DSP to use as a work buffer. + u32_dsp workBufferAddress; + /// Frames to delay by + u16_le frameCount; + + // Coefficients + s16_le g; ///< Fixed point with 7 fractional bits + s16_le a; ///< Fixed point with 7 fractional bits + s16_le b; ///< Fixed point with 7 fractional bits + }; + + DelayEffect delayEffect[2]; + struct ReverbEffect { + u16 pad[26]; ///< TODO + }; + + ReverbEffect reverbEffect[2]; + + u16_le syncMode; + u16 pad3; + union { + u32_le dirtyRaw2; + + BitField<16, 1, u32> syncModeDirty; + }; + }; + ASSERT_DSP_STRUCT(DspConfiguration, 196); + ASSERT_DSP_STRUCT(DspConfiguration::DelayEffect, 20); + ASSERT_DSP_STRUCT(DspConfiguration::ReverbEffect, 52); + static_assert(offsetof(DspConfiguration, syncMode) == 0xBC); + static_assert(offsetof(DspConfiguration, dirtyRaw2) == 0xC0); + + struct AdpcmCoefficients { + /// Coefficients are signed fixed point with 11 fractional bits. + /// Each source has 16 coefficients associated with it. + s16_le coeff[sourceCount][16]; + }; + ASSERT_DSP_STRUCT(AdpcmCoefficients, 768); + + struct DspStatus { + u16_le unknown; + u16_le dropped_frames; + u16 pad0[0xE]; + }; + ASSERT_DSP_STRUCT(DspStatus, 32); + + /// Final mixed output in PCM16 stereo format, what you hear out of the speakers. + /// When the application writes to this region it has no effect. + struct FinalMixSamples { + s16_le pcm16[samplesInFrame][2]; + }; + ASSERT_DSP_STRUCT(FinalMixSamples, 640); + + /// DSP writes output of intermediate mixers 1 and 2 here. + /// Writes to this region by the application edits the output of the intermediate mixers. + /// This seems to be intended to allow the application to do custom effects on the ARM11. + /// Values that exceed s16 range will be clipped by the DSP after further processing. + struct IntermediateMixSamples { + struct Samples { + s32_le pcm32[4][samplesInFrame]; ///< Little-endian as opposed to DSP middle-endian. + }; + + Samples mix1; + Samples mix2; + }; + ASSERT_DSP_STRUCT(IntermediateMixSamples, 5120); + + /// Compressor table + struct Compressor { + u16 pad[0xD20]; ///< TODO + }; + + /// There is no easy way to implement this in a HLE implementation. + struct DspDebug { + u16 pad[0x130]; + }; + ASSERT_DSP_STRUCT(DspDebug, 0x260); + + struct SharedMemory { + /// Padding + u16 pad[0x400]; + + DspStatus dspStatus; + DspDebug dspDebug; + FinalMixSamples finalSamples; + SourceStatus sourceStatuses; + + Compressor compressor; + DspConfiguration dspConfiguration; + IntermediateMixSamples intermediateMixSamples; + SourceConfiguration sourceConfigurations; + AdpcmCoefficients adpcmCoefficients; + + struct { + u16 pad[0x100]; + } unknown10; + + struct { + u16 pad[0xC0]; + } unknown11; + + struct { + u16 pad[0x180]; + } unknown12; + + struct { + u16 pad[0xA]; + } unknown13; + + struct { + u16 pad[0x13A3]; + } unknown14; + + u16_le frameCounter; + }; + ASSERT_DSP_STRUCT(SharedMemory, 0x8000); + + union DspMemory { + std::array rawMemory{}; + struct { + u8 unused0[0x50000]; + SharedMemory region0; + u8 unused1[0x18000]; + SharedMemory region1; + u8 unused2[0x8000]; + }; + }; + static_assert(offsetof(DspMemory, region0) == region0Offset, "DSP region 0 is at the wrong offset"); + static_assert(offsetof(DspMemory, region1) == region1Offset, "DSP region 1 is at the wrong offset"); + + // Structures must have an offset that is a multiple of two. + static_assert(offsetof(SharedMemory, frameCounter) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + static_assert(offsetof(SharedMemory, sourceConfigurations) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + static_assert(offsetof(SharedMemory, sourceStatuses) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + static_assert(offsetof(SharedMemory, adpcmCoefficients) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + static_assert(offsetof(SharedMemory, dspConfiguration) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + static_assert(offsetof(SharedMemory, dspStatus) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + static_assert(offsetof(SharedMemory, finalSamples) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + static_assert(offsetof(SharedMemory, intermediateMixSamples) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + static_assert(offsetof(SharedMemory, compressor) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + static_assert(offsetof(SharedMemory, dspDebug) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + static_assert(offsetof(SharedMemory, unknown10) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + static_assert(offsetof(SharedMemory, unknown11) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + static_assert(offsetof(SharedMemory, unknown12) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + static_assert(offsetof(SharedMemory, unknown13) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + static_assert(offsetof(SharedMemory, unknown14) % 2 == 0, "Structures in HLE::SharedMemory must be 2-byte aligned"); + +#undef INSERT_PADDING_DSPWORDS +#undef ASSERT_DSP_STRUCT + +} // namespace Audio::HLE \ No newline at end of file From 2e696deccff7edc5802df49e203e4d025bab292a Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 5 Apr 2024 00:44:31 +0300 Subject: [PATCH 19/60] Add HLE DSP files --- CMakeLists.txt | 3 +- include/audio/dsp_core.hpp | 2 +- include/audio/hle_core.hpp | 46 ++++++++++ src/core/audio/dsp_core.cpp | 9 +- src/core/audio/hle_core.cpp | 166 ++++++++++++++++++++++++++++++++++++ 5 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 include/audio/hle_core.hpp create mode 100644 src/core/audio/hle_core.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b763dfff..3dc7e467d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,7 +197,7 @@ set(APPLET_SOURCE_FILES src/core/applets/applet.cpp src/core/applets/mii_selecto src/core/applets/error_applet.cpp ) set(AUDIO_SOURCE_FILES src/core/audio/dsp_core.cpp src/core/audio/null_core.cpp src/core/audio/teakra_core.cpp - src/core/audio/miniaudio_device.cpp + src/core/audio/miniaudio_device.cpp src/core/audio/hle_core.cpp ) set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) @@ -234,6 +234,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/scheduler.hpp include/applets/error_applet.hpp include/audio/dsp_core.hpp include/audio/null_core.hpp include/audio/teakra_core.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp + include/audio/hle_core.hpp ) cmrc_add_resource_library( diff --git a/include/audio/dsp_core.hpp b/include/audio/dsp_core.hpp index 1a556f289..a4fb1ab19 100644 --- a/include/audio/dsp_core.hpp +++ b/include/audio/dsp_core.hpp @@ -37,7 +37,7 @@ namespace Audio { MAKE_LOG_FUNCTION(log, dspLogger) public: - enum class Type { Null, Teakra }; + enum class Type { Null, Teakra, HLE }; DSPCore(Memory& mem, Scheduler& scheduler, DSPService& dspService) : mem(mem), scheduler(scheduler), dspService(dspService) {} virtual ~DSPCore() {} diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp new file mode 100644 index 000000000..ae3c29cf6 --- /dev/null +++ b/include/audio/hle_core.hpp @@ -0,0 +1,46 @@ +#pragma once +#include + +#include "audio/dsp_core.hpp" +#include "audio/dsp_shared_mem.hpp" +#include "memory.hpp" + +namespace Audio { + class HLE_DSP : public DSPCore { + enum class DSPState : u32 { + Off, + On, + Slep, + }; + + // Number of DSP pipes + static constexpr size_t pipeCount = 8; + DSPState dspState; + + std::array, pipeCount> pipeData; // The data of each pipe + std::array dspRam; + + void resetAudioPipe(); + bool loaded = false; // Have we loaded a component? + + public: + HLE_DSP(Memory& mem, Scheduler& scheduler, DSPService& dspService) : DSPCore(mem, scheduler, dspService) {} + ~HLE_DSP() override {} + + void reset() override; + void runAudioFrame() override; + + u8* getDspMemory() override { return dspRam.data(); } + + u16 recvData(u32 regId) override; + bool recvDataIsReady(u32 regId) override { return true; } // Treat data as always ready + void writeProcessPipe(u32 channel, u32 size, u32 buffer) override; + std::vector readPipe(u32 channel, u32 peer, u32 size, u32 buffer) override; + + void loadComponent(std::vector& data, u32 programMask, u32 dataMask) override; + void unloadComponent() override; + void setSemaphore(u16 value) override {} + void setSemaphoreMask(u16 value) override {} + }; + +} // namespace Audio \ No newline at end of file diff --git a/src/core/audio/dsp_core.cpp b/src/core/audio/dsp_core.cpp index e4162e939..ee134fa2f 100644 --- a/src/core/audio/dsp_core.cpp +++ b/src/core/audio/dsp_core.cpp @@ -1,5 +1,6 @@ #include "audio/dsp_core.hpp" +#include "audio/hle_core.hpp" #include "audio/null_core.hpp" #include "audio/teakra_core.hpp" @@ -13,6 +14,7 @@ std::unique_ptr Audio::makeDSPCore(DSPCore::Type type, Memory& m switch (type) { case DSPCore::Type::Null: core = std::make_unique(mem, scheduler, dspService); break; case DSPCore::Type::Teakra: core = std::make_unique(mem, scheduler, dspService); break; + case DSPCore::Type::HLE: core = std::make_unique(mem, scheduler, dspService); break; default: Helpers::warn("Invalid DSP core selected!"); @@ -29,10 +31,8 @@ Audio::DSPCore::Type Audio::DSPCore::typeFromString(std::string inString) { std::transform(inString.begin(), inString.end(), inString.begin(), [](unsigned char c) { return std::tolower(c); }); static const std::unordered_map map = { - {"null", Audio::DSPCore::Type::Null}, - {"none", Audio::DSPCore::Type::Null}, - {"lle", Audio::DSPCore::Type::Teakra}, - {"teakra", Audio::DSPCore::Type::Teakra}, + {"null", Audio::DSPCore::Type::Null}, {"none", Audio::DSPCore::Type::Null}, {"lle", Audio::DSPCore::Type::Teakra}, + {"teakra", Audio::DSPCore::Type::Teakra}, {"hle", Audio::DSPCore::Type::HLE}, {"fast", Audio::DSPCore::Type::HLE}, }; if (auto search = map.find(inString); search != map.end()) { @@ -47,6 +47,7 @@ const char* Audio::DSPCore::typeToString(Audio::DSPCore::Type type) { switch (type) { case Audio::DSPCore::Type::Null: return "null"; case Audio::DSPCore::Type::Teakra: return "teakra"; + case Audio::DSPCore::Type::HLE: return "hle"; default: return "invalid"; } } diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp new file mode 100644 index 000000000..52576f63b --- /dev/null +++ b/src/core/audio/hle_core.cpp @@ -0,0 +1,166 @@ +#include "audio/hle_core.hpp" + +#include "services/dsp.hpp" + +namespace Audio { + namespace DSPPipeType { + enum : u32 { + Debug = 0, + DMA = 1, + Audio = 2, + Binary = 3, + }; + } + + void HLE_DSP::resetAudioPipe() { + // Hardcoded responses for now + // These are DSP DRAM offsets for various variables + // https://www.3dbrew.org/wiki/DSP_Memory_Region + static constexpr std::array responses = { + 0x000F, // Number of responses + 0xBFFF, // Frame counter + 0x9E92, // Source configs + 0x8680, // Source statuses + 0xA792, // ADPCM coefficients + 0x9430, // DSP configs + 0x8400, // DSP status + 0x8540, // Final samples + 0x9492, // Intermediate mix samples + 0x8710, // Compressor + 0x8410, // Debug + 0xA912, // ?? + 0xAA12, // ?? + 0xAAD2, // ?? + 0xAC52, // Surround sound biquad filter 1 + 0xAC5C // Surround sound biquad filter 2 + }; + + std::vector& audioPipe = pipeData[DSPPipeType::Audio]; + audioPipe.resize(responses.size() * sizeof(u16)); + + // Push back every response to the audio pipe + size_t index = 0; + for (auto e : responses) { + audioPipe[index++] = e & 0xff; + audioPipe[index++] = e >> 8; + } + } + + void HLE_DSP::reset() { + loaded = false; + for (auto& e : pipeData) { + e.clear(); + } + + // Note: Reset audio pipe AFTER resetting all pipes, otherwise the new data will be yeeted + resetAudioPipe(); + } + + void HLE_DSP::loadComponent(std::vector& data, u32 programMask, u32 dataMask) { + if (loaded) { + Helpers::warn("Loading DSP component when already loaded"); + } + + loaded = true; + scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::cyclesPerFrame); + } + + void HLE_DSP::unloadComponent() { + if (!loaded) { + Helpers::warn("Audio: unloadComponent called without a running program"); + } + + loaded = false; + scheduler.removeEvent(Scheduler::EventType::RunDSP); + } + + void HLE_DSP::runAudioFrame() { + // Signal audio pipe when an audio frame is done + if (dspState == DSPState::On) [[likely]] { + dspService.triggerPipeEvent(DSPPipeType::Audio); + } + + scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::cyclesPerFrame); + } + + u16 HLE_DSP::recvData(u32 regId) { + if (regId != 0) { + Helpers::panic("Audio: invalid register in null frontend"); + } + + return dspState == DSPState::On; + } + + void HLE_DSP::writeProcessPipe(u32 channel, u32 size, u32 buffer) { + enum class StateChange : u8 { + Initialize = 0, + Shutdown = 1, + Wakeup = 2, + Sleep = 3, + }; + + switch (channel) { + case DSPPipeType::Audio: { + if (size != 4) { + printf("Invalid size written to DSP Audio Pipe\n"); + break; + } + + // Get new state + const u8 state = mem.read8(buffer); + if (state > 3) { + log("WriteProcessPipe::Audio: Unknown state change type"); + } else { + switch (static_cast(state)) { + case StateChange::Initialize: + // TODO: Other initialization stuff here + dspState = DSPState::On; + resetAudioPipe(); + + dspService.triggerPipeEvent(DSPPipeType::Audio); + break; + + case StateChange::Shutdown: + dspState = DSPState::Off; + break; + + default: Helpers::panic("Unimplemented DSP audio pipe state change %d", state); + } + } + break; + } + + case DSPPipeType::Binary: + Helpers::warn("Unimplemented write to binary pipe! Size: %d\n", size); + + // This pipe and interrupt are normally used for requests like AAC decode + dspService.triggerPipeEvent(DSPPipeType::Binary); + break; + + default: log("Audio::HLE_DSP: Wrote to unimplemented pipe %d\n", channel); break; + } + } + + std::vector HLE_DSP::readPipe(u32 pipe, u32 peer, u32 size, u32 buffer) { + if (size & 1) Helpers::panic("Tried to read odd amount of bytes from DSP pipe"); + if (pipe >= pipeCount || size > 0xffff) { + return {}; + } + + if (pipe != DSPPipeType::Audio) { + log("Reading from non-audio pipe! This might be broken, might need to check what pipe is being read from and implement writing to it\n"); + } + + std::vector& data = pipeData[pipe]; + size = std::min(size, data.size()); // Clamp size to the maximum available data size + + if (size == 0) { + return {}; + } + + // Return "size" bytes from the audio pipe and erase them + std::vector out(data.begin(), data.begin() + size); + data.erase(data.begin(), data.begin() + size); + return out; + } +} // namespace Audio From a85ca0459a517aefc1ecdd51ad1fbce53a3e0f42 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 5 Apr 2024 01:13:02 +0300 Subject: [PATCH 20/60] HLE DSP: Proper audio pipe responses --- src/core/audio/hle_core.cpp | 38 +++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 52576f63b..791d14cc4 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -13,27 +13,29 @@ namespace Audio { } void HLE_DSP::resetAudioPipe() { - // Hardcoded responses for now - // These are DSP DRAM offsets for various variables +#define DSPOffset(var) (0x8000 + offsetof(Audio::HLE::SharedMemory, var) / 2) + + // These are DSP shared memory offsets for various variables // https://www.3dbrew.org/wiki/DSP_Memory_Region static constexpr std::array responses = { - 0x000F, // Number of responses - 0xBFFF, // Frame counter - 0x9E92, // Source configs - 0x8680, // Source statuses - 0xA792, // ADPCM coefficients - 0x9430, // DSP configs - 0x8400, // DSP status - 0x8540, // Final samples - 0x9492, // Intermediate mix samples - 0x8710, // Compressor - 0x8410, // Debug - 0xA912, // ?? - 0xAA12, // ?? - 0xAAD2, // ?? - 0xAC52, // Surround sound biquad filter 1 - 0xAC5C // Surround sound biquad filter 2 + 0x000F, // Number of responses + DSPOffset(frameCounter), // Frame counter + DSPOffset(sourceConfigurations), // Source configs + DSPOffset(sourceStatuses), // Source statuses + DSPOffset(adpcmCoefficients), // ADPCM coefficients + DSPOffset(dspConfiguration), // DSP configs + DSPOffset(dspStatus), // DSP status + DSPOffset(finalSamples), // Final samples + DSPOffset(intermediateMixSamples), // Intermediate mix samples + DSPOffset(compressor), // Compressor + DSPOffset(dspDebug), // Debug + DSPOffset(unknown10), // ?? + DSPOffset(unknown11), // ?? + DSPOffset(unknown12), // ?? + DSPOffset(unknown13), // Surround sound biquad filter 1 + DSPOffset(unknown14) // Surround sound biquad filter 2 }; +#undef DSPOffset std::vector& audioPipe = pipeData[DSPPipeType::Audio]; audioPipe.resize(responses.size() * sizeof(u16)); From 4070bea697984b13e9a32128f5c2f04ab2c2ff5c Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 5 Apr 2024 01:31:56 +0300 Subject: [PATCH 21/60] HLE DSP: Add region handling --- include/audio/hle_core.hpp | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index ae3c29cf6..c9d36c110 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -18,11 +18,31 @@ namespace Audio { DSPState dspState; std::array, pipeCount> pipeData; // The data of each pipe - std::array dspRam; + Audio::HLE::DspMemory dspRam; void resetAudioPipe(); bool loaded = false; // Have we loaded a component? + // Get the index for the current region we'll be reading. Returns the region with the highest frame counter + // Accounting for whether one of the frame counters has wrapped around + usize readRegionIndex() const { + const auto counter0 = dspRam.region0.frameCounter; + const auto counter1 = dspRam.region1.frameCounter; + + // Handle wraparound cases first + if (counter0 == 0xffff && counter1 != 0xfffe) { + return 1; + } else if (counter1 == 0xffff && counter0 != 0xfffe) { + return 0; + } else { + return counter0 > counter1 ? 0 : 0; + } + } + + // DSP shared memory is double buffered; One region is being written to while the other one is being read from + Audio::HLE::SharedMemory& readRegion() { return readRegionIndex() == 0 ? dspRam.region0 : dspRam.region1; } + Audio::HLE::SharedMemory& writeRegion() { return readRegionIndex() == 0 ? dspRam.region1 : dspRam.region0; } + public: HLE_DSP(Memory& mem, Scheduler& scheduler, DSPService& dspService) : DSPCore(mem, scheduler, dspService) {} ~HLE_DSP() override {} @@ -30,7 +50,7 @@ namespace Audio { void reset() override; void runAudioFrame() override; - u8* getDspMemory() override { return dspRam.data(); } + u8* getDspMemory() override { return dspRam.rawMemory.data(); } u16 recvData(u32 regId) override; bool recvDataIsReady(u32 regId) override { return true; } // Treat data as always ready From 2548bde538837d5a76dd95721dcf4ecfbc110354 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 5 Apr 2024 01:48:01 +0300 Subject: [PATCH 22/60] HLE DSP: Add frame types --- include/audio/hle_core.hpp | 20 ++++++++++++++++++++ src/core/audio/hle_core.cpp | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index c9d36c110..202b93086 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -7,6 +7,24 @@ namespace Audio { class HLE_DSP : public DSPCore { + // The audio frame types are public in case we want to use them for unit tests + public: + template + using Sample = std::array; + + template + using Frame = std::array, 160>; + + template + using MonoFrame = Frame; + + template + using StereoFrame = Frame; + + template + using QuadFrame = Frame; + + private: enum class DSPState : u32 { Off, On, @@ -43,6 +61,8 @@ namespace Audio { Audio::HLE::SharedMemory& readRegion() { return readRegionIndex() == 0 ? dspRam.region0 : dspRam.region1; } Audio::HLE::SharedMemory& writeRegion() { return readRegionIndex() == 0 ? dspRam.region1 : dspRam.region0; } + StereoFrame generateFrame(); + void outputFrame(); public: HLE_DSP(Memory& mem, Scheduler& scheduler, DSPService& dspService) : DSPCore(mem, scheduler, dspService) {} ~HLE_DSP() override {} diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 791d14cc4..326975342 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -82,6 +82,7 @@ namespace Audio { dspService.triggerPipeEvent(DSPPipeType::Audio); } + outputFrame(); scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::cyclesPerFrame); } @@ -165,4 +166,9 @@ namespace Audio { data.erase(data.begin(), data.begin() + size); return out; } + + void HLE_DSP::outputFrame() { + StereoFrame frame = generateFrame(); + Helpers::panic("HLE DSP: Output frame"); + } } // namespace Audio From 5da93d17bd584fee525a46ba632c9616d34b96b4 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 5 Apr 2024 19:42:18 +0300 Subject: [PATCH 23/60] HLE DSP: More of it --- include/audio/hle_core.hpp | 24 +++++++++++++++++++++++- src/core/audio/hle_core.cpp | 27 +++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index 202b93086..f957284dd 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -6,6 +6,27 @@ #include "memory.hpp" namespace Audio { + struct DSPSource { + std::array gain0, gain1, gain2; + + // Audio buffer information + // https://www.3dbrew.org/wiki/DSP_Memory_Region + struct Buffer { + u32 paddr; // Physical address of the buffer + u32 sampleCount; // Total number of samples + u8 adpcmScale; // ADPCM predictor/scale + u8 pad; // Unknown + + std::array previousSamples; // ADPCM y[n-1] and y[n-2] + bool adpcmDirty; + bool looping; + u16 bufferID; + }; + + void reset(); + DSPSource() { reset(); } + }; + class HLE_DSP : public DSPCore { // The audio frame types are public in case we want to use them for unit tests public: @@ -24,6 +45,7 @@ namespace Audio { template using QuadFrame = Frame; + using Source = Audio::DSPSource; private: enum class DSPState : u32 { Off, @@ -61,7 +83,7 @@ namespace Audio { Audio::HLE::SharedMemory& readRegion() { return readRegionIndex() == 0 ? dspRam.region0 : dspRam.region1; } Audio::HLE::SharedMemory& writeRegion() { return readRegionIndex() == 0 ? dspRam.region1 : dspRam.region0; } - StereoFrame generateFrame(); + void generateFrame(StereoFrame& frame); void outputFrame(); public: HLE_DSP(Memory& mem, Scheduler& scheduler, DSPService& dspService) : DSPCore(mem, scheduler, dspService) {} diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 326975342..65bcb2c65 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -1,5 +1,7 @@ #include "audio/hle_core.hpp" +#include + #include "services/dsp.hpp" namespace Audio { @@ -168,7 +170,28 @@ namespace Audio { } void HLE_DSP::outputFrame() { - StereoFrame frame = generateFrame(); - Helpers::panic("HLE DSP: Output frame"); + StereoFrame frame; + generateFrame(frame); + + if (audioEnabled) { + // Wait until we've actually got room to push our frame + while (sampleBuffer.size() + 2 > sampleBuffer.Capacity()) { + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + } + + sampleBuffer.push(frame.data(), frame.size()); + } } + + void HLE_DSP::generateFrame(StereoFrame& frame) { + using namespace Audio::HLE; + SharedMemory& read = readRegion(); + SharedMemory& write = writeRegion(); + + for (int source = 0; source < sourceCount; source++) { + Helpers::panic("Panda"); + } + } + + void DSPSource::reset() {} } // namespace Audio From 43a1c894783edf902b1de64fc23eafd3a789e5c7 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 5 Apr 2024 20:53:17 +0300 Subject: [PATCH 24/60] HLE DSP: Init/deinit sources better --- include/audio/hle_core.hpp | 7 +++++-- src/core/audio/hle_core.cpp | 12 ++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index f957284dd..75d02bca1 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -23,6 +23,8 @@ namespace Audio { u16 bufferID; }; + int index = 0; // Index of the voice in [0, 23] for debugging + void reset(); DSPSource() { reset(); } }; @@ -57,7 +59,8 @@ namespace Audio { static constexpr size_t pipeCount = 8; DSPState dspState; - std::array, pipeCount> pipeData; // The data of each pipe + std::array, pipeCount> pipeData; // The data of each pipe + std::array sources; // DSP voices Audio::HLE::DspMemory dspRam; void resetAudioPipe(); @@ -86,7 +89,7 @@ namespace Audio { void generateFrame(StereoFrame& frame); void outputFrame(); public: - HLE_DSP(Memory& mem, Scheduler& scheduler, DSPService& dspService) : DSPCore(mem, scheduler, dspService) {} + HLE_DSP(Memory& mem, Scheduler& scheduler, DSPService& dspService); ~HLE_DSP() override {} void reset() override; diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 65bcb2c65..6b0a32c9c 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -14,6 +14,13 @@ namespace Audio { }; } + HLE_DSP::HLE_DSP(Memory& mem, Scheduler& scheduler, DSPService& dspService) : DSPCore(mem, scheduler, dspService) { + // Set up source indices + for (int i = 0; i < sources.size(); i++) { + sources[i].index = i; + } + } + void HLE_DSP::resetAudioPipe() { #define DSPOffset(var) (0x8000 + offsetof(Audio::HLE::SharedMemory, var) / 2) @@ -56,6 +63,10 @@ namespace Audio { e.clear(); } + for (auto& source : sources) { + source.reset(); + } + // Note: Reset audio pipe AFTER resetting all pipes, otherwise the new data will be yeeted resetAudioPipe(); } @@ -189,6 +200,7 @@ namespace Audio { SharedMemory& write = writeRegion(); for (int source = 0; source < sourceCount; source++) { + //updateSourceConfig(sources[source]); Helpers::panic("Panda"); } } From 37f9f5d894cedd752e28683d0af782b55c61f3f1 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 6 Apr 2024 00:54:15 +0300 Subject: [PATCH 25/60] HLE DSP: Track voice status better --- include/audio/hle_core.hpp | 3 +++ src/core/audio/hle_core.cpp | 53 ++++++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index 75d02bca1..d4d72b62c 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -8,6 +8,8 @@ namespace Audio { struct DSPSource { std::array gain0, gain1, gain2; + u16 syncCount; + bool enabled; // Audio buffer information // https://www.3dbrew.org/wiki/DSP_Memory_Region @@ -86,6 +88,7 @@ namespace Audio { Audio::HLE::SharedMemory& readRegion() { return readRegionIndex() == 0 ? dspRam.region0 : dspRam.region1; } Audio::HLE::SharedMemory& writeRegion() { return readRegionIndex() == 0 ? dspRam.region1 : dspRam.region0; } + void updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config); void generateFrame(StereoFrame& frame); void outputFrame(); public: diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 6b0a32c9c..b27dd10b7 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -199,11 +199,56 @@ namespace Audio { SharedMemory& read = readRegion(); SharedMemory& write = writeRegion(); - for (int source = 0; source < sourceCount; source++) { - //updateSourceConfig(sources[source]); - Helpers::panic("Panda"); + for (int i = 0; i < sourceCount; i++) { + // Update source configuration from the read region of shared memory + auto& config = read.sourceConfigurations.config[i]; + auto& source = sources[i]; + updateSourceConfig(source, config); + + // Generate audio + + // Update write region of shared memory + auto& status = write.sourceStatuses.status[i]; + status.isEnabled = source.enabled; + status.syncCount = source.syncCount; + //status.lastBufferID=0,status.currentBufferID = 1; status.currentBufferIDDirty = 1; + //status.bufferPosition = } } - void DSPSource::reset() {} + void HLE_DSP::updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config) { + // Check if the any dirty bit is set, otherwise exit early + if (!config.dirtyRaw) { + return; + } + + if (config.enableDirty) { + config.enableDirty = 0; + source.enabled = config.enable != 0; + + printf("Voice %d enable set to %d\n", source.index, source.enabled); + } + + if (config.syncCountDirty) { + config.syncCountDirty = 0; + source.syncCount = config.syncCount; + } + + if (config.resetFlag) { + config.resetFlag = 0; + printf("Reset voice %d\n", source.index); + } + + if (config.partialResetFlag) { + config.partialResetFlag = 0; + printf("Partially reset voice %d\n", source.index); + } + + config.dirtyRaw = 0; + } + + void DSPSource::reset() { + enabled = false; + syncCount = 0; + } } // namespace Audio From 1c355041fad319d60b1ef064bd6df2ea76cd0a95 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 6 Apr 2024 16:53:29 +0300 Subject: [PATCH 26/60] HLE DSP: Add embedded buffers --- include/audio/hle_core.hpp | 26 ++++++++++++++++----- src/core/audio/hle_core.cpp | 45 +++++++++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index d4d72b62c..46caf2f5c 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -1,5 +1,7 @@ #pragma once #include +#include +#include #include "audio/dsp_core.hpp" #include "audio/dsp_shared_mem.hpp" @@ -7,24 +9,38 @@ namespace Audio { struct DSPSource { - std::array gain0, gain1, gain2; - u16 syncCount; - bool enabled; - // Audio buffer information // https://www.3dbrew.org/wiki/DSP_Memory_Region struct Buffer { u32 paddr; // Physical address of the buffer u32 sampleCount; // Total number of samples u8 adpcmScale; // ADPCM predictor/scale - u8 pad; // Unknown + u8 pad1; // Unknown std::array previousSamples; // ADPCM y[n-1] and y[n-2] bool adpcmDirty; bool looping; u16 bufferID; + u8 pad2; + + u32 playPosition = 0; // Current position in the buffer + bool fromQueue = false; // Is this buffer from the buffer queue or an embedded buffer? + bool hasPlayedOnce = false; // Has the buffer been played at least once before? + + bool operator<(const Buffer& other) const { + // Lower ID = Higher priority + // If this buffer ID is greater than the other one, then this buffer has a lower priority + return this->bufferID > other.bufferID; + } }; + using BufferQueue = std::priority_queue; + + std::array gain0, gain1, gain2; + u16 syncCount; + bool enabled; + + BufferQueue buffers; int index = 0; // Index of the voice in [0, 23] for debugging void reset(); diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index b27dd10b7..06d15945f 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -1,6 +1,7 @@ #include "audio/hle_core.hpp" #include +#include #include "services/dsp.hpp" @@ -206,6 +207,9 @@ namespace Audio { updateSourceConfig(source, config); // Generate audio + if (source.enabled) { + + } // Update write region of shared memory auto& status = write.sourceStatuses.status[i]; @@ -225,8 +229,6 @@ namespace Audio { if (config.enableDirty) { config.enableDirty = 0; source.enabled = config.enable != 0; - - printf("Voice %d enable set to %d\n", source.index, source.enabled); } if (config.syncCountDirty) { @@ -236,12 +238,45 @@ namespace Audio { if (config.resetFlag) { config.resetFlag = 0; - printf("Reset voice %d\n", source.index); + source.reset(); } if (config.partialResetFlag) { config.partialResetFlag = 0; - printf("Partially reset voice %d\n", source.index); + source.buffers = {}; + } + + if (config.embeddedBufferDirty) { + config.embeddedBufferDirty = 0; + if (s32(config.length) >= 0) [[likely]] { + // TODO: Add sample format and channel count + Source::Buffer buffer{ + .paddr = config.physicalAddress, + .sampleCount = config.length, + .adpcmScale = u8(config.adpcm_ps), + .previousSamples = {s16(config.adpcm_yn[0]), s16(config.adpcm_yn[1])}, + .adpcmDirty = config.adpcmDirty != 0, + .looping = config.isLooping != 0, + .bufferID = config.bufferID, + .playPosition = config.playPosition, + .fromQueue = false, + .hasPlayedOnce = false, + }; + + source.buffers.emplace(std::move(buffer)); + } else { + log("Invalid embedded buffer size for DSP voice %d\n", source.index); + } + } + + if (config.partialEmbeddedBufferDirty) { + config.partialEmbeddedBufferDirty = 0; + printf("Partial embedded buffer dirty for voice %d\n", source.index); + } + + if (config.bufferQueueDirty) { + config.bufferQueueDirty = 0; + printf("Buffer queue dirty for voice %d\n", source.index); } config.dirtyRaw = 0; @@ -250,5 +285,7 @@ namespace Audio { void DSPSource::reset() { enabled = false; syncCount = 0; + + buffers = {}; } } // namespace Audio From 1cc3bbf68da370b52396e60842235041a87b8e8b Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 6 Apr 2024 19:12:58 +0300 Subject: [PATCH 27/60] HLE DSP: Fix format and source type for audio buffers --- include/audio/hle_core.hpp | 27 +++++++++++++++++++++++++++ include/memory.hpp | 5 ++++- src/core/audio/hle_core.cpp | 28 ++++++++++++++++++++++++---- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index 46caf2f5c..6d7b3ad1d 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -8,6 +8,9 @@ #include "memory.hpp" namespace Audio { + using SampleFormat = HLE::SourceConfiguration::Configuration::Format; + using SourceType = HLE::SourceConfiguration::Configuration::MonoOrStereo; + struct DSPSource { // Audio buffer information // https://www.3dbrew.org/wiki/DSP_Memory_Region @@ -24,6 +27,9 @@ namespace Audio { u8 pad2; u32 playPosition = 0; // Current position in the buffer + SampleFormat format; + SourceType sourceType; + bool fromQueue = false; // Is this buffer from the buffer queue or an embedded buffer? bool hasPlayedOnce = false; // Has the buffer been played at least once before? @@ -81,6 +87,9 @@ namespace Audio { std::array sources; // DSP voices Audio::HLE::DspMemory dspRam; + SampleFormat sampleFormat = SampleFormat::ADPCM; + SourceType sourceType = SourceType::Stereo; + void resetAudioPipe(); bool loaded = false; // Have we loaded a component? @@ -104,9 +113,27 @@ namespace Audio { Audio::HLE::SharedMemory& readRegion() { return readRegionIndex() == 0 ? dspRam.region0 : dspRam.region1; } Audio::HLE::SharedMemory& writeRegion() { return readRegionIndex() == 0 ? dspRam.region1 : dspRam.region0; } + // Get a pointer of type T* to the data starting from physical address paddr + template + T* getPointerPhys(u32 paddr, u32 size = 0) { + if (paddr >= PhysicalAddrs::FCRAM && paddr + size <= PhysicalAddrs::FCRAMEnd) { + u8* fcram = mem.getFCRAM(); + u32 index = paddr - PhysicalAddrs::FCRAM; + + return (T*)&fcram[index]; + } else if (paddr >= PhysicalAddrs::DSP_RAM && paddr + size <= PhysicalAddrs::DSP_RAM_End) { + u32 index = paddr - PhysicalAddrs::DSP_RAM; + return (T*)&dspRam.rawMemory[index]; + } else [[unlikely]] { + Helpers::warn("[DSP] Tried to access unknown physical address: %08X", paddr); + return nullptr; + } + } + void updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config); void generateFrame(StereoFrame& frame); void outputFrame(); + void dumpBuffer(const Source::Buffer& buffer); public: HLE_DSP(Memory& mem, Scheduler& scheduler, DSPService& dspService); ~HLE_DSP() override {} diff --git a/include/memory.hpp b/include/memory.hpp index 1b6e622ca..33ccbae58 100644 --- a/include/memory.hpp +++ b/include/memory.hpp @@ -19,7 +19,10 @@ namespace PhysicalAddrs { VRAM = 0x18000000, VRAMEnd = VRAM + 0x005FFFFF, FCRAM = 0x20000000, - FCRAMEnd = FCRAM + 0x07FFFFFF + FCRAMEnd = FCRAM + 0x07FFFFFF, + + DSP_RAM = 0x1FF00000, + DSP_RAM_End = DSP_RAM + 0x0007FFFF }; } diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 06d15945f..f9f1248ce 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -59,7 +59,13 @@ namespace Audio { } void HLE_DSP::reset() { + dspState = DSPState::Off; loaded = false; + + // Initialize these to some sane defaults + sampleFormat = SampleFormat::ADPCM; + sourceType = SourceType::Stereo; + for (auto& e : pipeData) { e.clear(); } @@ -207,16 +213,19 @@ namespace Audio { updateSourceConfig(source, config); // Generate audio - if (source.enabled) { - + if (source.enabled && !source.buffers.empty()) { + const auto& buffer = source.buffers.top(); + const u8* data = getPointerPhys(buffer.paddr); + + if (data != nullptr) { + // TODO + } } // Update write region of shared memory auto& status = write.sourceStatuses.status[i]; status.isEnabled = source.enabled; status.syncCount = source.syncCount; - //status.lastBufferID=0,status.currentBufferID = 1; status.currentBufferIDDirty = 1; - //status.bufferPosition = } } @@ -245,6 +254,15 @@ namespace Audio { config.partialResetFlag = 0; source.buffers = {}; } + + // TODO: Should we check bufferQueueDirty here too? + if (config.formatDirty || config.embeddedBufferDirty) { + sampleFormat = config.format; + } + + if (config.monoOrStereoDirty || config.embeddedBufferDirty) { + sourceType = config.monoOrStereo; + } if (config.embeddedBufferDirty) { config.embeddedBufferDirty = 0; @@ -259,6 +277,8 @@ namespace Audio { .looping = config.isLooping != 0, .bufferID = config.bufferID, .playPosition = config.playPosition, + .format = sampleFormat, + .sourceType = sourceType, .fromQueue = false, .hasPlayedOnce = false, }; From 55d99734e1abd7a75e7c08736b8bcde24c6d0d41 Mon Sep 17 00:00:00 2001 From: GPUCode Date: Tue, 9 Apr 2024 02:27:43 +0300 Subject: [PATCH 28/60] panda_sdl: Use sym instead of scancode --- src/panda_sdl/frontend_sdl.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index f94f98f44..0c78eea15 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -95,7 +95,7 @@ void FrontendSDL::run() { case SDL_KEYDOWN: { if (emu.romType == ROMType::None) break; - u32 key = getMapping(event.key.keysym.scancode); + u32 key = getMapping(event.key.keysym.sym); if (key != HID::Keys::Null) { switch (key) { case HID::Keys::CirclePadRight: @@ -138,7 +138,7 @@ void FrontendSDL::run() { case SDL_KEYUP: { if (emu.romType == ROMType::None) break; - u32 key = getMapping(event.key.keysym.scancode); + u32 key = getMapping(event.key.keysym.sym); if (key != HID::Keys::Null) { switch (key) { // Err this is probably not ideal From 0aa024876e1a79499a8d68da840edf424e2ffa5f Mon Sep 17 00:00:00 2001 From: Ishan09811 <156402647+Ishan09811@users.noreply.github.com> Date: Sun, 14 Apr 2024 14:14:31 +0530 Subject: [PATCH 29/60] add gradle caching system --- .github/workflows/Android_Build.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/Android_Build.yml b/.github/workflows/Android_Build.yml index 2a06a12cb..11811f8b1 100644 --- a/.github/workflows/Android_Build.yml +++ b/.github/workflows/Android_Build.yml @@ -23,6 +23,16 @@ jobs: - name: Fetch submodules run: git submodule update --init --recursive + - name: Set up gradle caches + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-pandroid-x86_64-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-pandroid-x86_64- + - name: Setup Vulkan SDK uses: humbletim/setup-vulkan-sdk@v1.2.0 with: @@ -78,6 +88,16 @@ jobs: - name: Fetch submodules run: git submodule update --init --recursive + - name: Set up gradle caches + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-pandroid-arm64-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-pandroid-arm64- + - name: Setup Vulkan SDK uses: humbletim/setup-vulkan-sdk@v1.2.0 with: From ea6818eb4ba67750df8f87fe47f889083650c22c Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 14 Apr 2024 13:13:19 +0300 Subject: [PATCH 30/60] HLE DSP: Formatting --- src/core/audio/dsp_core.cpp | 12 ++++++------ src/core/audio/hle_core.cpp | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/audio/dsp_core.cpp b/src/core/audio/dsp_core.cpp index ee134fa2f..01cee11ed 100644 --- a/src/core/audio/dsp_core.cpp +++ b/src/core/audio/dsp_core.cpp @@ -1,20 +1,20 @@ #include "audio/dsp_core.hpp" -#include "audio/hle_core.hpp" -#include "audio/null_core.hpp" -#include "audio/teakra_core.hpp" - #include #include #include +#include "audio/hle_core.hpp" +#include "audio/null_core.hpp" +#include "audio/teakra_core.hpp" + std::unique_ptr Audio::makeDSPCore(DSPCore::Type type, Memory& mem, Scheduler& scheduler, DSPService& dspService) { std::unique_ptr core; switch (type) { case DSPCore::Type::Null: core = std::make_unique(mem, scheduler, dspService); break; case DSPCore::Type::Teakra: core = std::make_unique(mem, scheduler, dspService); break; - case DSPCore::Type::HLE: core = std::make_unique(mem, scheduler, dspService); break; + case DSPCore::Type::HLE: core = std::make_unique(mem, scheduler, dspService); break; default: Helpers::warn("Invalid DSP core selected!"); @@ -47,7 +47,7 @@ const char* Audio::DSPCore::typeToString(Audio::DSPCore::Type type) { switch (type) { case Audio::DSPCore::Type::Null: return "null"; case Audio::DSPCore::Type::Teakra: return "teakra"; - case Audio::DSPCore::Type::HLE: return "hle"; + case Audio::DSPCore::Type::HLE: return "hle"; default: return "invalid"; } } diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index f9f1248ce..245894ce4 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -108,7 +108,7 @@ namespace Audio { u16 HLE_DSP::recvData(u32 regId) { if (regId != 0) { - Helpers::panic("Audio: invalid register in null frontend"); + Helpers::panic("Audio: invalid register in HLE frontend"); } return dspState == DSPState::On; From 1af7664efc96f88bee4d5e2d02808ad6c7aec19f Mon Sep 17 00:00:00 2001 From: Ishan09811 <156402647+Ishan09811@users.noreply.github.com> Date: Sat, 20 Apr 2024 15:38:57 +0530 Subject: [PATCH 31/60] Ci: some fixes (#496) * Windows_Build: automatic fetch build type for path * Hydra_Build: automatic fetch build_type for path --- .github/workflows/Hydra_Build.yml | 2 +- .github/workflows/Windows_Build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Hydra_Build.yml b/.github/workflows/Hydra_Build.yml index 3387d46d0..6ef72bf55 100644 --- a/.github/workflows/Hydra_Build.yml +++ b/.github/workflows/Hydra_Build.yml @@ -36,7 +36,7 @@ jobs: uses: actions/upload-artifact@v2 with: name: Windows core - path: '${{github.workspace}}/build/Release/Alber.dll' + path: '${{github.workspace}}/build/${{ env.BUILD_TYPE }}/Alber.dll' MacOS: diff --git a/.github/workflows/Windows_Build.yml b/.github/workflows/Windows_Build.yml index ae9fd5879..a06889ebe 100644 --- a/.github/workflows/Windows_Build.yml +++ b/.github/workflows/Windows_Build.yml @@ -43,4 +43,4 @@ jobs: uses: actions/upload-artifact@v2 with: name: Windows executable - path: './build/Release/Alber.exe' + path: './build/${{ env.BUILD_TYPE }}/Alber.exe' From b797c92b2ecec30d2d4dd43a3263d990c2ad7ecd Mon Sep 17 00:00:00 2001 From: PSI-Rockin Date: Mon, 22 Apr 2024 18:17:58 -0400 Subject: [PATCH 32/60] [CRO] Offset the old data address by the start of the CRO The old logic caused bad data relocations --- .gitignore | 1 + src/core/services/ldr_ro.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 786db9129..528462ad6 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ fb.bat *.smdh config.toml +CMakeSettings.json diff --git a/src/core/services/ldr_ro.cpp b/src/core/services/ldr_ro.cpp index 2ad62d99c..a61147299 100644 --- a/src/core/services/ldr_ro.cpp +++ b/src/core/services/ldr_ro.cpp @@ -437,7 +437,7 @@ class CRO { const u32 segmentID = mem.read32(segmentTable.offset + 12 * segment + SegmentTable::ID); switch (segmentID) { case SegmentTable::SegmentID::DATA: - *oldDataVaddr = segmentOffset + dataVaddr; oldDataSegmentOffset = segmentOffset; segmentOffset = dataVaddr; break; + *oldDataVaddr = segmentOffset + croPointer; oldDataSegmentOffset = segmentOffset; segmentOffset = dataVaddr; break; case SegmentTable::SegmentID::BSS: segmentOffset = bssVaddr; break; case SegmentTable::SegmentID::TEXT: case SegmentTable::SegmentID::RODATA: From 8c296905f4a84409eafd98f5b2f7da62929b2814 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 26 Apr 2024 00:54:21 +0300 Subject: [PATCH 33/60] Qt: Add support for dumping DSP firmware --- include/panda_qt/main_window.hpp | 1 + include/services/dsp.hpp | 20 +++++++++++--- src/core/services/dsp.cpp | 25 +++++++++++++++--- src/panda_qt/main_window.cpp | 45 ++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index 208da2c37..7e93bdf61 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -106,6 +106,7 @@ class MainWindow : public QMainWindow { void emuThreadMainLoop(); void selectLuaFile(); void selectROM(); + void dumpDspFirmware(); void dumpRomFS(); void openLuaEditor(); void openCheatsEditor(); diff --git a/include/services/dsp.hpp b/include/services/dsp.hpp index bc27377d3..5cbd4fd5c 100644 --- a/include/services/dsp.hpp +++ b/include/services/dsp.hpp @@ -1,6 +1,9 @@ #pragma once #include +#include #include +#include + #include "audio/dsp_core.hpp" #include "helpers.hpp" #include "logger.hpp" @@ -34,9 +37,10 @@ class DSPService { // Total number of DSP service events registered with registerInterruptEvents size_t totalEventCount; + std::vector loadedComponent; // Service functions - void convertProcessAddressFromDspDram(u32 messagePointer); // Nice function name + void convertProcessAddressFromDspDram(u32 messagePointer); // Nice function name void flushDataCache(u32 messagePointer); void getHeadphoneStatus(u32 messagePointer); void getSemaphoreEventHandle(u32 messagePointer); @@ -51,23 +55,31 @@ class DSPService { void unloadComponent(u32 messagePointer); void writeProcessPipe(u32 messagePointer); -public: + public: DSPService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {} void reset(); void handleSyncRequest(u32 messagePointer); void setDSPCore(Audio::DSPCore* pointer) { dsp = pointer; } - + // Special callback that's ran when the semaphore event is signalled void onSemaphoreEventSignal() { dsp->setSemaphore(semaphoreMask); } enum class SoundOutputMode : u8 { Mono = 0, Stereo = 1, - Surround = 2 + Surround = 2, + }; + + enum class ComponentDumpResult : u8 { + Success = 0, + NotLoaded, + FileFailure, }; void triggerPipeEvent(int index); void triggerSemaphoreEvent(); void triggerInterrupt0(); void triggerInterrupt1(); + + ComponentDumpResult dumpComponent(const std::filesystem::path& path); }; \ No newline at end of file diff --git a/src/core/services/dsp.cpp b/src/core/services/dsp.cpp index 33c1703d1..8c5147611 100644 --- a/src/core/services/dsp.cpp +++ b/src/core/services/dsp.cpp @@ -3,6 +3,7 @@ #include "kernel.hpp" #include +#include namespace DSPCommands { enum : u32 { @@ -41,6 +42,8 @@ void DSPService::reset() { for (DSPEvent& e : pipeEvents) { e = std::nullopt; } + + loadedComponent.clear(); } void DSPService::handleSyncRequest(u32 messagePointer) { @@ -80,13 +83,14 @@ void DSPService::loadComponent(u32 messagePointer) { u32 dataMask = mem.read32(messagePointer + 12); u32 buffer = mem.read32(messagePointer + 20); - std::vector data(size); + loadedComponent.resize(size); + for (u32 i = 0; i < size; i++) { - data[i] = mem.read8(buffer + i); + loadedComponent[i] = mem.read8(buffer + i); } log("DSP::LoadComponent (size = %08X, program mask = %X, data mask = %X\n", size, programMask, dataMask); - dsp->loadComponent(data, programMask, dataMask); + dsp->loadComponent(loadedComponent, programMask, dataMask); mem.write32(messagePointer, IPC::responseHeader(0x11, 2, 2)); mem.write32(messagePointer + 4, Result::Success); @@ -262,6 +266,21 @@ void DSPService::invalidateDCache(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } +DSPService::ComponentDumpResult DSPService::dumpComponent(const std::filesystem::path& path) { + if (loadedComponent.empty()) { + return ComponentDumpResult::NotLoaded; + } + + std::ofstream file(path, std::ios::out | std::ios::binary); + if (file.bad()) { + return ComponentDumpResult::FileFailure; + } + + file.write((char*)&loadedComponent[0], loadedComponent.size() * sizeof(u8)); + file.close(); + return ComponentDumpResult::Success; +} + void DSPService::triggerPipeEvent(int index) { if (index < pipeCount && pipeEvents[index].has_value()) { kernel.signalEvent(*pipeEvents[index]); diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index a4fc20f02..da9d27062 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -9,6 +9,7 @@ #include "cheats.hpp" #include "input_mappings.hpp" +#include "services/dsp.hpp" MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), keyboardMappings(InputMappings::defaultKeyboardMappings()), screen(this) { setWindowTitle("Alber"); @@ -53,9 +54,12 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) auto dumpRomFSAction = toolsMenu->addAction(tr("Dump RomFS")); auto luaEditorAction = toolsMenu->addAction(tr("Open Lua Editor")); auto cheatsEditorAction = toolsMenu->addAction(tr("Open Cheats Editor")); + auto dumpDspFirmware = toolsMenu->addAction(tr("Dump loaded DSP firmware")); + connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS); connect(luaEditorAction, &QAction::triggered, this, &MainWindow::openLuaEditor); connect(cheatsEditorAction, &QAction::triggered, this, &MainWindow::openCheatsEditor); + connect(dumpDspFirmware, &QAction::triggered, this, &MainWindow::dumpDspFirmware); auto aboutAction = aboutMenu->addAction(tr("About Panda3DS")); connect(aboutAction, &QAction::triggered, this, &MainWindow::showAboutMenu); @@ -241,6 +245,47 @@ void MainWindow::dumpRomFS() { } } +void MainWindow::dumpDspFirmware() { + auto file = QFileDialog::getSaveFileName(this, tr("Select file"), "", tr("DSP firmware file (*.cdc)")); + + if (file.isEmpty()) { + return; + } + std::filesystem::path path(file.toStdU16String()); + + messageQueueMutex.lock(); + auto res = emu->getServiceManager().getDSP().dumpComponent(path); + messageQueueMutex.unlock(); + + switch (res) { + case DSPService::ComponentDumpResult::Success: break; + case DSPService::ComponentDumpResult::NotLoaded: { + QMessageBox messageBox( + QMessageBox::Icon::Warning, tr("No DSP firmware loaded"), + tr("The currently loaded app has not uploaded a firmware to the DSP") + ); + + QAbstractButton* button = messageBox.addButton(tr("OK"), QMessageBox::ButtonRole::YesRole); + button->setIcon(QIcon(":/docs/img/rsob_icon.png")); + messageBox.exec(); + break; + } + + case DSPService::ComponentDumpResult::FileFailure: { + QMessageBox messageBox( + QMessageBox::Icon::Warning, tr("Failed to open output file"), + tr("The currently loaded DSP firmware could not be written to the selected file. Please make sure you have permission to access this " + "file") + ); + + QAbstractButton* button = messageBox.addButton(tr("OK"), QMessageBox::ButtonRole::YesRole); + button->setIcon(QIcon(":/docs/img/rstarstruck_icon.png")); + messageBox.exec(); + break; + } + } +} + void MainWindow::showAboutMenu() { AboutWindow about(this); about.exec(); From 01875e080a269cf422789a3492b3bd672a331275 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 26 Apr 2024 01:21:02 +0300 Subject: [PATCH 34/60] CI: Switch to MacOS 13 --- .github/workflows/Hydra_Build.yml | 2 +- .github/workflows/MacOS_Build.yml | 2 +- .github/workflows/Qt_Build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Hydra_Build.yml b/.github/workflows/Hydra_Build.yml index 3387d46d0..1664d7d27 100644 --- a/.github/workflows/Hydra_Build.yml +++ b/.github/workflows/Hydra_Build.yml @@ -40,7 +40,7 @@ jobs: MacOS: - runs-on: macos-latest + runs-on: macos-13 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/MacOS_Build.yml b/.github/workflows/MacOS_Build.yml index b659e3fa9..f6fafde99 100644 --- a/.github/workflows/MacOS_Build.yml +++ b/.github/workflows/MacOS_Build.yml @@ -16,7 +16,7 @@ jobs: # well on Windows or Mac. You can convert this to a matrix build if you need # cross-platform coverage. # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix - runs-on: macos-latest + runs-on: macos-13 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/Qt_Build.yml b/.github/workflows/Qt_Build.yml index 0b3910d7f..5e622c548 100644 --- a/.github/workflows/Qt_Build.yml +++ b/.github/workflows/Qt_Build.yml @@ -51,7 +51,7 @@ jobs: path: upload MacOS: - runs-on: macos-latest + runs-on: macos-13 steps: - uses: actions/checkout@v2 From 609eb6d880e808e7e1e5b8f0cb510a3a0bcd2065 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 26 Apr 2024 21:53:17 +0000 Subject: [PATCH 35/60] DSP HLE: Get ADPCM audio decoding working (#499) * Start decoding ADPCM * Fix accidentally skipping ADPCM samples * DSP HLE: ADPCM weights are signed * Format * Format * Fix broken amend --- include/audio/hle_core.hpp | 47 ++++++++++--- src/core/audio/hle_core.cpp | 127 +++++++++++++++++++++++++++++++++--- 2 files changed, 157 insertions(+), 17 deletions(-) diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index 6d7b3ad1d..b533727b4 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -1,6 +1,9 @@ #pragma once #include +#include +#include #include +#include #include #include "audio/dsp_core.hpp" @@ -18,7 +21,7 @@ namespace Audio { u32 paddr; // Physical address of the buffer u32 sampleCount; // Total number of samples u8 adpcmScale; // ADPCM predictor/scale - u8 pad1; // Unknown + u8 pad1; // Unknown std::array previousSamples; // ADPCM y[n-1] and y[n-2] bool adpcmDirty; @@ -39,17 +42,40 @@ namespace Audio { return this->bufferID > other.bufferID; } }; + // Buffer of decoded PCM16 samples. TODO: Are there better alternatives to use over deque? + using SampleBuffer = std::deque>; using BufferQueue = std::priority_queue; + BufferQueue buffers; std::array gain0, gain1, gain2; u16 syncCount; - bool enabled; - - BufferQueue buffers; + bool enabled; // Is the source enabled? + + // ADPCM decoding info: + // An array of fixed point S5.11 coefficients. These provide "weights" for the history samples + // The system describing how an ADPCM output sample is generated is + // y[n] = x[n] + 0.5 + coeff1 * y[n-1] + coeff2 * y[n-2] + // Where y[n] is the output sample we're generating, x[n] is the ADPCM "differential" of the current sample + // And coeff1/coeff2 are the coefficients from this array that are used for weighing the history samples + std::array adpcmCoefficients; + s16 history1; // y[n-1], the previous output sample + s16 history2; // y[n-2], the previous previous output sample + + SampleBuffer currentSamples; int index = 0; // Index of the voice in [0, 23] for debugging void reset(); + // Pop a buffer from the buffer queue and return it + Buffer popBuffer() { + assert(!buffers.empty()); + + Buffer ret = buffers.top(); + buffers.pop(); + + return ret; + } + DSPSource() { reset(); } }; @@ -61,7 +87,7 @@ namespace Audio { template using Frame = std::array, 160>; - + template using MonoFrame = Frame; @@ -72,6 +98,8 @@ namespace Audio { using QuadFrame = Frame; using Source = Audio::DSPSource; + using SampleBuffer = Source::SampleBuffer; + private: enum class DSPState : u32 { Off, @@ -91,7 +119,7 @@ namespace Audio { SourceType sourceType = SourceType::Stereo; void resetAudioPipe(); - bool loaded = false; // Have we loaded a component? + bool loaded = false; // Have we loaded a component? // Get the index for the current region we'll be reading. Returns the region with the highest frame counter // Accounting for whether one of the frame counters has wrapped around @@ -130,10 +158,13 @@ namespace Audio { } } - void updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config); + void updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config, s16_le* adpcmCoefficients); void generateFrame(StereoFrame& frame); void outputFrame(); - void dumpBuffer(const Source::Buffer& buffer); + // Decode an entire buffer worth of audio + void decodeBuffer(DSPSource& source); + SampleBuffer decodeADPCM(const u8* data, usize sampleCount, Source& source); + public: HLE_DSP(Memory& mem, Scheduler& scheduler, DSPService& dspService); ~HLE_DSP() override {} diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 245894ce4..4ee5a1dcd 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -1,5 +1,7 @@ #include "audio/hle_core.hpp" +#include +#include #include #include @@ -105,7 +107,7 @@ namespace Audio { outputFrame(); scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::cyclesPerFrame); } - + u16 HLE_DSP::recvData(u32 regId) { if (regId != 0) { Helpers::panic("Audio: invalid register in HLE frontend"); @@ -139,14 +141,11 @@ namespace Audio { // TODO: Other initialization stuff here dspState = DSPState::On; resetAudioPipe(); - - dspService.triggerPipeEvent(DSPPipeType::Audio); - break; - case StateChange::Shutdown: - dspState = DSPState::Off; + dspService.triggerPipeEvent(DSPPipeType::Audio); break; + case StateChange::Shutdown: dspState = DSPState::Off; break; default: Helpers::panic("Unimplemented DSP audio pipe state change %d", state); } } @@ -210,7 +209,7 @@ namespace Audio { // Update source configuration from the read region of shared memory auto& config = read.sourceConfigurations.config[i]; auto& source = sources[i]; - updateSourceConfig(source, config); + updateSourceConfig(source, config, read.adpcmCoefficients.coeff[i]); // Generate audio if (source.enabled && !source.buffers.empty()) { @@ -229,7 +228,7 @@ namespace Audio { } } - void HLE_DSP::updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config) { + void HLE_DSP::updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config, s16_le* adpcmCoefficients) { // Check if the any dirty bit is set, otherwise exit early if (!config.dirtyRaw) { return; @@ -245,6 +244,15 @@ namespace Audio { source.syncCount = config.syncCount; } + if (config.adpcmCoefficientsDirty) { + config.adpcmCoefficientsDirty = 0; + // Convert the ADPCM coefficients in DSP shared memory from s16_le to s16 and cache them in source.adpcmCoefficients + std::transform( + adpcmCoefficients, adpcmCoefficients + source.adpcmCoefficients.size(), source.adpcmCoefficients.begin(), + [](const s16_le& input) -> s16 { return s16(input); } + ); + } + if (config.resetFlag) { config.resetFlag = 0; source.reset(); @@ -254,7 +262,7 @@ namespace Audio { config.partialResetFlag = 0; source.buffers = {}; } - + // TODO: Should we check bufferQueueDirty here too? if (config.formatDirty || config.embeddedBufferDirty) { sampleFormat = config.format; @@ -302,6 +310,107 @@ namespace Audio { config.dirtyRaw = 0; } + void HLE_DSP::decodeBuffer(DSPSource& source) { + if (source.buffers.empty()) { + // No queued buffers, there's nothing to decode so return + return; + } + + DSPSource::Buffer buffer = source.popBuffer(); + if (buffer.adpcmDirty) { + source.history1 = buffer.previousSamples[0]; + source.history2 = buffer.previousSamples[1]; + } + + const u8* data = getPointerPhys(buffer.paddr); + if (data == nullptr) { + return; + } + + switch (buffer.format) { + case SampleFormat::PCM8: + case SampleFormat::PCM16: Helpers::warn("Unimplemented sample format!"); break; + + case SampleFormat::ADPCM: source.currentSamples = decodeADPCM(data, buffer.sampleCount, source); break; + default: Helpers::warn("Invalid DSP sample format"); break; + } + } + + HLE_DSP::SampleBuffer HLE_DSP::decodeADPCM(const u8* data, usize sampleCount, Source& source) { + static constexpr uint samplesPerBlock = 14; + // An ADPCM block is comprised of a single header which contains the scale and predictor value for the block, and then 14 4bpp samples (hence + // the / 2) + static constexpr usize blockSize = sizeof(u8) + samplesPerBlock / 2; + + // How many ADPCM blocks we'll be consuming. It's sampleCount / samplesPerBlock, rounded up. + const usize blockCount = (sampleCount + (samplesPerBlock - 1)) / samplesPerBlock; + const usize outputSize = sampleCount + (sampleCount & 1); // Bump the output size to a multiple of 2 + + usize outputCount = 0; // How many stereo samples have we output thus far? + SampleBuffer decodedSamples(outputSize); + + s16 history1 = source.history1; + s16 history2 = source.history2; + + // Decode samples in frames. Stop when we reach sampleCount samples + for (uint blockIndex = 0; blockIndex < blockCount; blockIndex++) { + const u8 scaleAndPredictor = *data++; + + const u32 scale = 1 << u32(scaleAndPredictor & 0xF); + // This is referred to as 4-bit in some documentation, but I am pretty sure that's a mistake + const u32 predictor = (scaleAndPredictor >> 4) & 0x7; + + // Fixed point (s5.11) coefficients for the history samples + const s32 weight1 = source.adpcmCoefficients[predictor * 2]; + const s32 weight2 = source.adpcmCoefficients[predictor * 2 + 1]; + + // Decode samples in batches of 2 + // Each 4 bit ADPCM differential corresponds to 1 mono sample which will be output from both the left and right channel + // So each byte of ADPCM data ends up generating 2 stereo samples + for (uint sampleIndex = 0; sampleIndex < samplesPerBlock && outputCount < sampleCount; sampleIndex += 2) { + const auto decode = [&](s32 nibble) -> s16 { + static constexpr s32 ONE = 0x800; // 1.0 in S5.11 fixed point + static constexpr s32 HALF = ONE / 2; // 0.5 similarly + + // Sign extend our nibble from s4 to s32 + nibble = (nibble << 28) >> 28; + + // Scale the extended nibble by the scale specified in the ADPCM block header, to get the real value of the sample's differential + const s32 diff = nibble * scale; + + // Convert ADPCM to PCM using y[n] = x[n] + 0.5 + coeff1 * y[n - 1] + coeff2 * y[n - 2] + // The coefficients are in s5.11 fixed point so we also perform the proper conversions + s32 output = ((diff << 11) + HALF + weight1 * history1 + weight2 * history2) >> 11; + output = std::clamp(output, -32768, 32767); + + // Write back new history samples + history2 = history1; // y[n-2] = y[n-1] + history1 = output; // y[n-1] = y[n] + + return s16(output); + }; + + const u8 samples = *data++; // Fetch the byte containing 2 4-bpp samples + const s32 topNibble = s32(samples) >> 4; // First sample + const s32 bottomNibble = s32(samples) & 0xF; // Second sample + + // Decode and write first sample, then the second one + const s16 sample1 = decode(topNibble); + decodedSamples[outputCount].fill(sample1); + + const s16 sample2 = decode(bottomNibble); + decodedSamples[outputCount + 1].fill(sample2); + + outputCount += 2; + } + } + + // Store new history samples in the DSP source and return samples + source.history1 = history1; + source.history2 = history2; + return decodedSamples; + } + void DSPSource::reset() { enabled = false; syncCount = 0; From 2eaaccd96b7ea479e0341a2a9cc2bc8e81336e9f Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 27 Apr 2024 00:56:35 +0300 Subject: [PATCH 36/60] Remove unused span include --- include/audio/hle_core.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index b533727b4..257ab5ac3 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include "audio/dsp_core.hpp" @@ -185,4 +184,4 @@ namespace Audio { void setSemaphoreMask(u16 value) override {} }; -} // namespace Audio \ No newline at end of file +} // namespace Audio From eab1a12b074c7b809b45b79bc13f1caf046114dd Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 29 Apr 2024 00:28:46 +0300 Subject: [PATCH 37/60] Integrate Capstone disassembler --- .gitmodules | 3 +++ CMakeLists.txt | 11 +++++++-- include/capstone.hpp | 53 ++++++++++++++++++++++++++++++++++++++++++++ src/lua.cpp | 43 ++++++++++++++++++++++++++++------- third_party/capstone | 1 + 5 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 include/capstone.hpp create mode 160000 third_party/capstone diff --git a/.gitmodules b/.gitmodules index 259800549..1f1d11fca 100644 --- a/.gitmodules +++ b/.gitmodules @@ -67,3 +67,6 @@ [submodule "third_party/Catch2"] path = third_party/Catch2 url = https://github.com/catchorg/Catch2.git +[submodule "third_party/capstone"] + path = third_party/capstone + url = https://github.com/capstone-engine/capstone diff --git a/CMakeLists.txt b/CMakeLists.txt index 3dc7e467d..6d3901b62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -153,8 +153,15 @@ if(HOST_X64 OR HOST_ARM64) else() message(FATAL_ERROR "Currently unsupported CPU architecture") endif() + add_subdirectory(third_party/teakra EXCLUDE_FROM_ALL) +set(CAPSTONE_ARCHITECTURE_DEFAULT OFF) +set(CAPSTONE_ARM_SUPPORT ON) +set(CAPSTONE_BUILD_MACOS_THIN ON) +add_subdirectory(third_party/capstone) +include_directories(third_party/capstone/include) + set(SOURCE_FILES src/emulator.cpp src/io_file.cpp src/config.cpp src/core/CPU/cpu_dynarmic.cpp src/core/CPU/dynarmic_cycles.cpp src/core/memory.cpp src/renderer.cpp src/core/renderer_null/renderer_null.cpp @@ -234,7 +241,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/scheduler.hpp include/applets/error_applet.hpp include/audio/dsp_core.hpp include/audio/null_core.hpp include/audio/teakra_core.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp - include/audio/hle_core.hpp + include/audio/hle_core.hpp include/capstone.hpp ) cmrc_add_resource_library( @@ -399,7 +406,7 @@ set(ALL_SOURCES ${SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERN target_sources(AlberCore PRIVATE ${ALL_SOURCES}) target_link_libraries(AlberCore PRIVATE dynarmic cryptopp glad resources_console_fonts teakra) -target_link_libraries(AlberCore PUBLIC glad) +target_link_libraries(AlberCore PUBLIC glad capstone) if(ENABLE_DISCORD_RPC AND NOT ANDROID) target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_DISCORD_RPC=1") diff --git a/include/capstone.hpp b/include/capstone.hpp new file mode 100644 index 000000000..e4d6b7d73 --- /dev/null +++ b/include/capstone.hpp @@ -0,0 +1,53 @@ +#pragma once +#include + +#include +#include +#include + +#include "helpers.hpp" + +namespace Common { + class CapstoneDisassembler { + csh handle; // Handle to our disassembler object + cs_insn* instructions = nullptr; // Pointer to instruction object + bool initialized = false; + + public: + void init(cs_arch arch, cs_mode mode) { initialized = (cs_open(arch, mode, &handle) == CS_ERR_OK); } + + CapstoneDisassembler() {} + CapstoneDisassembler(cs_arch arch, cs_mode mode) { init(arch, mode); } + + // Returns the number of instructions successfully disassembled + // pc: program counter of the instruction to disassemble + // bytes: Byte representation of instruction + // buffer: text buffer to output the disassembly too + usize disassemble(std::string& buffer, u32 pc, std::span bytes, u64 offset = 0) { + if (!initialized) { + buffer = "Capstone was not properly initialized"; + return 0; + } + + usize count = cs_disasm(handle, bytes.data(), bytes.size(), pc, offset, &instructions); + if (count == 0) { + // Error in disassembly, quit early and return empty string + buffer = "Error disassembling instructions with Capstone"; + return 0; + } + + buffer = ""; + for (usize i = 0; i < count; i++) { + buffer += std::string(instructions[i].mnemonic) + " " + std::string(instructions[i].op_str); + + if (i < count - 1) { + // Append newlines between instructions, sans the final instruction + buffer += "\n"; + } + } + + cs_free(instructions, count); + return count; + } + }; +} // namespace Common \ No newline at end of file diff --git a/src/lua.cpp b/src/lua.cpp index d12faf7ee..209ea32e5 100644 --- a/src/lua.cpp +++ b/src/lua.cpp @@ -1,10 +1,13 @@ #ifdef PANDA3DS_ENABLE_LUA +#include + +#include "capstone.hpp" #include "emulator.hpp" #include "lua_manager.hpp" #ifndef __ANDROID__ extern "C" { - #include "luv.h" +#include "luv.h" } #endif @@ -203,6 +206,27 @@ static int getButtonThunk(lua_State* L) { return 1; } +static int disassembleARMThunk(lua_State* L) { + static Common::CapstoneDisassembler disassembler(CS_ARCH_ARM, CS_MODE_ARM); + + const u32 pc = u32(lua_tonumber(L, 1)); + const u32 instruction = u32(lua_tonumber(L, 2)); + + std::string disassembly; + // Convert instruction to byte array to pass to Capstone + std::array bytes = { + instruction & 0xff, + (instruction >> 8) & 0xff, + (instruction >> 16) & 0xff, + (instruction >> 24) & 0xff, + }; + + disassembler.disassemble(disassembly, pc, std::span(bytes)); + lua_pushstring(L, disassembly.c_str()); + + return 1; +} + // clang-format off static constexpr luaL_Reg functions[] = { { "__read8", read8Thunk }, @@ -214,13 +238,14 @@ static constexpr luaL_Reg functions[] = { { "__write32", write32Thunk }, { "__write64", write64Thunk }, { "__getAppID", getAppIDThunk }, - { "__pause", pauseThunk}, - { "__resume", resumeThunk}, - { "__reset", resetThunk}, - { "__loadROM", loadROMThunk}, - { "__getButtons", getButtonsThunk}, - { "__getCirclepad", getCirclepadThunk}, - { "__getButton", getButtonThunk}, + { "__pause", pauseThunk }, + { "__resume", resumeThunk }, + { "__reset", resetThunk }, + { "__loadROM", loadROMThunk }, + { "__getButtons", getButtonsThunk }, + { "__getCirclepad", getCirclepadThunk }, + { "__getButton", getButtonThunk }, + { "__disassembleARM", disassembleARMThunk }, { nullptr, nullptr }, }; // clang-format on @@ -254,6 +279,8 @@ void LuaManager::initializeThunks() { getButton = function(button) return GLOBALS.__getButton(button) end, getCirclepad = function() return GLOBALS.__getCirclepad() end, + disassembleARM = function(pc, instruction) return GLOBALS.__disassembleARM(pc, instruction) end, + Frame = __Frame, ButtonA = __ButtonA, ButtonB = __ButtonB, diff --git a/third_party/capstone b/third_party/capstone new file mode 160000 index 000000000..eb4fc2d76 --- /dev/null +++ b/third_party/capstone @@ -0,0 +1 @@ +Subproject commit eb4fc2d7612db10379adf7aeb287a7923dcc0fc7 From 27828b13512ba8e482ec49486a82e25c84abc1b5 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 29 Apr 2024 00:46:22 +0300 Subject: [PATCH 38/60] Lua: Add Teak disassembler --- src/lua.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/lua.cpp b/src/lua.cpp index 209ea32e5..e53ae6a1a 100644 --- a/src/lua.cpp +++ b/src/lua.cpp @@ -1,4 +1,6 @@ #ifdef PANDA3DS_ENABLE_LUA +#include + #include #include "capstone.hpp" @@ -227,6 +229,15 @@ static int disassembleARMThunk(lua_State* L) { return 1; } +static int disassembleTeakThunk(lua_State* L) { + const u16 instruction = u16(lua_tonumber(L, 1)); + const u16 expansion = u16(lua_tonumber(L, 2)); + + std::string disassembly = Teakra::Disassembler::Do(instruction, expansion); + lua_pushstring(L, disassembly.c_str()); + return 1; +} + // clang-format off static constexpr luaL_Reg functions[] = { { "__read8", read8Thunk }, @@ -246,6 +257,7 @@ static constexpr luaL_Reg functions[] = { { "__getCirclepad", getCirclepadThunk }, { "__getButton", getButtonThunk }, { "__disassembleARM", disassembleARMThunk }, + { "__disassembleTeak", disassembleTeakThunk }, { nullptr, nullptr }, }; // clang-format on @@ -280,6 +292,7 @@ void LuaManager::initializeThunks() { getCirclepad = function() return GLOBALS.__getCirclepad() end, disassembleARM = function(pc, instruction) return GLOBALS.__disassembleARM(pc, instruction) end, + disassembleTeak = function(opcode, exp) return GLOBALS.__disassembleTeak(opcode, exp or 0) end, Frame = __Frame, ButtonA = __ButtonA, From 5161ef8ff3808ce1eb83804f6be34cb5aaadb6a1 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 29 Apr 2024 00:48:11 +0300 Subject: [PATCH 39/60] Avoid narrowing conversions in initializer list --- src/lua.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lua.cpp b/src/lua.cpp index e53ae6a1a..e835d4638 100644 --- a/src/lua.cpp +++ b/src/lua.cpp @@ -217,10 +217,10 @@ static int disassembleARMThunk(lua_State* L) { std::string disassembly; // Convert instruction to byte array to pass to Capstone std::array bytes = { - instruction & 0xff, - (instruction >> 8) & 0xff, - (instruction >> 16) & 0xff, - (instruction >> 24) & 0xff, + u8(instruction & 0xff), + u8((instruction >> 8) & 0xff), + u8((instruction >> 16) & 0xff), + u8((instruction >> 24) & 0xff), }; disassembler.disassemble(disassembly, pc, std::span(bytes)); From f77c9720a628b4d4d719ec2c2a2faaf79184f189 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 29 Apr 2024 01:08:04 +0300 Subject: [PATCH 40/60] Avoid unnecessarily initializing disassembler --- include/capstone.hpp | 1 + src/lua.cpp | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/include/capstone.hpp b/include/capstone.hpp index e4d6b7d73..c08a41697 100644 --- a/include/capstone.hpp +++ b/include/capstone.hpp @@ -14,6 +14,7 @@ namespace Common { bool initialized = false; public: + bool isInitialized() { return initialized; } void init(cs_arch arch, cs_mode mode) { initialized = (cs_open(arch, mode, &handle) == CS_ERR_OK); } CapstoneDisassembler() {} diff --git a/src/lua.cpp b/src/lua.cpp index e835d4638..6a16ab5b3 100644 --- a/src/lua.cpp +++ b/src/lua.cpp @@ -209,7 +209,11 @@ static int getButtonThunk(lua_State* L) { } static int disassembleARMThunk(lua_State* L) { - static Common::CapstoneDisassembler disassembler(CS_ARCH_ARM, CS_MODE_ARM); + static Common::CapstoneDisassembler disassembler; + // We want the disassembler to only be fully initialized when this function is first used + if (!disassembler.isInitialized()) { + disassembler.init(CS_ARCH_ARM, CS_MODE_ARM); + } const u32 pc = u32(lua_tonumber(L, 1)); const u32 instruction = u32(lua_tonumber(L, 2)); From 88e8491c7f51aebe125f613923cfa399520e95ec Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 29 Apr 2024 13:59:46 +0300 Subject: [PATCH 41/60] CapstoneDisassembler: Remove outdated cstdio include --- include/capstone.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/capstone.hpp b/include/capstone.hpp index c08a41697..32ca404f2 100644 --- a/include/capstone.hpp +++ b/include/capstone.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -51,4 +50,4 @@ namespace Common { return count; } }; -} // namespace Common \ No newline at end of file +} // namespace Common From f004aa6021ece76bc74f9275d1a99083951c0da6 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 29 Apr 2024 23:01:03 +0300 Subject: [PATCH 42/60] Implement GPUREG_VSH_OUTMAP_MASK --- include/PICA/gpu.hpp | 31 +++++++++++++++++++++++++++++++ include/PICA/regs.hpp | 1 + src/core/PICA/gpu.cpp | 11 +++++++++-- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/include/PICA/gpu.hpp b/include/PICA/gpu.hpp index 2336493c7..61020f768 100644 --- a/include/PICA/gpu.hpp +++ b/include/PICA/gpu.hpp @@ -6,6 +6,7 @@ #include "PICA/pica_vertex.hpp" #include "PICA/regs.hpp" #include "PICA/shader_unit.hpp" +#include "compiler_builtins.hpp" #include "config.hpp" #include "helpers.hpp" #include "logger.hpp" @@ -35,6 +36,12 @@ class GPU { std::array immediateModeAttributes; // Vertex attributes uploaded via immediate mode submission std::array immediateModeVertices; + + // Pointers for the output registers as arranged after GPUREG_VSH_OUTMAP_MASK is applied + std::array vsOutputRegisters; + // Previous value for GPUREG_VSH_OUTMAP_MASK + u32 oldVsOutputMask; + uint immediateModeVertIndex; uint immediateModeAttrIndex; // Index of the immediate mode attribute we're uploading @@ -167,4 +174,28 @@ class GPU { // We have them in the end of the struct for cache locality reasons. Tl;dr we want the more commonly used things to be packed in the start // Of the struct, instead of externalRegs being in the middle ExternalRegisters externalRegs; + + ALWAYS_INLINE void setVsOutputMask(u32 val) { + val &= 0xffff; + + // Avoid recomputing this if not necessary + if (oldVsOutputMask != val) [[unlikely]] { + oldVsOutputMask = val; + + uint count = 0; + // See which registers are actually enabled and ignore the disabled ones + for (int i = 0; i < 16; i++) { + if (val & 1) { + vsOutputRegisters[count++] = &shaderUnit.vs.outputs[i][0]; + } + + val >>= 1; + } + + // For the others, map the index to a vs output directly (TODO: What does hw actually do?) + for (; count < 16; count++) { + vsOutputRegisters[count] = &shaderUnit.vs.outputs[count][0]; + } + } + } }; diff --git a/include/PICA/regs.hpp b/include/PICA/regs.hpp index 70cecf7b3..4342ebe5f 100644 --- a/include/PICA/regs.hpp +++ b/include/PICA/regs.hpp @@ -143,6 +143,7 @@ namespace PICA { VertexIntUniform3 = 0x2B4, VertexShaderEntrypoint = 0x2BA, + VertexShaderOutputMask = 0x2BD, VertexShaderTransferEnd = 0x2BF, VertexFloatUniformIndex = 0x2C0, VertexFloatUniformData0 = 0x2C1, diff --git a/src/core/PICA/gpu.cpp b/src/core/PICA/gpu.cpp index c04993821..a777d0a30 100644 --- a/src/core/PICA/gpu.cpp +++ b/src/core/PICA/gpu.cpp @@ -77,6 +77,9 @@ void GPU::reset() { fixedAttrBuff.fill(0); + oldVsOutputMask = 0; + setVsOutputMask(0xFFFF); + for (auto& e : attributeInfo) { e.offset = 0; e.size = 0; @@ -134,6 +137,8 @@ void GPU::drawArrays() { shaderJIT.prepare(shaderUnit.vs); } + setVsOutputMask(regs[PICA::InternalRegs::VertexShaderOutputMask]); + // Base address for vertex attributes // The vertex base is always on a quadword boundary because the PICA does weird alignment shit any time possible const u32 vertexBase = ((regs[PICA::InternalRegs::VertexAttribLoc] >> 1) & 0xfffffff) * 16; @@ -329,7 +334,7 @@ void GPU::drawArrays() { for (int j = 0; j < 4; j++) { // pls unroll const u32 mapping = (config >> (j * 8)) & 0x1F; - out.raw[mapping] = shaderUnit.vs.outputs[i][j]; + out.raw[mapping] = vsOutputRegisters[i][j]; } } } @@ -338,6 +343,8 @@ void GPU::drawArrays() { } PICA::Vertex GPU::getImmediateModeVertex() { + setVsOutputMask(regs[PICA::InternalRegs::VertexShaderOutputMask]); + PICA::Vertex v; const int totalAttrCount = (regs[PICA::InternalRegs::VertexShaderAttrNum] & 0xf) + 1; @@ -356,7 +363,7 @@ PICA::Vertex GPU::getImmediateModeVertex() { for (int j = 0; j < 4; j++) { // pls unroll const u32 mapping = (config >> (j * 8)) & 0x1F; - v.raw[mapping] = shaderUnit.vs.outputs[i][j]; + v.raw[mapping] = vsOutputRegisters[i][j]; } } From 2fc9c0a5737262fd78ebc29cb054ba98d1c35c6f Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 28 Apr 2024 16:58:42 +0300 Subject: [PATCH 43/60] DSP HLE: Broken PCM16 and handle DSP voice status better --- include/audio/dsp_shared_mem.hpp | 2 +- include/audio/hle_core.hpp | 25 ++++-- src/core/audio/hle_core.cpp | 134 +++++++++++++++++++++++++++---- 3 files changed, 137 insertions(+), 24 deletions(-) diff --git a/include/audio/dsp_shared_mem.hpp b/include/audio/dsp_shared_mem.hpp index 148986f9d..25806ea1c 100644 --- a/include/audio/dsp_shared_mem.hpp +++ b/include/audio/dsp_shared_mem.hpp @@ -297,7 +297,7 @@ namespace Audio::HLE { u8 isEnabled; ///< Is this channel enabled? (Doesn't have to be playing anything.) u8 currentBufferIDDirty; ///< Non-zero when current_buffer_id changes u16_le syncCount; ///< Is set by the DSP to the value of SourceConfiguration::sync_count - u32_dsp bufferPosition; ///< Number of samples into the current buffer + u32_dsp samplePosition; ///< Number of samples into the current buffer u16_le currentBufferID; ///< Updated when a buffer finishes playing u16_le lastBufferID; ///< Updated when all buffers in the queue finish playing }; diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index 257ab5ac3..cee2b0c83 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -32,8 +32,8 @@ namespace Audio { SampleFormat format; SourceType sourceType; - bool fromQueue = false; // Is this buffer from the buffer queue or an embedded buffer? - bool hasPlayedOnce = false; // Has the buffer been played at least once before? + bool fromQueue = false; // Is this buffer from the buffer queue or an embedded buffer? + bool hasPlayedOnce = false; // Has the buffer been played at least once before? bool operator<(const Buffer& other) const { // Lower ID = Higher priority @@ -47,9 +47,17 @@ namespace Audio { using BufferQueue = std::priority_queue; BufferQueue buffers; + SampleFormat sampleFormat = SampleFormat::ADPCM; + SourceType sourceType = SourceType::Stereo; + std::array gain0, gain1, gain2; + u32 samplePosition; // Sample number into the current audio buffer u16 syncCount; - bool enabled; // Is the source enabled? + u16 currentBufferID; + u16 previousBufferID; + + bool enabled; // Is the source enabled? + bool isBufferIDDirty = false; // Did we change buffers? // ADPCM decoding info: // An array of fixed point S5.11 coefficients. These provide "weights" for the history samples @@ -65,6 +73,10 @@ namespace Audio { int index = 0; // Index of the voice in [0, 23] for debugging void reset(); + + // Push a buffer to the buffer queue + void pushBuffer(const Buffer& buffer) { buffers.push(buffer); } + // Pop a buffer from the buffer queue and return it Buffer popBuffer() { assert(!buffers.empty()); @@ -114,9 +126,6 @@ namespace Audio { std::array sources; // DSP voices Audio::HLE::DspMemory dspRam; - SampleFormat sampleFormat = SampleFormat::ADPCM; - SourceType sourceType = SourceType::Stereo; - void resetAudioPipe(); bool loaded = false; // Have we loaded a component? @@ -159,9 +168,13 @@ namespace Audio { void updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config, s16_le* adpcmCoefficients); void generateFrame(StereoFrame& frame); + void generateFrame(DSPSource& source); void outputFrame(); + // Decode an entire buffer worth of audio void decodeBuffer(DSPSource& source); + + SampleBuffer decodePCM16(const u8* data, usize sampleCount, Source& source); SampleBuffer decodeADPCM(const u8* data, usize sampleCount, Source& source); public: diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 4ee5a1dcd..e92432b5b 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -7,6 +7,8 @@ #include "services/dsp.hpp" +std::vector> samplezorz = {}; + namespace Audio { namespace DSPPipeType { enum : u32 { @@ -64,10 +66,6 @@ namespace Audio { dspState = DSPState::Off; loaded = false; - // Initialize these to some sane defaults - sampleFormat = SampleFormat::ADPCM; - sourceType = SourceType::Stereo; - for (auto& e : pipeData) { e.clear(); } @@ -212,12 +210,16 @@ namespace Audio { updateSourceConfig(source, config, read.adpcmCoefficients.coeff[i]); // Generate audio - if (source.enabled && !source.buffers.empty()) { - const auto& buffer = source.buffers.top(); - const u8* data = getPointerPhys(buffer.paddr); + if (source.enabled) { + generateFrame(source); + + if (samplezorz.size() > 160 * 60 * 60 * 3) { + using namespace std; + ofstream fout("audio_data.bin", ios::out | ios::binary); + fout.write((char*)&samplezorz[0], samplezorz.size() * sizeof(Sample)); + fout.close(); - if (data != nullptr) { - // TODO + Helpers::panic("Bwaa"); } } @@ -225,6 +227,13 @@ namespace Audio { auto& status = write.sourceStatuses.status[i]; status.isEnabled = source.enabled; status.syncCount = source.syncCount; + status.currentBufferIDDirty = source.isBufferIDDirty ? 1 : 0; + status.currentBufferID = source.currentBufferID; + status.lastBufferID = source.previousBufferID; + // TODO: Properly update sample position + status.samplePosition = source.samplePosition; + + source.isBufferIDDirty = false; } } @@ -265,11 +274,11 @@ namespace Audio { // TODO: Should we check bufferQueueDirty here too? if (config.formatDirty || config.embeddedBufferDirty) { - sampleFormat = config.format; + source.sampleFormat = config.format; } if (config.monoOrStereoDirty || config.embeddedBufferDirty) { - sourceType = config.monoOrStereo; + source.sourceType = config.monoOrStereo; } if (config.embeddedBufferDirty) { @@ -285,8 +294,8 @@ namespace Audio { .looping = config.isLooping != 0, .bufferID = config.bufferID, .playPosition = config.playPosition, - .format = sampleFormat, - .sourceType = sourceType, + .format = source.sampleFormat, + .sourceType = source.sourceType, .fromQueue = false, .hasPlayedOnce = false, }; @@ -327,15 +336,97 @@ namespace Audio { return; } - switch (buffer.format) { - case SampleFormat::PCM8: - case SampleFormat::PCM16: Helpers::warn("Unimplemented sample format!"); break; + source.currentBufferID = buffer.bufferID; + source.previousBufferID = 0; + // For looping buffers, this is only set for the first time we play it. Loops do not set the dirty bit. + source.isBufferIDDirty = !buffer.hasPlayedOnce && buffer.fromQueue; + + if (buffer.hasPlayedOnce) { + source.samplePosition = 0; + } else { + // Mark that the buffer has already been played once, needed for looping buffers + buffer.hasPlayedOnce = true; + // Play position is only used for the initial time the buffer is played. Loops will start from the beginning of the buffer. + source.samplePosition = buffer.playPosition; + } + switch (buffer.format) { + case SampleFormat::PCM8: Helpers::warn("Unimplemented sample format!"); break; + case SampleFormat::PCM16: source.currentSamples = decodePCM16(data, buffer.sampleCount, source); break; case SampleFormat::ADPCM: source.currentSamples = decodeADPCM(data, buffer.sampleCount, source); break; - default: Helpers::warn("Invalid DSP sample format"); break; + + default: + Helpers::warn("Invalid DSP sample format"); + source.currentSamples = {}; + break; + } + + // If the buffer is a looping buffer, re-push it + if (buffer.looping) { + source.pushBuffer(buffer); } } + void HLE_DSP::generateFrame(DSPSource& source) { + if (source.currentSamples.empty()) { + // There's no audio left to play, turn the voice off + if (source.buffers.empty()) { + source.enabled = false; + source.isBufferIDDirty = true; + source.previousBufferID = source.currentBufferID; + source.currentBufferID = 0; + + return; + } + + decodeBuffer(source); + } else { + constexpr uint maxSampleCount = Audio::samplesInFrame; + uint outputCount = 0; + + while (outputCount < maxSampleCount) { + if (source.currentSamples.empty()) { + if (source.buffers.empty()) { + break; + } else { + decodeBuffer(source); + } + } + + const uint sampleCount = std::min(maxSampleCount - outputCount, source.currentSamples.size()); + samplezorz.insert(samplezorz.end(), source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); + source.currentSamples.erase(source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); + + outputCount += sampleCount; + } + } + } + + HLE_DSP::SampleBuffer HLE_DSP::decodePCM16(const u8* data, usize sampleCount, Source& source) { + SampleBuffer decodedSamples(sampleCount); + const s16* data16 = reinterpret_cast(data); + + if (source.sourceType == SourceType::Stereo) { + for (usize i = 0; i < sampleCount; i++) { + s16 left = *data16++; + s16 right = *data16++; + + if (left != 0 || right != 0) { + Helpers::panic("panda..."); + } + + decodedSamples[i] = {left, right}; + } + } else { + // Mono + for (usize i = 0; i < sampleCount; i++) { + decodedSamples[i].fill(*data16++); + } + } + + return decodedSamples; + } + HLE_DSP::SampleBuffer HLE_DSP::decodeADPCM(const u8* data, usize sampleCount, Source& source) { static constexpr uint samplesPerBlock = 14; // An ADPCM block is comprised of a single header which contains the scale and predictor value for the block, and then 14 4bpp samples (hence @@ -413,6 +504,15 @@ namespace Audio { void DSPSource::reset() { enabled = false; + isBufferIDDirty = false; + + // Initialize these to some sane defaults + sampleFormat = SampleFormat::ADPCM; + sourceType = SourceType::Stereo; + + samplePosition = 0; + previousBufferID = 0; + currentBufferID = 0; syncCount = 0; buffers = {}; From fb8130a868897627f2d22ce270a454d434d67e8a Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 30 Apr 2024 21:56:39 +0300 Subject: [PATCH 44/60] HLE DSP: Remove debug artifacts --- src/core/audio/hle_core.cpp | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index e92432b5b..d6ba21ec7 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -7,8 +7,6 @@ #include "services/dsp.hpp" -std::vector> samplezorz = {}; - namespace Audio { namespace DSPPipeType { enum : u32 { @@ -102,6 +100,7 @@ namespace Audio { dspService.triggerPipeEvent(DSPPipeType::Audio); } + // TODO: Should this be called if dspState != DSPState::On? outputFrame(); scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::cyclesPerFrame); } @@ -212,15 +211,6 @@ namespace Audio { // Generate audio if (source.enabled) { generateFrame(source); - - if (samplezorz.size() > 160 * 60 * 60 * 3) { - using namespace std; - ofstream fout("audio_data.bin", ios::out | ios::binary); - fout.write((char*)&samplezorz[0], samplezorz.size() * sizeof(Sample)); - fout.close(); - - Helpers::panic("Bwaa"); - } } // Update write region of shared memory @@ -394,7 +384,7 @@ namespace Audio { } const uint sampleCount = std::min(maxSampleCount - outputCount, source.currentSamples.size()); - samplezorz.insert(samplezorz.end(), source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); + // samples.insert(samples.end(), source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); source.currentSamples.erase(source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); outputCount += sampleCount; @@ -408,19 +398,15 @@ namespace Audio { if (source.sourceType == SourceType::Stereo) { for (usize i = 0; i < sampleCount; i++) { - s16 left = *data16++; - s16 right = *data16++; - - if (left != 0 || right != 0) { - Helpers::panic("panda..."); - } - + const s16 left = *data16++; + const s16 right = *data16++; decodedSamples[i] = {left, right}; } } else { // Mono for (usize i = 0; i < sampleCount; i++) { - decodedSamples[i].fill(*data16++); + const s16 sample = *data16++; + decodedSamples[i] = {sample, sample}; } } From 0490c6753fb4c56fc4b9835b504618e6d8101f18 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 1 May 2024 01:56:17 +0300 Subject: [PATCH 45/60] HLE DSP: Stub AAC --- CMakeLists.txt | 2 +- include/audio/aac.hpp | 71 +++++++++++++++++++++++++++++++++++++ include/audio/hle_core.hpp | 2 ++ src/core/audio/hle_core.cpp | 47 ++++++++++++++++++++++-- 4 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 include/audio/aac.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d3901b62..48a2a0db8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -241,7 +241,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/scheduler.hpp include/applets/error_applet.hpp include/audio/dsp_core.hpp include/audio/null_core.hpp include/audio/teakra_core.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp - include/audio/hle_core.hpp include/capstone.hpp + include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp ) cmrc_add_resource_library( diff --git a/include/audio/aac.hpp b/include/audio/aac.hpp new file mode 100644 index 000000000..c780e6d20 --- /dev/null +++ b/include/audio/aac.hpp @@ -0,0 +1,71 @@ +#pragma once +#include +#include + +#include "helpers.hpp" +#include "swap.hpp" + +namespace Audio::AAC { + namespace ResultCode { + enum : u32 { + Success = 0, + }; + } + + // Enum values from Citra and struct definitions based off Citra + namespace Command { + enum : u16 { + Init = 0, // Initialize encoder/decoder + EncodeDecode = 1, // Encode/Decode AAC + Shutdown = 2, // Shutdown encoder/decoder + LoadState = 3, + SaveState = 4, + }; + } + + namespace SampleRate { + enum : u32 { + Rate48000 = 0, + Rate44100 = 1, + Rate32000 = 2, + Rate24000 = 3, + Rate22050 = 4, + Rate16000 = 5, + Rate12000 = 6, + Rate11025 = 7, + Rate8000 = 8, + }; + } + + namespace Mode { + enum : u16 { + None = 0, + Decode = 1, + Encode = 2, + }; + } + + struct DecodeResponse { + u32_le sampleRate = SampleRate::Rate48000; + u32_le channelCount = 0; + u32_le size = 0; + u32_le unknown1 = 0; + u32_le unknown2 = 0; + u32_le sampleCount = 0; + }; + + struct Message { + u16_le mode = Mode::None; // Encode or decode AAC? + u16_le command = Command::Init; + u32_le resultCode = ResultCode::Success; + + // Info on the AAC request + union { + std::array commandData = {}; + DecodeResponse decodeResponse; + }; + }; + + static_assert(sizeof(Message) == 32); + static_assert(std::is_trivially_copyable()); +} // namespace Audio::AAC \ No newline at end of file diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index cee2b0c83..c57f221e0 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -5,6 +5,7 @@ #include #include +#include "audio/aac.hpp" #include "audio/dsp_core.hpp" #include "audio/dsp_shared_mem.hpp" #include "memory.hpp" @@ -166,6 +167,7 @@ namespace Audio { } } + void handleAACRequest(const AAC::Message& request); void updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config, s16_le* adpcmCoefficients); void generateFrame(StereoFrame& frame); void generateFrame(DSPSource& source); diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index d6ba21ec7..98d07ce64 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -149,12 +149,24 @@ namespace Audio { break; } - case DSPPipeType::Binary: - Helpers::warn("Unimplemented write to binary pipe! Size: %d\n", size); + case DSPPipeType::Binary: { + log("Unimplemented write to binary pipe! Size: %d\n", size); + + AAC::Message request; + if (size == sizeof(request)) { + std::array raw; + for (uint i = 0; i < size; i++) { + raw[i] = mem.read32(buffer + i); + } + + std::memcpy(&request, raw.data(), sizeof(request)); + handleAACRequest(request); + } // This pipe and interrupt are normally used for requests like AAC decode dspService.triggerPipeEvent(DSPPipeType::Binary); break; + } default: log("Audio::HLE_DSP: Wrote to unimplemented pipe %d\n", channel); break; } @@ -488,6 +500,37 @@ namespace Audio { return decodedSamples; } + void HLE_DSP::handleAACRequest(const AAC::Message& request) { + AAC::Message response = {}; + + switch (request.command) { + case AAC::Command::EncodeDecode: + // Dummy response to stop games from hanging + // TODO: Fix this when implementing AAC + response.resultCode = AAC::ResultCode::Success; + response.decodeResponse.channelCount = 2; + response.decodeResponse.sampleCount = 1024; + response.decodeResponse.size = 0; + response.decodeResponse.sampleRate = AAC::SampleRate::Rate48000; + break; + + case AAC::Command::Init: + case AAC::Command::Shutdown: + case AAC::Command::LoadState: + case AAC::Command::SaveState: + response = request; + response.resultCode = AAC::ResultCode::Success; + break; + + default: Helpers::warn("Unknown AAC command type"); break; + } + + // Copy response data to the binary pipe + auto& pipe = pipeData[DSPPipeType::Binary]; + pipe.resize(sizeof(response)); + std::memcpy(&pipe[0], &response, sizeof(response)); + } + void DSPSource::reset() { enabled = false; isBufferIDDirty = false; From ad380b8c5ae9bb8e9a2ce5bdd470a31f04385f20 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 1 May 2024 01:59:32 +0300 Subject: [PATCH 46/60] Warn on invalid AAC request --- src/core/audio/hle_core.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 98d07ce64..e38d4821a 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -161,6 +161,8 @@ namespace Audio { std::memcpy(&request, raw.data(), sizeof(request)); handleAACRequest(request); + } else { + Helpers::warn("Invalid size for AAC request"); } // This pipe and interrupt are normally used for requests like AAC decode From e4b81d61a46816116808d868eb3d39b96313acb4 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 1 May 2024 16:10:51 +0300 Subject: [PATCH 47/60] HLE DSP: Fix AAC response stub --- include/audio/dsp_shared_mem.hpp | 4 ++-- src/core/audio/hle_core.cpp | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/include/audio/dsp_shared_mem.hpp b/include/audio/dsp_shared_mem.hpp index 25806ea1c..e776211d7 100644 --- a/include/audio/dsp_shared_mem.hpp +++ b/include/audio/dsp_shared_mem.hpp @@ -294,12 +294,12 @@ namespace Audio::HLE { struct SourceStatus { struct Status { - u8 isEnabled; ///< Is this channel enabled? (Doesn't have to be playing anything.) + u8 enabled; ///< Is this channel enabled? (Doesn't have to be playing anything.) u8 currentBufferIDDirty; ///< Non-zero when current_buffer_id changes u16_le syncCount; ///< Is set by the DSP to the value of SourceConfiguration::sync_count u32_dsp samplePosition; ///< Number of samples into the current buffer u16_le currentBufferID; ///< Updated when a buffer finishes playing - u16_le lastBufferID; ///< Updated when all buffers in the queue finish playing + u16_le previousBufferID; ///< Updated when all buffers in the queue finish playing }; Status status[sourceCount]; diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index e38d4821a..146c7bdfb 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -229,11 +229,11 @@ namespace Audio { // Update write region of shared memory auto& status = write.sourceStatuses.status[i]; - status.isEnabled = source.enabled; + status.enabled = source.enabled; status.syncCount = source.syncCount; status.currentBufferIDDirty = source.isBufferIDDirty ? 1 : 0; status.currentBufferID = source.currentBufferID; - status.lastBufferID = source.previousBufferID; + status.previousBufferID = source.previousBufferID; // TODO: Properly update sample position status.samplePosition = source.samplePosition; @@ -503,7 +503,7 @@ namespace Audio { } void HLE_DSP::handleAACRequest(const AAC::Message& request) { - AAC::Message response = {}; + AAC::Message response; switch (request.command) { case AAC::Command::EncodeDecode: @@ -514,6 +514,9 @@ namespace Audio { response.decodeResponse.sampleCount = 1024; response.decodeResponse.size = 0; response.decodeResponse.sampleRate = AAC::SampleRate::Rate48000; + + response.command = request.command; + response.mode = request.mode; break; case AAC::Command::Init: From 6a424a7a66bac534800746cd90b2d7d26786bf86 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 1 May 2024 16:20:24 +0300 Subject: [PATCH 48/60] Fix CI --- include/audio/aac.hpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/include/audio/aac.hpp b/include/audio/aac.hpp index c780e6d20..afd2dbbae 100644 --- a/include/audio/aac.hpp +++ b/include/audio/aac.hpp @@ -12,7 +12,7 @@ namespace Audio::AAC { }; } - // Enum values from Citra and struct definitions based off Citra + // Enum values and struct definitions based off Citra namespace Command { enum : u16 { Init = 0, // Initialize encoder/decoder @@ -46,12 +46,12 @@ namespace Audio::AAC { } struct DecodeResponse { - u32_le sampleRate = SampleRate::Rate48000; - u32_le channelCount = 0; - u32_le size = 0; - u32_le unknown1 = 0; - u32_le unknown2 = 0; - u32_le sampleCount = 0; + u32_le sampleRate; + u32_le channelCount; + u32_le size; + u32_le unknown1; + u32_le unknown2; + u32_le sampleCount; }; struct Message { @@ -61,11 +61,11 @@ namespace Audio::AAC { // Info on the AAC request union { - std::array commandData = {}; + std::array commandData{}; DecodeResponse decodeResponse; }; }; static_assert(sizeof(Message) == 32); static_assert(std::is_trivially_copyable()); -} // namespace Audio::AAC \ No newline at end of file +} // namespace Audio::AAC From 70f733ffb8437b51cfce6ad386a46d5bddfb6e7f Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 2 May 2024 00:22:13 +0300 Subject: [PATCH 49/60] GPU: Handle invalid floating point uniform writes --- include/PICA/shader.hpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/include/PICA/shader.hpp b/include/PICA/shader.hpp index 5b05e0b79..a9216b17f 100644 --- a/include/PICA/shader.hpp +++ b/include/PICA/shader.hpp @@ -256,8 +256,10 @@ class PICAShader { void uploadFloatUniform(u32 word) { floatUniformBuffer[floatUniformWordCount++] = word; - if (floatUniformIndex >= 96) { - Helpers::panic("[PICA] Tried to write float uniform %d", floatUniformIndex); + // Check if the program tries to upload to a non-existent uniform, and empty the queue without writing in that case + if (floatUniformIndex >= 96) [[unlikely]] { + floatUniformWordCount = 0; + return; } if ((f32UniformTransfer && floatUniformWordCount >= 4) || (!f32UniformTransfer && floatUniformWordCount >= 3)) { From 81932421cfe08cf0994bfa3a5dccb909e562a4cd Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 2 May 2024 00:28:13 +0300 Subject: [PATCH 50/60] Optimize float uniform setting --- include/PICA/shader.hpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/PICA/shader.hpp b/include/PICA/shader.hpp index a9216b17f..10f6ec88a 100644 --- a/include/PICA/shader.hpp +++ b/include/PICA/shader.hpp @@ -256,16 +256,16 @@ class PICAShader { void uploadFloatUniform(u32 word) { floatUniformBuffer[floatUniformWordCount++] = word; - // Check if the program tries to upload to a non-existent uniform, and empty the queue without writing in that case - if (floatUniformIndex >= 96) [[unlikely]] { - floatUniformWordCount = 0; - return; - } if ((f32UniformTransfer && floatUniformWordCount >= 4) || (!f32UniformTransfer && floatUniformWordCount >= 3)) { - vec4f& uniform = floatUniforms[floatUniformIndex++]; floatUniformWordCount = 0; + // Check if the program tries to upload to a non-existent uniform, and empty the queue without writing in that case + if (floatUniformIndex >= 96) [[unlikely]] { + return; + } + vec4f& uniform = floatUniforms[floatUniformIndex++]; + if (f32UniformTransfer) { uniform[0] = f24::fromFloat32(*(float*)&floatUniformBuffer[3]); uniform[1] = f24::fromFloat32(*(float*)&floatUniformBuffer[2]); From 66bcf384f38d3c86052a45f824f9854359229c76 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 7 May 2024 23:08:24 +0300 Subject: [PATCH 51/60] Qt: Add file patcher --- .gitmodules | 3 + CMakeLists.txt | 5 +- include/panda_qt/ellided_label.hpp | 21 +++++ include/panda_qt/main_window.hpp | 5 +- include/panda_qt/patch_window.hpp | 21 +++++ src/panda_qt/ellided_label.cpp | 25 ++++++ src/panda_qt/main_window.cpp | 4 + src/panda_qt/patch_window.cpp | 123 +++++++++++++++++++++++++++++ third_party/hips | 1 + 9 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 include/panda_qt/ellided_label.hpp create mode 100644 include/panda_qt/patch_window.hpp create mode 100644 src/panda_qt/ellided_label.cpp create mode 100644 src/panda_qt/patch_window.cpp create mode 160000 third_party/hips diff --git a/.gitmodules b/.gitmodules index 1f1d11fca..5a136acb6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -70,3 +70,6 @@ [submodule "third_party/capstone"] path = third_party/capstone url = https://github.com/capstone-engine/capstone +[submodule "third_party/hips"] + path = third_party/hips + url = https://github.com/wheremyfoodat/Hips diff --git a/CMakeLists.txt b/CMakeLists.txt index 48a2a0db8..dca69d6b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ include_directories(${PROJECT_SOURCE_DIR}/include/kernel) include_directories(${FMT_INCLUDE_DIR}) include_directories(third_party/boost/) include_directories(third_party/elfio/) +include_directories(third_party/hips/include/) include_directories(third_party/imgui/) include_directories(third_party/dynarmic/src) include_directories(third_party/cryptopp/) @@ -448,9 +449,11 @@ if(NOT BUILD_HYDRA_CORE) set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp src/panda_qt/mappings.cpp - ) + src/panda_qt/patch_window.cpp src/panda_qt/ellided_label.cpp + ) set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp + include/panda_qt/patch_window.hpp include/panda_qt/ellided_label.hpp ) source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) diff --git a/include/panda_qt/ellided_label.hpp b/include/panda_qt/ellided_label.hpp new file mode 100644 index 000000000..19fd8c74c --- /dev/null +++ b/include/panda_qt/ellided_label.hpp @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include +#include + +class EllidedLabel : public QLabel { + Q_OBJECT + public: + explicit EllidedLabel(Qt::TextElideMode elideMode = Qt::ElideLeft, QWidget* parent = nullptr); + explicit EllidedLabel(QString text, Qt::TextElideMode elideMode = Qt::ElideLeft, QWidget* parent = nullptr); + void setText(QString text); + + protected: + void resizeEvent(QResizeEvent* event); + + private: + void updateText(); + QString m_text; + Qt::TextElideMode m_elideMode; +}; \ No newline at end of file diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index 7e93bdf61..72725257b 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -17,6 +17,7 @@ #include "panda_qt/about_window.hpp" #include "panda_qt/cheats_window.hpp" #include "panda_qt/config_window.hpp" +#include "panda_qt/patch_window.hpp" #include "panda_qt/screen.hpp" #include "panda_qt/text_editor.hpp" #include "services/hid.hpp" @@ -90,13 +91,14 @@ class MainWindow : public QMainWindow { std::mutex messageQueueMutex; std::vector messageQueue; + QMenuBar* menuBar = nullptr; InputMappings keyboardMappings; ScreenWidget screen; AboutWindow* aboutWindow; ConfigWindow* configWindow; CheatsWindow* cheatsEditor; TextEditorWindow* luaEditor; - QMenuBar* menuBar = nullptr; + PatchWindow* patchWindow; // We use SDL's game controller API since it's the sanest API that supports as many controllers as possible SDL_GameController* gameController = nullptr; @@ -110,6 +112,7 @@ class MainWindow : public QMainWindow { void dumpRomFS(); void openLuaEditor(); void openCheatsEditor(); + void openPatchWindow(); void showAboutMenu(); void initControllers(); void pollControllers(); diff --git a/include/panda_qt/patch_window.hpp b/include/panda_qt/patch_window.hpp new file mode 100644 index 000000000..652c9a238 --- /dev/null +++ b/include/panda_qt/patch_window.hpp @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include + +#include "panda_qt/ellided_label.hpp" + +class PatchWindow final : public QWidget { + Q_OBJECT + + public: + PatchWindow(QWidget* parent = nullptr); + ~PatchWindow() = default; + + private: + std::filesystem::path inputPath = ""; + std::filesystem::path patchPath = ""; + + EllidedLabel* inputPathLabel = nullptr; + EllidedLabel* patchPathLabel = nullptr; +}; diff --git a/src/panda_qt/ellided_label.cpp b/src/panda_qt/ellided_label.cpp new file mode 100644 index 000000000..68c0da763 --- /dev/null +++ b/src/panda_qt/ellided_label.cpp @@ -0,0 +1,25 @@ +#include "panda_qt/ellided_label.hpp" + +// Based on https://stackoverflow.com/questions/7381100/text-overflow-for-a-qlabel-s-text-rendering-in-qt +EllidedLabel::EllidedLabel(Qt::TextElideMode elideMode, QWidget* parent) : EllidedLabel("", elideMode, parent) {} + +EllidedLabel::EllidedLabel(QString text, Qt::TextElideMode elideMode, QWidget* parent) : QLabel(parent) { + m_elideMode = elideMode; + setText(text); +} + +void EllidedLabel::setText(QString text) { + m_text = text; + updateText(); +} + +void EllidedLabel::resizeEvent(QResizeEvent* event) { + QLabel::resizeEvent(event); + updateText(); +} + +void EllidedLabel::updateText() { + QFontMetrics metrics(font()); + QString elided = metrics.elidedText(m_text, m_elideMode, width()); + QLabel::setText(elided); +} \ No newline at end of file diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index da9d27062..54e4fabe0 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -54,11 +54,13 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) auto dumpRomFSAction = toolsMenu->addAction(tr("Dump RomFS")); auto luaEditorAction = toolsMenu->addAction(tr("Open Lua Editor")); auto cheatsEditorAction = toolsMenu->addAction(tr("Open Cheats Editor")); + auto patchWindowAction = toolsMenu->addAction(tr("Open Patch Window")); auto dumpDspFirmware = toolsMenu->addAction(tr("Dump loaded DSP firmware")); connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS); connect(luaEditorAction, &QAction::triggered, this, &MainWindow::openLuaEditor); connect(cheatsEditorAction, &QAction::triggered, this, &MainWindow::openCheatsEditor); + connect(patchWindowAction, &QAction::triggered, this, &MainWindow::openPatchWindow); connect(dumpDspFirmware, &QAction::triggered, this, &MainWindow::dumpDspFirmware); auto aboutAction = aboutMenu->addAction(tr("About Panda3DS")); @@ -71,6 +73,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) aboutWindow = new AboutWindow(nullptr); configWindow = new ConfigWindow(this); cheatsEditor = new CheatsWindow(emu, {}, this); + patchWindow = new PatchWindow(this); luaEditor = new TextEditorWindow(this, "script.lua", ""); auto args = QCoreApplication::arguments(); @@ -293,6 +296,7 @@ void MainWindow::showAboutMenu() { void MainWindow::openLuaEditor() { luaEditor->show(); } void MainWindow::openCheatsEditor() { cheatsEditor->show(); } +void MainWindow::openPatchWindow() { patchWindow->show(); } void MainWindow::dispatchMessage(const EmulatorMessage& message) { switch (message.type) { diff --git a/src/panda_qt/patch_window.cpp b/src/panda_qt/patch_window.cpp new file mode 100644 index 000000000..98983865c --- /dev/null +++ b/src/panda_qt/patch_window.cpp @@ -0,0 +1,123 @@ +#include "panda_qt/patch_window.hpp" + +#include +#include +#include +#include +#include + +#include "hips.hpp" +#include "io_file.hpp" + +PatchWindow::PatchWindow(QWidget* parent) : QWidget(parent, Qt::Window) { + QVBoxLayout* layout = new QVBoxLayout; + layout->setContentsMargins(6, 6, 6, 6); + setLayout(layout); + + QWidget* inputBox = new QWidget; + QHBoxLayout* inputLayout = new QHBoxLayout; + QLabel* inputText = new QLabel(tr("Select input file")); + QPushButton* inputButton = new QPushButton(tr("Select")); + inputPathLabel = new EllidedLabel(""); + inputPathLabel->setFixedWidth(200); + + inputLayout->addWidget(inputText); + inputLayout->addWidget(inputButton); + inputLayout->addWidget(inputPathLabel); + inputBox->setLayout(inputLayout); + + QWidget* patchBox = new QWidget; + QHBoxLayout* patchLayout = new QHBoxLayout; + QLabel* patchText = new QLabel(tr("Select patch file")); + QPushButton* patchButton = new QPushButton(tr("Select")); + patchPathLabel = new EllidedLabel(""); + patchPathLabel->setFixedWidth(200); + + patchLayout->addWidget(patchText); + patchLayout->addWidget(patchButton); + patchLayout->addWidget(patchPathLabel); + patchBox->setLayout(patchLayout); + + QWidget* actionBox = new QWidget; + QHBoxLayout* actionLayout = new QHBoxLayout; + QPushButton* applyPatchButton = new QPushButton(tr("Apply patch")); + actionLayout->addWidget(applyPatchButton); + actionBox->setLayout(actionLayout); + + layout->addWidget(inputBox); + layout->addWidget(patchBox); + layout->addWidget(actionBox); + + connect(inputButton, &QPushButton::clicked, this, [this]() { + auto path = QFileDialog::getOpenFileName(this, tr("Select file to patch"), "", tr("All files (*.*)")); + inputPath = std::filesystem::path(path.toStdU16String()); + + inputPathLabel->setText(path); + }); + + connect(patchButton, &QPushButton::clicked, this, [this]() { + auto path = QFileDialog::getOpenFileName(this, tr("Select patch file"), "", tr("Patch files (*.ips *.ups *.bps)")); + patchPath = std::filesystem::path(path.toStdU16String()); + + patchPathLabel->setText(path); + }); + + connect(applyPatchButton, &QPushButton::clicked, this, [this]() { + if (inputPath.empty() || patchPath.empty()) { + printf("Pls set paths properly"); + return; + } + + auto path = QFileDialog::getSaveFileName(this, tr("Select file"), QString::fromStdU16String(inputPath.u16string()), tr("All files (*.*)")); + std::filesystem::path outputPath = std::filesystem::path(path.toStdU16String()); + + if (outputPath.empty()) { + printf("Pls set paths properly"); + return; + } + + Hips::PatchType patchType; + auto extension = patchPath.extension(); + + // Figure out what sort of patch we're dealing with + if (extension == ".ips") { + patchType = Hips::PatchType::IPS; + } else if (extension == ".ups") { + patchType = Hips::PatchType::UPS; + } else if (extension == ".bps") { + patchType = Hips::PatchType::BPS; + } else { + printf("Unknown patch format\n"); + return; + } + + // Read input and patch files into buffers + IOFile input(inputPath, "rb"); + IOFile patch(patchPath, "rb"); + + if (!input.isOpen() || !patch.isOpen()) { + printf("Failed to open input or patch file.\n"); + return; + } + + // Read the files into arrays + const auto inputSize = *input.size(); + const auto patchSize = *patch.size(); + + std::unique_ptr inputData(new uint8_t[inputSize]); + std::unique_ptr patchData(new uint8_t[patchSize]); + + input.rewind(); + patch.rewind(); + input.readBytes(inputData.get(), inputSize); + patch.readBytes(patchData.get(), patchSize); + + auto [bytes, result] = Hips::patch(inputData.get(), inputSize, patchData.get(), patchSize, patchType); + + // Write patched file + if (!bytes.empty()) { + IOFile output(outputPath, "wb"); + output.writeBytes(bytes.data(), bytes.size()); + } + }); +} \ No newline at end of file diff --git a/third_party/hips b/third_party/hips new file mode 160000 index 000000000..bbe8faf14 --- /dev/null +++ b/third_party/hips @@ -0,0 +1 @@ +Subproject commit bbe8faf149c4e10aaa45e2454fdb386e4cabf0cb From 332fbcfff184b98f8e6a521e7b8e834d89414177 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 7 May 2024 23:55:32 +0300 Subject: [PATCH 52/60] Qt: Add patching errors --- CMakeLists.txt | 2 +- docs/img/rpog_icon.png | Bin 0 -> 26925 bytes include/panda_qt/patch_window.hpp | 10 +++++++ src/panda_qt/patch_window.cpp | 45 ++++++++++++++++++++++++++---- 4 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 docs/img/rpog_icon.png diff --git a/CMakeLists.txt b/CMakeLists.txt index dca69d6b2..88ad6aeb4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -486,7 +486,7 @@ if(NOT BUILD_HYDRA_CORE) qt_add_resources(AlberCore "app_images" PREFIX "/" FILES - docs/img/rsob_icon.png docs/img/rstarstruck_icon.png + docs/img/rsob_icon.png docs/img/rstarstruck_icon.png docs/img/rpog_icon.png ) else() set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp src/panda_sdl/mappings.cpp) diff --git a/docs/img/rpog_icon.png b/docs/img/rpog_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b5426aed8a19dd97e2727087a0e44494574fab2f GIT binary patch literal 26925 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 m!U915`vy;YXKN47PtLCYf6N +#include #include #include @@ -13,6 +14,15 @@ class PatchWindow final : public QWidget { ~PatchWindow() = default; private: + // Show a message box + // Title: Title of the message box to display + // Message: Message to display + // Icon: The type of icon (error, warning, information, etc) to display + // IconPath: If non-null, then a path to an icon in our assets to display on the OK button + void displayMessage( + const QString& title, const QString& message, QMessageBox::Icon icon = QMessageBox::Icon::Warning, const char* iconPath = nullptr + ); + std::filesystem::path inputPath = ""; std::filesystem::path patchPath = ""; diff --git a/src/panda_qt/patch_window.cpp b/src/panda_qt/patch_window.cpp index 98983865c..de5cd2775 100644 --- a/src/panda_qt/patch_window.cpp +++ b/src/panda_qt/patch_window.cpp @@ -1,7 +1,9 @@ #include "panda_qt/patch_window.hpp" +#include #include #include +#include #include #include #include @@ -64,15 +66,20 @@ PatchWindow::PatchWindow(QWidget* parent) : QWidget(parent, Qt::Window) { connect(applyPatchButton, &QPushButton::clicked, this, [this]() { if (inputPath.empty() || patchPath.empty()) { - printf("Pls set paths properly"); + displayMessage(tr("Paths not provided correctly"), tr("Please provide paths for both the input file and the patch file")); return; } - auto path = QFileDialog::getSaveFileName(this, tr("Select file"), QString::fromStdU16String(inputPath.u16string()), tr("All files (*.*)")); + // std::filesystem::path only has += and not + for reasons unknown to humanity + auto defaultPath = inputPath.parent_path() / inputPath.stem(); + defaultPath += "-patched"; + defaultPath += inputPath.extension(); + + auto path = QFileDialog::getSaveFileName(this, tr("Select file"), QString::fromStdU16String(defaultPath.u16string()), tr("All files (*.*)")); std::filesystem::path outputPath = std::filesystem::path(path.toStdU16String()); if (outputPath.empty()) { - printf("Pls set paths properly"); + displayMessage(tr("No output path"), tr("No path was provided for the output file, no patching was done")); return; } @@ -87,7 +94,7 @@ PatchWindow::PatchWindow(QWidget* parent) : QWidget(parent, Qt::Window) { } else if (extension == ".bps") { patchType = Hips::PatchType::BPS; } else { - printf("Unknown patch format\n"); + displayMessage(tr("Unknown patch format"), tr("Unknown format for patch file. Currently IPS, UPS and BPS are supported")); return; } @@ -96,7 +103,7 @@ PatchWindow::PatchWindow(QWidget* parent) : QWidget(parent, Qt::Window) { IOFile patch(patchPath, "rb"); if (!input.isOpen() || !patch.isOpen()) { - printf("Failed to open input or patch file.\n"); + displayMessage(tr("Failed to open input files"), tr("Make sure they're in a directory Panda3DS has access to")); return; } @@ -119,5 +126,33 @@ PatchWindow::PatchWindow(QWidget* parent) : QWidget(parent, Qt::Window) { IOFile output(outputPath, "wb"); output.writeBytes(bytes.data(), bytes.size()); } + + switch (result) { + case Hips::Result::Success: + displayMessage( + tr("Patching Success"), tr("Your file was patched successfully."), QMessageBox::Icon::Information, ":/docs/img/rpog_icon.png" + ); + break; + + case Hips::Result::ChecksumMismatch: + displayMessage( + tr("Checksum mismatch"), + tr("Patch was applied successfully but a checksum mismatch was detected. The input or output files might not be correct") + ); + break; + + default: displayMessage(tr("Patching error"), tr("An error occured while patching")); break; + } }); +} + +void PatchWindow::PatchWindow::displayMessage(const QString& title, const QString& message, QMessageBox::Icon icon, const char* iconPath) { + QMessageBox messageBox(icon, title, message); + QAbstractButton* button = messageBox.addButton(tr("OK"), QMessageBox::ButtonRole::YesRole); + + if (iconPath != nullptr) { + button->setIcon(QIcon(iconPath)); + } + + messageBox.exec(); } \ No newline at end of file From aa7a6bfe7a17219a42b0830c8c646484eafa7852 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 8 May 2024 00:20:39 +0000 Subject: [PATCH 53/60] s/ellided/elided (#510) * s/ellided/elided * Fix header name --- CMakeLists.txt | 4 +-- .../{ellided_label.hpp => elided_label.hpp} | 6 ++--- include/panda_qt/patch_window.hpp | 2 +- src/panda_qt/elided_label.cpp | 25 +++++++++++++++++++ src/panda_qt/ellided_label.cpp | 25 ------------------- src/panda_qt/patch_window.cpp | 4 +-- 6 files changed, 33 insertions(+), 33 deletions(-) rename include/panda_qt/{ellided_label.hpp => elided_label.hpp} (53%) create mode 100644 src/panda_qt/elided_label.cpp delete mode 100644 src/panda_qt/ellided_label.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 88ad6aeb4..748c298b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -449,11 +449,11 @@ if(NOT BUILD_HYDRA_CORE) set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp src/panda_qt/mappings.cpp - src/panda_qt/patch_window.cpp src/panda_qt/ellided_label.cpp + src/panda_qt/patch_window.cpp src/panda_qt/elided_label.cpp ) set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp - include/panda_qt/patch_window.hpp include/panda_qt/ellided_label.hpp + include/panda_qt/patch_window.hpp include/panda_qt/elided_label.hpp ) source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) diff --git a/include/panda_qt/ellided_label.hpp b/include/panda_qt/elided_label.hpp similarity index 53% rename from include/panda_qt/ellided_label.hpp rename to include/panda_qt/elided_label.hpp index 19fd8c74c..9d937f9b1 100644 --- a/include/panda_qt/ellided_label.hpp +++ b/include/panda_qt/elided_label.hpp @@ -4,11 +4,11 @@ #include #include -class EllidedLabel : public QLabel { +class ElidedLabel : public QLabel { Q_OBJECT public: - explicit EllidedLabel(Qt::TextElideMode elideMode = Qt::ElideLeft, QWidget* parent = nullptr); - explicit EllidedLabel(QString text, Qt::TextElideMode elideMode = Qt::ElideLeft, QWidget* parent = nullptr); + explicit ElidedLabel(Qt::TextElideMode elideMode = Qt::ElideLeft, QWidget* parent = nullptr); + explicit ElidedLabel(QString text, Qt::TextElideMode elideMode = Qt::ElideLeft, QWidget* parent = nullptr); void setText(QString text); protected: diff --git a/include/panda_qt/patch_window.hpp b/include/panda_qt/patch_window.hpp index 06676a66a..ccffae4f1 100644 --- a/include/panda_qt/patch_window.hpp +++ b/include/panda_qt/patch_window.hpp @@ -4,7 +4,7 @@ #include #include -#include "panda_qt/ellided_label.hpp" +#include "panda_qt/elided_label.hpp" class PatchWindow final : public QWidget { Q_OBJECT diff --git a/src/panda_qt/elided_label.cpp b/src/panda_qt/elided_label.cpp new file mode 100644 index 000000000..f15cf11de --- /dev/null +++ b/src/panda_qt/elided_label.cpp @@ -0,0 +1,25 @@ +#include "panda_qt/elided_label.hpp" + +// Based on https://stackoverflow.com/questions/7381100/text-overflow-for-a-qlabel-s-text-rendering-in-qt +ElidedLabel::ElidedLabel(Qt::TextElideMode elideMode, QWidget* parent) : ElidedLabel("", elideMode, parent) {} + +ElidedLabel::ElidedLabel(QString text, Qt::TextElideMode elideMode, QWidget* parent) : QLabel(parent) { + m_elideMode = elideMode; + setText(text); +} + +void ElidedLabel::setText(QString text) { + m_text = text; + updateText(); +} + +void ElidedLabel::resizeEvent(QResizeEvent* event) { + QLabel::resizeEvent(event); + updateText(); +} + +void ElidedLabel::updateText() { + QFontMetrics metrics(font()); + QString elided = metrics.elidedText(m_text, m_elideMode, width()); + QLabel::setText(elided); +} \ No newline at end of file diff --git a/src/panda_qt/ellided_label.cpp b/src/panda_qt/ellided_label.cpp deleted file mode 100644 index 68c0da763..000000000 --- a/src/panda_qt/ellided_label.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "panda_qt/ellided_label.hpp" - -// Based on https://stackoverflow.com/questions/7381100/text-overflow-for-a-qlabel-s-text-rendering-in-qt -EllidedLabel::EllidedLabel(Qt::TextElideMode elideMode, QWidget* parent) : EllidedLabel("", elideMode, parent) {} - -EllidedLabel::EllidedLabel(QString text, Qt::TextElideMode elideMode, QWidget* parent) : QLabel(parent) { - m_elideMode = elideMode; - setText(text); -} - -void EllidedLabel::setText(QString text) { - m_text = text; - updateText(); -} - -void EllidedLabel::resizeEvent(QResizeEvent* event) { - QLabel::resizeEvent(event); - updateText(); -} - -void EllidedLabel::updateText() { - QFontMetrics metrics(font()); - QString elided = metrics.elidedText(m_text, m_elideMode, width()); - QLabel::setText(elided); -} \ No newline at end of file diff --git a/src/panda_qt/patch_window.cpp b/src/panda_qt/patch_window.cpp index de5cd2775..189288eb4 100644 --- a/src/panda_qt/patch_window.cpp +++ b/src/panda_qt/patch_window.cpp @@ -20,7 +20,7 @@ PatchWindow::PatchWindow(QWidget* parent) : QWidget(parent, Qt::Window) { QHBoxLayout* inputLayout = new QHBoxLayout; QLabel* inputText = new QLabel(tr("Select input file")); QPushButton* inputButton = new QPushButton(tr("Select")); - inputPathLabel = new EllidedLabel(""); + inputPathLabel = new ElidedLabel(""); inputPathLabel->setFixedWidth(200); inputLayout->addWidget(inputText); @@ -32,7 +32,7 @@ PatchWindow::PatchWindow(QWidget* parent) : QWidget(parent, Qt::Window) { QHBoxLayout* patchLayout = new QHBoxLayout; QLabel* patchText = new QLabel(tr("Select patch file")); QPushButton* patchButton = new QPushButton(tr("Select")); - patchPathLabel = new EllidedLabel(""); + patchPathLabel = new ElidedLabel(""); patchPathLabel->setFixedWidth(200); patchLayout->addWidget(patchText); From 9a50a57d327471a5a20a954285466dc00115d3ff Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 10 May 2024 02:13:58 +0300 Subject: [PATCH 54/60] Fix CI --- include/panda_qt/patch_window.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/panda_qt/patch_window.hpp b/include/panda_qt/patch_window.hpp index ccffae4f1..a6e1a1296 100644 --- a/include/panda_qt/patch_window.hpp +++ b/include/panda_qt/patch_window.hpp @@ -26,6 +26,6 @@ class PatchWindow final : public QWidget { std::filesystem::path inputPath = ""; std::filesystem::path patchPath = ""; - EllidedLabel* inputPathLabel = nullptr; - EllidedLabel* patchPathLabel = nullptr; + ElidedLabel* inputPathLabel = nullptr; + ElidedLabel* patchPathLabel = nullptr; }; From 2f9d5e30b409d0498c8a235b09b2a15181d43a75 Mon Sep 17 00:00:00 2001 From: NerduMiner Date: Sat, 11 May 2024 15:04:53 -0400 Subject: [PATCH 55/60] Index with iterator value in CAMService::startCapture rather than getSingleIndex() The port may have a value of 3 in this function, which will cause a panic. getPortIndices() handles this case for us already, so the iterator vale is safe to use --- src/core/services/cam.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/services/cam.cpp b/src/core/services/cam.cpp index b3dfd1dcf..d9c005e74 100644 --- a/src/core/services/cam.cpp +++ b/src/core/services/cam.cpp @@ -343,7 +343,7 @@ void CAMService::startCapture(u32 messagePointer) { if (port.isValid()) { for (int i : port.getPortIndices()) { - auto& event = ports[port.getSingleIndex()].receiveEvent; + auto& event = ports[i].receiveEvent; // Until we properly implement cameras, immediately signal the receive event if (event.has_value()) { From 85a17c3fcd507083192da82534b825bc90cbce44 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 13 May 2024 01:10:44 +0300 Subject: [PATCH 56/60] Add UBO support to opengl.hpp --- third_party/opengl/opengl.hpp | 94 +++++++++++++++++++++++++---------- 1 file changed, 68 insertions(+), 26 deletions(-) diff --git a/third_party/opengl/opengl.hpp b/third_party/opengl/opengl.hpp index f368f573a..9997e63b5 100644 --- a/third_party/opengl/opengl.hpp +++ b/third_party/opengl/opengl.hpp @@ -430,36 +430,36 @@ namespace OpenGL { glDispatchCompute(groupsX, groupsY, groupsZ); } - struct VertexBuffer { - GLuint m_handle = 0; + struct VertexBuffer { + GLuint m_handle = 0; - void create() { - if (m_handle == 0) { - glGenBuffers(1, &m_handle); - } - } + void create() { + if (m_handle == 0) { + glGenBuffers(1, &m_handle); + } + } - void createFixedSize(GLsizei size, GLenum usage = GL_DYNAMIC_DRAW) { - create(); - bind(); - glBufferData(GL_ARRAY_BUFFER, size, nullptr, usage); - } + void createFixedSize(GLsizei size, GLenum usage = GL_DYNAMIC_DRAW) { + create(); + bind(); + glBufferData(GL_ARRAY_BUFFER, size, nullptr, usage); + } - VertexBuffer(bool shouldCreate = false) { - if (shouldCreate) { - create(); - } - } + VertexBuffer(bool shouldCreate = false) { + if (shouldCreate) { + create(); + } + } #ifdef OPENGL_DESTRUCTORS - ~VertexBuffer() { free(); } -#endif - GLuint handle() const { return m_handle; } - bool exists() const { return m_handle != 0; } - void bind() const { glBindBuffer(GL_ARRAY_BUFFER, m_handle); } - void free() { glDeleteBuffers(1, &m_handle); } + ~VertexBuffer() { free(); } +#endif + GLuint handle() const { return m_handle; } + bool exists() const { return m_handle != 0; } + void bind() const { glBindBuffer(GL_ARRAY_BUFFER, m_handle); } + void free() { glDeleteBuffers(1, &m_handle); } - // Reallocates the buffer on every call. Prefer the sub version if possible. + // Reallocates the buffer on every call. Prefer the sub version if possible. template void bufferVerts(VertType* vertices, int vertCount, GLenum usage = GL_DYNAMIC_DRAW) { glBufferData(GL_ARRAY_BUFFER, sizeof(VertType) * vertCount, vertices, usage); @@ -471,7 +471,7 @@ namespace OpenGL { glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(VertType) * vertCount, vertices); } - // If C++20 is available, add overloads that take std::span instead of raw pointers + // If C++20 is available, add overloads that take std::span instead of raw pointers #ifdef OPENGL_HAVE_CPP20 template void bufferVerts(std::span vertices, GLenum usage = GL_DYNAMIC_DRAW) { @@ -485,6 +485,48 @@ namespace OpenGL { #endif }; + struct UniformBuffer { + GLuint m_handle = 0; + + void create() { + if (m_handle == 0) { + glGenBuffers(1, &m_handle); + } + } + + void createFixedSize(GLsizei size, GLenum usage = GL_DYNAMIC_DRAW) { + create(); + bind(); + glBufferData(GL_UNIFORM_BUFFER, size, nullptr, usage); + } + + UniformBuffer(bool shouldCreate = false) { + if (shouldCreate) { + create(); + } + } + +#ifdef OPENGL_DESTRUCTORS + ~UniformBuffer() { free(); } +#endif + GLuint handle() const { return m_handle; } + bool exists() const { return m_handle != 0; } + void bind() const { glBindBuffer(GL_UNIFORM_BUFFER, m_handle); } + void free() { glDeleteBuffers(1, &m_handle); } + + // Reallocates the buffer on every call. Prefer the sub version if possible. + template + void buffer(const UniformType& uniformData, GLenum usage = GL_DYNAMIC_DRAW) { + glBufferData(GL_UNIFORM_BUFFER, sizeof(uniformData), &uniformData, usage); + } + + // Only use if you used createFixedSize + template + void bufferSub(const UniformType& uniformData, int vertCount, GLintptr offset = 0) { + glBufferSubData(GL_UNIFORM_BUFFER, offset, sizeof(uniformData), &uniformData); + } + }; + enum DepthFunc { Never = GL_NEVER, // Depth test never passes Always = GL_ALWAYS, // Depth test always passes @@ -693,4 +735,4 @@ namespace OpenGL { using Rect = Rectangle; -} // end namespace OpenGL \ No newline at end of file +} // end namespace OpenGL From 43aff85381257fc805705b26c1aee9c0937dcc27 Mon Sep 17 00:00:00 2001 From: Ishan09811 <156402647+Ishan09811@users.noreply.github.com> Date: Sat, 8 Jun 2024 12:12:11 +0530 Subject: [PATCH 57/60] auto git versioning --- src/pandroid/app/build.gradle.kts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/pandroid/app/build.gradle.kts b/src/pandroid/app/build.gradle.kts index b67f94198..82b7cde29 100644 --- a/src/pandroid/app/build.gradle.kts +++ b/src/pandroid/app/build.gradle.kts @@ -10,8 +10,8 @@ android { applicationId = "com.panda3ds.pandroid" minSdk = 24 targetSdk = 33 - versionCode = 1 - versionName = "1.0" + versionCode = getVersionCode() + versionName = getVersion() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -54,3 +54,25 @@ dependencies { implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("com.google.code.gson:gson:2.10.1") } + +def getVersionName() { + def tag = '1.0' + try { + def process = 'git describe --tags --abbrev=0'.execute() + tag = process.text.trim() + if (tag.startsWith('v')) { + tag = tag.substring(1) + } + } catch (Exception e) { + println "Failed to get latest Git tag: ${e.message}" + } + return tag +} + +def getVersionCode() { + def versionCode = 1 + if (getVersionName() && getVersionName()[0].isDigit()) { + versionCode = getVersionName()[0].toInteger() + } + return versionCode +} From ff3449067861209f5bbabf939bfefd99f6022fd6 Mon Sep 17 00:00:00 2001 From: Ishan09811 <156402647+Ishan09811@users.noreply.github.com> Date: Sat, 8 Jun 2024 12:15:00 +0530 Subject: [PATCH 58/60] typo --- src/pandroid/app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pandroid/app/build.gradle.kts b/src/pandroid/app/build.gradle.kts index 82b7cde29..e0ada6d4d 100644 --- a/src/pandroid/app/build.gradle.kts +++ b/src/pandroid/app/build.gradle.kts @@ -11,7 +11,7 @@ android { minSdk = 24 targetSdk = 33 versionCode = getVersionCode() - versionName = getVersion() + versionName = getVersionName() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" From 93ed2284df8eb4aa28557eccfa9be7d3e117b866 Mon Sep 17 00:00:00 2001 From: Ishan09811 <156402647+Ishan09811@users.noreply.github.com> Date: Sat, 8 Jun 2024 15:04:13 +0530 Subject: [PATCH 59/60] forgot that is build.gradle.kts not build.gradle --- src/pandroid/app/build.gradle.kts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/pandroid/app/build.gradle.kts b/src/pandroid/app/build.gradle.kts index e0ada6d4d..6493ef97d 100644 --- a/src/pandroid/app/build.gradle.kts +++ b/src/pandroid/app/build.gradle.kts @@ -55,24 +55,25 @@ dependencies { implementation("com.google.code.gson:gson:2.10.1") } -def getVersionName() { - def tag = '1.0' +fun getVersionName(): String { + var tag = "1.0" try { - def process = 'git describe --tags --abbrev=0'.execute() - tag = process.text.trim() - if (tag.startsWith('v')) { + val process = Runtime.getRuntime().exec("git describe --tags --abbrev=0") + tag = process.inputStream.bufferedReader().readText().trim() + if (tag.startsWith("v")) { tag = tag.substring(1) } - } catch (Exception e) { - println "Failed to get latest Git tag: ${e.message}" + } catch (e: Exception) { + println("Failed to get latest Git tag: ${e.message}") } return tag } -def getVersionCode() { - def versionCode = 1 - if (getVersionName() && getVersionName()[0].isDigit()) { - versionCode = getVersionName()[0].toInteger() +fun getVersionCode(): Int { + var versionCode = 1 + val tag = getVersionName() + if (tag.isNotEmpty() && tag[0].isDigit()) { + versionCode = tag[0].toString().toInt() } return versionCode } From fb42642e45bd5f92a14b5573bc03ece65ac022ac Mon Sep 17 00:00:00 2001 From: Ishan09811 <156402647+Ishan09811@users.noreply.github.com> Date: Sat, 8 Jun 2024 18:11:47 +0530 Subject: [PATCH 60/60] bonk --- .github/workflows/Android_Build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/Android_Build.yml b/.github/workflows/Android_Build.yml index 11811f8b1..af3172c86 100644 --- a/.github/workflows/Android_Build.yml +++ b/.github/workflows/Android_Build.yml @@ -4,6 +4,8 @@ on: push: branches: - master + tags: + - '*' pull_request: jobs: